mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-27 04:43:54 +01:00
Synced branch with master
This commit is contained in:
commit
4fc3040d52
479 changed files with 11666 additions and 3806 deletions
15
.travis.yml
15
.travis.yml
|
|
@ -17,14 +17,17 @@ jdk:
|
||||||
|
|
||||||
android:
|
android:
|
||||||
components:
|
components:
|
||||||
- platform-tools
|
|
||||||
- tools
|
- tools
|
||||||
- build-tools-26.0.1
|
- platform-tools
|
||||||
|
- build-tools-26.0.2
|
||||||
- extra-google-m2repository
|
- extra-google-m2repository
|
||||||
- extra-android-m2repository
|
- extra-android-m2repository
|
||||||
- ${ANDROID_TARGET}
|
- ${ANDROID_TARGET}
|
||||||
- android-25
|
- android-25
|
||||||
|
- android-26
|
||||||
- sys-img-${ANDROID_ABI}-${ANDROID_TARGET}
|
- sys-img-${ANDROID_ABI}-${ANDROID_TARGET}
|
||||||
|
licenses:
|
||||||
|
- 'android-sdk-license-.+'
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- echo no | android create avd --force -n test -t $ANDROID_TARGET --abi $ANDROID_ABI
|
- echo no | android create avd --force -n test -t $ANDROID_TARGET --abi $ANDROID_ABI
|
||||||
|
|
@ -32,14 +35,16 @@ before_script:
|
||||||
- android-wait-for-emulator
|
- android-wait-for-emulator
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- ./gradlew clean check connectedCheck jacocoTestReport --stacktrace
|
- ./gradlew clean check connectedCheck jacocoTestReport
|
||||||
|
|
||||||
after_success:
|
after_success:
|
||||||
- bash <(curl -s https://codecov.io/bash)
|
- bash <(curl -s https://codecov.io/bash)
|
||||||
|
|
||||||
after_failure:
|
after_failure:
|
||||||
- echo '*** Connected Test Rsults ***'
|
- echo '*** Debug Unit Test Results ***'
|
||||||
- w3m -dump ${TRAVIS_BUILD_DIR}/app/build/reports/androidTests/connected/*Test.html
|
- w3m -dump ${TRAVIS_BUILD_DIR}/app/build/reports/tests/*/classes/*Test.html
|
||||||
|
- echo '*** Connected Test Results ***'
|
||||||
|
- w3m -dump ${TRAVIS_BUILD_DIR}/app/build/reports/androidTests/connected/flavors/*/*Test.html
|
||||||
|
|
||||||
before_cache:
|
before_cache:
|
||||||
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
||||||
|
|
|
||||||
42
CHANGELOG.md
42
CHANGELOG.md
|
|
@ -1,5 +1,47 @@
|
||||||
# Wikimedia Commons for Android
|
# Wikimedia Commons for Android
|
||||||
|
|
||||||
|
## v2.6.7
|
||||||
|
- Added null checks to prevent frequent crashes in ModificationsSyncAdapter
|
||||||
|
|
||||||
|
## v2.6.6
|
||||||
|
- Refactored Dagger to fix crashes encountered in production
|
||||||
|
- Fixed "?" displaying in description of Nearby places
|
||||||
|
- Database-related cleanup and tests
|
||||||
|
- Optimized dimens.xml
|
||||||
|
- Fixed issue where map opens with incorrect coordinates
|
||||||
|
|
||||||
|
## v2.6.5 beta
|
||||||
|
- Changed "send log" feature to only send logs to private Google group forum
|
||||||
|
- Switched to using Wikimedia maps server instead of Mapbox for privacy reasons
|
||||||
|
- Removed event logging from app for privacy reasons
|
||||||
|
- Fixed crash caused by rapidly switching from Nearby map to list while loading
|
||||||
|
|
||||||
|
## v2.6.4 beta
|
||||||
|
- Excluded httpclient and commons-logging to fix release build errors
|
||||||
|
- Fixed crashes caused by Fresco and Dagger
|
||||||
|
|
||||||
|
## v2.6.3 beta
|
||||||
|
- Same as 2.6.2 except with localizations added for Google Code-In
|
||||||
|
|
||||||
|
## v2.6.2 beta
|
||||||
|
- Reverted temporarily to last stable version while working on crash fix
|
||||||
|
|
||||||
|
## v2.6.1 beta
|
||||||
|
- Failed attempt to fix crashes in release build with the previous beta release
|
||||||
|
|
||||||
|
## v2.6.0 beta
|
||||||
|
- Multiple bugfixes for location updates and list/map loading in Nearby
|
||||||
|
- Multiple fixes for various crashes and memory leaks
|
||||||
|
- Added several unit tests
|
||||||
|
- Modified About page to include WMF disclaimer and modified Privacy Policy link to point to our individual privacy policy
|
||||||
|
- Added option for users to send logs to developers (has to be manually activated by user)
|
||||||
|
- Converted PNGs to WebPs
|
||||||
|
- Improved login screen with new design and privacy policy link
|
||||||
|
- Improved category display, if a category has an exact name entered, it will be shown first
|
||||||
|
- New UI for Nearby list
|
||||||
|
- Added product flavors for production and the beta-cluster Wikimedia servers
|
||||||
|
- Various improvements to navigation flow and backstack
|
||||||
|
|
||||||
## v2.5.0 beta
|
## v2.5.0 beta
|
||||||
- Added one-time popup for beta users to provide feedback on IEG renewal proposal
|
- Added one-time popup for beta users to provide feedback on IEG renewal proposal
|
||||||
- Added link to Commons policies in ShareActivity
|
- Added link to Commons policies in ShareActivity
|
||||||
|
|
|
||||||
1
CONTRIBUTING.md
Normal file
1
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Please see our guidelines in the wiki: https://github.com/commons-app/apps-android-commons/wiki/Volunteers-welcome%21
|
||||||
951
CREDITS
951
CREDITS
|
|
@ -29,6 +29,7 @@ their contribution to the product.
|
||||||
* Jan Piotrowski
|
* Jan Piotrowski
|
||||||
* Bruke Mekuria Mulugeta
|
* Bruke Mekuria Mulugeta
|
||||||
* Paul Hawke
|
* Paul Hawke
|
||||||
|
* Vishan Seru
|
||||||
|
|
||||||
3rd party open source libraries used:
|
3rd party open source libraries used:
|
||||||
* Butterknife
|
* Butterknife
|
||||||
|
|
@ -38,3 +39,953 @@ their contribution to the product.
|
||||||
|
|
||||||
3rd party open source apps from which significant code has been reused:
|
3rd party open source apps from which significant code has been reused:
|
||||||
* Android Wikipedia app https://github.com/wikimedia/apps-android-wikipedia
|
* Android Wikipedia app https://github.com/wikimedia/apps-android-wikipedia
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
The Wikimedia Commons Android app uses portions of MapBox.
|
||||||
|
|
||||||
|
mapbox-gl-native copyright (c) 2014-2018 Mapbox.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in
|
||||||
|
the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
||||||
|
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||||
|
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||||
|
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of Android Gesture Detectors Framework.
|
||||||
|
|
||||||
|
Copyright (c) 2012, Almer Thie
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||||
|
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
|
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of Android Support Library.
|
||||||
|
|
||||||
|
Copyright (c) 2005-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.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of Boost.
|
||||||
|
|
||||||
|
Distributed under the Boost Software License, Version 1.0.
|
||||||
|
|
||||||
|
http://www.boost.org/LICENSE_1_0.txt
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of Clipper.
|
||||||
|
|
||||||
|
Author : Angus Johnson
|
||||||
|
Version : 6.1.3a
|
||||||
|
Date : 22 January 2014
|
||||||
|
Website : http://www.angusj.com
|
||||||
|
Copyright : Angus Johnson 2010-2014
|
||||||
|
|
||||||
|
License:
|
||||||
|
Use, modification & distribution is subject to Boost Software License Ver 1.
|
||||||
|
http://www.boost.org/LICENSE_1_0.txt
|
||||||
|
|
||||||
|
Attributions:
|
||||||
|
The code in this library is an extension of Bala Vatti's clipping algorithm:
|
||||||
|
"A generic solution to polygon clipping"
|
||||||
|
Communications of the ACM, Vol 35, Issue 7 (July 1992) pp 56-63.
|
||||||
|
http://portal.acm.org/citation.cfm?id=129906
|
||||||
|
|
||||||
|
Computer graphics and geometric modeling: implementation and algorithms
|
||||||
|
By Max K. Agoston
|
||||||
|
Springer; 1 edition (January 4, 2005)
|
||||||
|
http://books.google.com/books?q=vatti+clipping+agoston
|
||||||
|
|
||||||
|
See also:
|
||||||
|
"Polygon Offsetting by Computing Winding Numbers"
|
||||||
|
Paper no. DETC2005-85513 pp. 565-575
|
||||||
|
ASME 2005 International Design Engineering Technical Conferences
|
||||||
|
and Computers and Information in Engineering Conference (IDETC/CIE2005)
|
||||||
|
September 24-28, 2005 , Long Beach, California, USA
|
||||||
|
http://www.me.berkeley.edu/~mcmains/pubs/DAC05OffsetPolygon.pdf
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of BugshotKit.
|
||||||
|
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 marcoarment
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of CSS Color Parser.
|
||||||
|
|
||||||
|
(c) Dean McNamee <dean@gmail.com>, 2012.
|
||||||
|
C++ port by Konstantin Käfer <mail@kkaefer.com>, 2014.
|
||||||
|
|
||||||
|
https://github.com/deanm/css-color-parser-js
|
||||||
|
https://github.com/kkaefer/css-color-parser-cpp
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of GLFW.
|
||||||
|
|
||||||
|
Copyright (c) 2002-2006 Marcus Geelnard
|
||||||
|
Copyright (c) 2006-2010 Camilla Berglund <elmindreda@elmindreda.org>
|
||||||
|
|
||||||
|
This software is provided 'as-is', without any express or implied
|
||||||
|
warranty. In no event will the authors be held liable for any damages
|
||||||
|
arising from the use of this software.
|
||||||
|
|
||||||
|
Permission is granted to anyone to use this software for any purpose,
|
||||||
|
including commercial applications, and to alter it and redistribute it
|
||||||
|
freely, subject to the following restrictions:
|
||||||
|
|
||||||
|
1. The origin of this software must not be misrepresented; you must not
|
||||||
|
claim that you wrote the original software. If you use this software
|
||||||
|
in a product, an acknowledgment in the product documentation would
|
||||||
|
be appreciated but is not required.
|
||||||
|
|
||||||
|
2. Altered source versions must be plainly marked as such, and must not
|
||||||
|
be misrepresented as being the original software.
|
||||||
|
|
||||||
|
3. This notice may not be removed or altered from any source
|
||||||
|
distribution.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of libc++.
|
||||||
|
|
||||||
|
The libc++ library is dual licensed under both the University of Illinois
|
||||||
|
"BSD-Like" license and the MIT license. As a user of this code you may choose
|
||||||
|
to use it under either license. As a contributor, you agree to allow your code
|
||||||
|
to be used under both.
|
||||||
|
|
||||||
|
Full text of the relevant licenses is included below.
|
||||||
|
|
||||||
|
====
|
||||||
|
|
||||||
|
University of Illinois/NCSA
|
||||||
|
Open Source License
|
||||||
|
|
||||||
|
Copyright (c) 2009-2015 by the contributors listed in CREDITS.TXT
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Developed by:
|
||||||
|
|
||||||
|
LLVM Team
|
||||||
|
|
||||||
|
University of Illinois at Urbana-Champaign
|
||||||
|
|
||||||
|
http://llvm.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 with
|
||||||
|
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:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimers.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimers in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
* Neither the names of the LLVM Team, University of Illinois at
|
||||||
|
Urbana-Champaign, nor the names of its contributors may be used to
|
||||||
|
endorse or promote products derived from this Software without specific
|
||||||
|
prior written permission.
|
||||||
|
|
||||||
|
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
|
||||||
|
CONTRIBUTORS 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 WITH THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
====
|
||||||
|
|
||||||
|
Copyright (c) 2009-2014 by the contributors listed in CREDITS.TXT
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of libcurl.
|
||||||
|
|
||||||
|
COPYRIGHT AND PERMISSION NOTICE
|
||||||
|
|
||||||
|
Copyright (c) 1996 - 2015, Daniel Stenberg, <daniel@haxx.se>.
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and distribute this software for any purpose
|
||||||
|
with or without fee is hereby granted, provided that the above copyright
|
||||||
|
notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
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 OF THIRD PARTY RIGHTS. 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.
|
||||||
|
|
||||||
|
Except as contained in this notice, the name of a copyright holder shall not
|
||||||
|
be used in advertising or otherwise to promote the sale, use or other dealings
|
||||||
|
in this Software without prior written authorization of the copyright holder.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of libjpeg-turbo.
|
||||||
|
|
||||||
|
This software is based in part on the work of the Independent JPEG Group.
|
||||||
|
|
||||||
|
Copyright (C)2009-2015 D. R. Commander. All Rights Reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
- Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer.
|
||||||
|
- Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
- Neither the name of the libjpeg-turbo Project nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from this
|
||||||
|
software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS",
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
|
||||||
|
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
TurboJPEG/LJT: this implements the TurboJPEG API using libjpeg or libjpeg-turbo
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of libpng.
|
||||||
|
|
||||||
|
This copy of the libpng notices is provided for your convenience. In case of
|
||||||
|
any discrepancy between this copy and the notices in the file png.h that is
|
||||||
|
included in the libpng distribution, the latter shall prevail.
|
||||||
|
|
||||||
|
COPYRIGHT NOTICE, DISCLAIMER, and LICENSE:
|
||||||
|
|
||||||
|
If you modify libpng you may insert additional notices immediately following
|
||||||
|
this sentence.
|
||||||
|
|
||||||
|
This code is released under the libpng license.
|
||||||
|
|
||||||
|
libpng versions 1.0.7, July 1, 2000, through 1.6.18, July 23, 2015, are
|
||||||
|
Copyright (c) 2000-2002, 2004, 2006-2015 Glenn Randers-Pehrson, and are
|
||||||
|
distributed according to the same disclaimer and license as libpng-1.0.6
|
||||||
|
with the following individuals added to the list of Contributing Authors:
|
||||||
|
|
||||||
|
Simon-Pierre Cadieux
|
||||||
|
Eric S. Raymond
|
||||||
|
Mans Rullgard
|
||||||
|
Cosmin Truta
|
||||||
|
Gilles Vollant
|
||||||
|
James Yu
|
||||||
|
|
||||||
|
and with the following additions to the disclaimer:
|
||||||
|
|
||||||
|
There is no warranty against interference with your enjoyment of the
|
||||||
|
library or against infringement. There is no warranty that our
|
||||||
|
efforts or the library will fulfill any of your particular purposes
|
||||||
|
or needs. This library is provided with all faults, and the entire
|
||||||
|
risk of satisfactory quality, performance, accuracy, and effort is with
|
||||||
|
the user.
|
||||||
|
|
||||||
|
libpng versions 0.97, January 1998, through 1.0.6, March 20, 2000, are
|
||||||
|
Copyright (c) 1998-2000 Glenn Randers-Pehrson, and are distributed according
|
||||||
|
to the same disclaimer and license as libpng-0.96, with the following
|
||||||
|
individuals added to the list of Contributing Authors:
|
||||||
|
|
||||||
|
Tom Lane
|
||||||
|
Glenn Randers-Pehrson
|
||||||
|
Willem van Schaik
|
||||||
|
|
||||||
|
libpng versions 0.89, June 1996, through 0.96, May 1997, are
|
||||||
|
Copyright (c) 1996-1997 Andreas Dilger, and are
|
||||||
|
distributed according to the same disclaimer and license as libpng-0.88,
|
||||||
|
with the following individuals added to the list of Contributing Authors:
|
||||||
|
|
||||||
|
John Bowler
|
||||||
|
Kevin Bracey
|
||||||
|
Sam Bushell
|
||||||
|
Magnus Holmgren
|
||||||
|
Greg Roelofs
|
||||||
|
Tom Tanner
|
||||||
|
|
||||||
|
libpng versions 0.5, May 1995, through 0.88, January 1996, are
|
||||||
|
Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc.
|
||||||
|
|
||||||
|
For the purposes of this copyright and license, "Contributing Authors"
|
||||||
|
is defined as the following set of individuals:
|
||||||
|
|
||||||
|
Andreas Dilger
|
||||||
|
Dave Martindale
|
||||||
|
Guy Eric Schalnat
|
||||||
|
Paul Schmidt
|
||||||
|
Tim Wegner
|
||||||
|
|
||||||
|
The PNG Reference Library is supplied "AS IS". The Contributing Authors
|
||||||
|
and Group 42, Inc. disclaim all warranties, expressed or implied,
|
||||||
|
including, without limitation, the warranties of merchantability and of
|
||||||
|
fitness for any purpose. The Contributing Authors and Group 42, Inc.
|
||||||
|
assume no liability for direct, indirect, incidental, special, exemplary,
|
||||||
|
or consequential damages, which may result from the use of the PNG
|
||||||
|
Reference Library, even if advised of the possibility of such damage.
|
||||||
|
|
||||||
|
Permission is hereby granted to use, copy, modify, and distribute this
|
||||||
|
source code, or portions hereof, for any purpose, without fee, subject
|
||||||
|
to the following restrictions:
|
||||||
|
|
||||||
|
1. The origin of this source code must not be misrepresented.
|
||||||
|
|
||||||
|
2. Altered versions must be plainly marked as such and must not
|
||||||
|
be misrepresented as being the original source.
|
||||||
|
|
||||||
|
3. This Copyright notice may not be removed or altered from any
|
||||||
|
source or altered source distribution.
|
||||||
|
|
||||||
|
The Contributing Authors and Group 42, Inc. specifically permit, without
|
||||||
|
fee, and encourage the use of this source code as a component to
|
||||||
|
supporting the PNG file format in commercial products. If you use this
|
||||||
|
source code in a product, acknowledgment is not required but would be
|
||||||
|
appreciated.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of libuv.
|
||||||
|
|
||||||
|
libuv is part of the Node project: http://nodejs.org/
|
||||||
|
libuv may be distributed alone under Node's license:
|
||||||
|
|
||||||
|
====
|
||||||
|
|
||||||
|
Copyright Joyent, Inc. and other Node contributors. All rights reserved.
|
||||||
|
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.
|
||||||
|
|
||||||
|
====
|
||||||
|
|
||||||
|
This license applies to all parts of libuv that are not externally
|
||||||
|
maintained libraries.
|
||||||
|
|
||||||
|
The externally maintained libraries used by libuv are:
|
||||||
|
|
||||||
|
- tree.h (from FreeBSD), copyright Niels Provos. Two clause BSD license.
|
||||||
|
|
||||||
|
- inet_pton and inet_ntop implementations, contained in src/inet.c, are
|
||||||
|
copyright the Internet Systems Consortium, Inc., and licensed under the ISC
|
||||||
|
license.
|
||||||
|
|
||||||
|
- stdint-msvc2008.h (from msinttypes), copyright Alexander Chemeris. Three
|
||||||
|
clause BSD license.
|
||||||
|
|
||||||
|
- pthread-fixes.h, pthread-fixes.c, copyright Google Inc. and Sony Mobile
|
||||||
|
Communications AB. Three clause BSD license.
|
||||||
|
|
||||||
|
- android-ifaddrs.h, android-ifaddrs.c, copyright Berkeley Software Design
|
||||||
|
Inc, Kenneth MacKay and Emergya (Cloud4all, FP7/2007-2013, grant agreement
|
||||||
|
n° 289016). Three clause BSD license.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of libzip.
|
||||||
|
|
||||||
|
Copyright (C) 1999-2014 Dieter Baron and Thomas Klausner
|
||||||
|
|
||||||
|
The authors can be contacted at <libzip@nih.at>
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in
|
||||||
|
the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
|
||||||
|
3. The names of the authors may not be used to endorse or promote
|
||||||
|
products derived from this software without specific prior
|
||||||
|
written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS
|
||||||
|
OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY
|
||||||
|
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
||||||
|
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||||
|
IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
||||||
|
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
|
||||||
|
IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of LOST.
|
||||||
|
|
||||||
|
Copyright (c) 2014 Mapzen
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of the Mapbox iOS SDK, which was derived from the
|
||||||
|
Route-Me open source project, including the Alpstein fork of it.
|
||||||
|
|
||||||
|
The Route-Me license appears below.
|
||||||
|
|
||||||
|
Copyright (c) 2008-2013, Route-Me Contributors
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||||
|
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of nunicode.
|
||||||
|
|
||||||
|
Copyright (c) 2013 Aleksey Tulinov <aleksey.tulinov@gmail.com>
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of OkHTTP.
|
||||||
|
|
||||||
|
Copyright 2014 Square, Inc.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of OpenSSL.
|
||||||
|
|
||||||
|
LICENSE ISSUES
|
||||||
|
==============
|
||||||
|
|
||||||
|
The OpenSSL toolkit stays under a dual license, i.e. both the conditions of
|
||||||
|
the OpenSSL License and the original SSLeay license apply to the toolkit.
|
||||||
|
See below for the actual license texts. Actually both licenses are BSD-style
|
||||||
|
Open Source licenses. In case of any license issues related to OpenSSL
|
||||||
|
please contact openssl-core@openssl.org.
|
||||||
|
|
||||||
|
OpenSSL License
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Copyright (c) 1998-2011 The OpenSSL Project. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in
|
||||||
|
the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
|
||||||
|
3. All advertising materials mentioning features or use of this
|
||||||
|
software must display the following acknowledgment:
|
||||||
|
"This product includes software developed by the OpenSSL Project
|
||||||
|
for use in the OpenSSL Toolkit. (http://www.openssl.org/)"
|
||||||
|
|
||||||
|
4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to
|
||||||
|
endorse or promote products derived from this software without
|
||||||
|
prior written permission. For written permission, please contact
|
||||||
|
openssl-core@openssl.org.
|
||||||
|
|
||||||
|
5. Products derived from this software may not be called "OpenSSL"
|
||||||
|
nor may "OpenSSL" appear in their names without prior written
|
||||||
|
permission of the OpenSSL Project.
|
||||||
|
|
||||||
|
6. Redistributions of any form whatsoever must retain the following
|
||||||
|
acknowledgment:
|
||||||
|
"This product includes software developed by the OpenSSL Project
|
||||||
|
for use in the OpenSSL Toolkit (http://www.openssl.org/)"
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY
|
||||||
|
EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR
|
||||||
|
ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||||
|
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||||
|
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||||
|
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
|
||||||
|
OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
This product includes cryptographic software written by Eric Young
|
||||||
|
(eay@cryptsoft.com). This product includes software written by Tim
|
||||||
|
Hudson (tjh@cryptsoft.com).
|
||||||
|
|
||||||
|
Original SSLeay License
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com)
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This package is an SSL implementation written
|
||||||
|
by Eric Young (eay@cryptsoft.com).
|
||||||
|
The implementation was written so as to conform with Netscapes SSL.
|
||||||
|
|
||||||
|
This library is free for commercial and non-commercial use as long as
|
||||||
|
The following conditions are aheared to. The following conditions
|
||||||
|
apply to all code found in this distribution, be it the RC4, RSA,
|
||||||
|
lhash, DES, etc., code; not just the SSL code. The SSL documentation
|
||||||
|
included with this distribution is covered by the same copyright terms
|
||||||
|
except that the holder is Tim Hudson (tjh@cryptsoft.com).
|
||||||
|
|
||||||
|
Copyright remains Eric Young's, and as such any Copyright notices in
|
||||||
|
the code are not to be removed.
|
||||||
|
If this package is used in a product, Eric Young should be given attribution
|
||||||
|
as the author of the parts of the library used.
|
||||||
|
This can be in the form of a textual message at program startup or
|
||||||
|
in documentation (online or textual) provided with the package.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
1. Redistributions of source code must retain the copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
3. All advertising materials mentioning features or use of this software
|
||||||
|
must display the following acknowledgement:
|
||||||
|
"This product includes cryptographic software written by
|
||||||
|
Eric Young (eay@cryptsoft.com)"
|
||||||
|
The word 'cryptographic' can be left out if the rouines from the library
|
||||||
|
being used are not cryptographic related :-).
|
||||||
|
4. If you include any Windows specific code (or a derivative thereof) from
|
||||||
|
the apps directory (application code) you must include an acknowledgement:
|
||||||
|
"This product includes software written by Tim Hudson (tjh@cryptsoft.com)"
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||||
|
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||||
|
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||||
|
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||||
|
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGE.
|
||||||
|
|
||||||
|
The licence and distribution terms for any publically available version or
|
||||||
|
derivative of this code cannot be changed. i.e. this code cannot simply be
|
||||||
|
copied and put under another distribution licence
|
||||||
|
[including the GNU Public Licence.]
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of RapidJSON.
|
||||||
|
|
||||||
|
Tencent is pleased to support the open source community by making RapidJSON
|
||||||
|
available.
|
||||||
|
|
||||||
|
Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. All rights
|
||||||
|
reserved.
|
||||||
|
|
||||||
|
If you have downloaded a copy of the RapidJSON binary from Tencent, please note
|
||||||
|
that the RapidJSON binary is licensed under the MIT License. If you have
|
||||||
|
downloaded a copy of the RapidJSON source code from Tencent, please note that
|
||||||
|
RapidJSON source code is licensed under the MIT License, except for the third-
|
||||||
|
party components listed below which are subject to different license terms.
|
||||||
|
Your integration of RapidJSON into your own projects may require compliance with
|
||||||
|
the MIT License, as well as the other licenses applicable to the third-party
|
||||||
|
components included within RapidJSON. To avoid the problematic JSON license in
|
||||||
|
your own projects, it's sufficient to exclude the bin/jsonchecker/ directory, as
|
||||||
|
it's the only code under the JSON license. A copy of the MIT License is included
|
||||||
|
in this file.
|
||||||
|
|
||||||
|
Other dependencies and licenses:
|
||||||
|
|
||||||
|
Open Source Software Licensed Under the BSD License:
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
|
||||||
|
The msinttypes r29
|
||||||
|
Copyright (c) 2006-2013 Alexander Chemeris
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
* Neither the name of copyright holder nor the names of its contributors may be
|
||||||
|
used to endorse or promote products derived from this software without
|
||||||
|
specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
|
||||||
|
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
|
||||||
|
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
|
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
Open Source Software Licensed Under the JSON License:
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
|
||||||
|
json.org
|
||||||
|
Copyright (c) 2002 JSON.org
|
||||||
|
All Rights Reserved.
|
||||||
|
|
||||||
|
JSON_checker
|
||||||
|
Copyright (c) 2002 JSON.org
|
||||||
|
All Rights Reserved.
|
||||||
|
|
||||||
|
Terms of the JSON License:
|
||||||
|
---------------------------------------------------
|
||||||
|
|
||||||
|
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 shall be used for Good, not Evil.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Terms of the MIT License:
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of Reachability.
|
||||||
|
|
||||||
|
Copyright (c) 2011, Tony Million.
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of SQLite.
|
||||||
|
|
||||||
|
2001 September 15
|
||||||
|
|
||||||
|
The author disclaims copyright to this source code. In place of
|
||||||
|
a legal notice, here is a blessing:
|
||||||
|
|
||||||
|
May you do good and not evil.
|
||||||
|
May you find forgiveness for yourself and forgive others.
|
||||||
|
May you share freely, never taking more than you give.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of SVPulsingAnnotationView.
|
||||||
|
|
||||||
|
Copyright (c) 2013, Sam Vermette <hello@samvermette.com>
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any purpose
|
||||||
|
with or without fee is hereby granted, provided that the above copyright notice
|
||||||
|
and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||||
|
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
||||||
|
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||||
|
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||||
|
THIS SOFTWARE.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of zlib.
|
||||||
|
|
||||||
|
Acknowledgments:
|
||||||
|
|
||||||
|
The deflate format used by zlib was defined by Phil Katz. The deflate and
|
||||||
|
zlib specifications were written by L. Peter Deutsch. Thanks to all the
|
||||||
|
people who reported problems and suggested various improvements in zlib; they
|
||||||
|
are too numerous to cite here.
|
||||||
|
|
||||||
|
Copyright notice:
|
||||||
|
|
||||||
|
(C) 1995-2013 Jean-loup Gailly and Mark Adler
|
||||||
|
|
||||||
|
This software is provided 'as-is', without any express or implied
|
||||||
|
warranty. In no event will the authors be held liable for any damages
|
||||||
|
arising from the use of this software.
|
||||||
|
|
||||||
|
Permission is granted to anyone to use this software for any purpose,
|
||||||
|
including commercial applications, and to alter it and redistribute it
|
||||||
|
freely, subject to the following restrictions:
|
||||||
|
|
||||||
|
1. The origin of this software must not be misrepresented; you must not
|
||||||
|
claim that you wrote the original software. If you use this software
|
||||||
|
in a product, an acknowledgment in the product documentation would be
|
||||||
|
appreciated but is not required.
|
||||||
|
2. Altered source versions must be plainly marked as such, and must not be
|
||||||
|
misrepresented as being the original software.
|
||||||
|
3. This notice may not be removed or altered from any source distribution.
|
||||||
|
|
||||||
|
Jean-loup Gailly Mark Adler
|
||||||
|
jloup@gzip.org madler@alumni.caltech.edu
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of Realm Objective-C.
|
||||||
|
|
||||||
|
Copyright 2015 Realm Inc.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
|
||||||
35
ISSUE_TEMPLATE.md
Normal file
35
ISSUE_TEMPLATE.md
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
_Before creating an issue, please search the existing issues to see if a similar one has already been created. You can search issues by specific labels (e.g. `label:nearby `) or just by typing keywords into the search filter._
|
||||||
|
|
||||||
|
**Summary:**
|
||||||
|
|
||||||
|
Summarize your issue in one sentence (what goes wrong, what did you expect to happen)
|
||||||
|
|
||||||
|
**Steps to reproduce:**
|
||||||
|
|
||||||
|
How can we reproduce the issue?
|
||||||
|
|
||||||
|
**Add System logs:**
|
||||||
|
|
||||||
|
Add logcat files here (if possible).
|
||||||
|
|
||||||
|
**Expected behavior:**
|
||||||
|
|
||||||
|
What did you expect the App to do?
|
||||||
|
|
||||||
|
**Observed behavior:**
|
||||||
|
|
||||||
|
What did you see instead? Describe your issue in detail here.
|
||||||
|
|
||||||
|
**Device and Android version:**
|
||||||
|
|
||||||
|
What make and model device (e.g., Samsung J7) did you encounter this on? What Android
|
||||||
|
version (e.g., Android 4.0 Ice Cream Sandwich or Android 6.0 Marshmallow) are you running? Is it
|
||||||
|
the stock version from the manufacturer or a custom ROM ?
|
||||||
|
|
||||||
|
**Commons app version:**
|
||||||
|
|
||||||
|
You can find this information by going to the navigation drawer in the app and tapping 'About'
|
||||||
|
|
||||||
|
**Screen-shots:**
|
||||||
|
|
||||||
|
Can be created by pressing the Volume Down and Power Button at the same time on Android 4.0 and higher.
|
||||||
|
|
@ -2,9 +2,7 @@
|
||||||
|
|
||||||
The Wikimedia Commons Android app allows users to upload pictures from their Android phone/tablet to Wikimedia Commons. Download the app [here][1], or view our [website][2].
|
The Wikimedia Commons Android app allows users to upload pictures from their Android phone/tablet to Wikimedia Commons. Download the app [here][1], or view our [website][2].
|
||||||
|
|
||||||
Initially started by the Wikimedia Foundation, this app is now maintained by volunteers. Anyone is welcome to improve it, just choose among the [open issues][3] and send us a pull request :-)
|
Initially started by the Wikimedia Foundation, this app is now maintained by grantees and volunteers of the Wikimedia community. Anyone is welcome to improve it, just choose among the [open issues][3] and send us a pull request :-)
|
||||||
|
|
||||||
We are currently applying for an [IEG renewal][10] to work on the app for the next 6 months. Feedback is very much welcomed.
|
|
||||||
|
|
||||||
<a href="https://f-droid.org/repository/browse/?fdid=fr.free.nrw.commons" target="_blank">
|
<a href="https://f-droid.org/repository/browse/?fdid=fr.free.nrw.commons" target="_blank">
|
||||||
<img src="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="90"/></a>
|
<img src="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="90"/></a>
|
||||||
|
|
|
||||||
117
app/build.gradle
117
app/build.gradle
|
|
@ -1,58 +1,78 @@
|
||||||
|
apply from: '../gitutils.gradle'
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'me.tatarka.retrolambda'
|
apply plugin: 'kotlin-android'
|
||||||
|
apply plugin: 'kotlin-kapt'
|
||||||
apply plugin: 'jacoco-android'
|
apply plugin: 'jacoco-android'
|
||||||
apply from: 'quality.gradle'
|
apply from: 'quality.gradle'
|
||||||
apply plugin: 'com.getkeepsafe.dexcount'
|
apply plugin: 'com.getkeepsafe.dexcount'
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile 'com.github.nicolas-raoul:Quadtree:ac16ea8035bf07'
|
implementation 'com.github.nicolas-raoul:Quadtree:ac16ea8035bf07'
|
||||||
compile 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar'
|
implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar'
|
||||||
compile 'in.yuvi:http.fluent:1.3'
|
implementation 'in.yuvi:http.fluent:1.3'
|
||||||
compile 'com.android.volley:volley:1.0.0'
|
implementation 'com.android.volley:volley:1.0.0'
|
||||||
compile 'ch.acra:acra:4.7.0'
|
implementation 'ch.acra:acra:4.7.0'
|
||||||
compile 'org.mediawiki:api:1.3'
|
implementation 'org.mediawiki:api:1.3'
|
||||||
compile 'commons-codec:commons-codec:1.10'
|
implementation 'commons-codec:commons-codec:1.10'
|
||||||
compile 'com.github.pedrovgs:renderers:3.3.3'
|
implementation 'com.github.pedrovgs:renderers:3.3.3'
|
||||||
compile 'com.google.code.gson:gson:2.8.0'
|
implementation 'com.google.code.gson:gson:2.8.1'
|
||||||
compile 'com.jakewharton.timber:timber:4.5.1'
|
implementation 'com.jakewharton.timber:timber:4.5.1'
|
||||||
compile 'info.debatty:java-string-similarity:0.24'
|
implementation 'info.debatty:java-string-similarity:0.24'
|
||||||
compile ('com.mapbox.mapboxsdk:mapbox-android-sdk:5.1.0@aar'){
|
implementation ('com.mapbox.mapboxsdk:mapbox-android-sdk:5.2.1@aar'){
|
||||||
transitive=true
|
transitive=true
|
||||||
}
|
}
|
||||||
|
|
||||||
compile "com.android.support:support-v4:${project.supportLibVersion}"
|
|
||||||
compile "com.android.support:appcompat-v7:${project.supportLibVersion}"
|
|
||||||
compile "com.android.support:design:${project.supportLibVersion}"
|
|
||||||
|
|
||||||
compile "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION"
|
implementation "com.android.support:support-v4:$SUPPORT_LIB_VERSION"
|
||||||
annotationProcessor "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
|
implementation "com.android.support:appcompat-v7:$SUPPORT_LIB_VERSION"
|
||||||
|
implementation "com.android.support:design:$SUPPORT_LIB_VERSION"
|
||||||
|
|
||||||
compile 'com.squareup.okhttp3:okhttp:3.8.1'
|
implementation "com.android.support:cardview-v7:$SUPPORT_LIB_VERSION"
|
||||||
compile 'com.squareup.okio:okio:1.13.0'
|
|
||||||
|
|
||||||
compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
|
implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION"
|
||||||
|
kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
|
||||||
|
|
||||||
|
implementation 'com.squareup.okhttp3:okhttp:3.8.1'
|
||||||
|
implementation 'com.squareup.okio:okio:1.13.0'
|
||||||
|
|
||||||
|
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
|
||||||
// Because RxAndroid releases are few and far between, it is recommended you also
|
// Because RxAndroid releases are few and far between, it is recommended you also
|
||||||
// explicitly depend on RxJava's latest version for bug fixes and new features.
|
// explicitly depend on RxJava's latest version for bug fixes and new features.
|
||||||
compile 'io.reactivex.rxjava2:rxjava:2.1.2'
|
implementation 'io.reactivex.rxjava2:rxjava:2.1.2'
|
||||||
compile 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
|
implementation 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
|
||||||
compile 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.0.0'
|
implementation 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.0.0'
|
||||||
compile 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.0.0'
|
implementation 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.0.0'
|
||||||
compile 'com.jakewharton.rxbinding2:rxbinding-design:2.0.0'
|
implementation 'com.jakewharton.rxbinding2:rxbinding-design:2.0.0'
|
||||||
|
|
||||||
compile 'com.facebook.fresco:fresco:1.3.0'
|
implementation 'com.facebook.fresco:fresco:1.5.0'
|
||||||
compile 'com.facebook.stetho:stetho:1.5.0'
|
implementation 'com.facebook.stetho:stetho:1.5.0'
|
||||||
|
|
||||||
testCompile 'junit:junit:4.12'
|
implementation "com.google.dagger:dagger:$DAGGER_VERSION"
|
||||||
testCompile 'org.robolectric:robolectric:3.3.2'
|
implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
|
||||||
|
|
||||||
testCompile 'com.squareup.okhttp3:mockwebserver:3.8.1'
|
kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
|
||||||
androidTestCompile 'com.squareup.okhttp3:mockwebserver:3.8.1'
|
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
|
||||||
androidTestCompile "com.android.support:support-annotations:${project.supportLibVersion}"
|
|
||||||
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'
|
|
||||||
|
|
||||||
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1'
|
testImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
|
||||||
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
|
androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
|
||||||
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
|
|
||||||
|
testImplementation 'junit:junit:4.12'
|
||||||
|
testImplementation 'org.robolectric:robolectric:3.4'
|
||||||
|
testImplementation 'org.mockito:mockito-all:1.10.19'
|
||||||
|
|
||||||
|
testImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1'
|
||||||
|
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1'
|
||||||
|
androidTestImplementation "com.android.support:support-annotations:$SUPPORT_LIB_VERSION"
|
||||||
|
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
|
||||||
|
|
||||||
|
debugImplementation "com.squareup.leakcanary:leakcanary-android:$LEAK_CANARY"
|
||||||
|
releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY"
|
||||||
|
testImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY"
|
||||||
|
|
||||||
|
implementation "com.google.dagger:dagger:$DAGGER_VERSION"
|
||||||
|
implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
|
||||||
|
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
|
||||||
|
kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
@ -63,24 +83,38 @@ android {
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId 'fr.free.nrw.commons'
|
applicationId 'fr.free.nrw.commons'
|
||||||
versionCode 74
|
versionCode 82
|
||||||
versionName '2.5.0'
|
versionName '2.6.7'
|
||||||
|
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
|
||||||
|
|
||||||
minSdkVersion project.minSdkVersion
|
minSdkVersion project.minSdkVersion
|
||||||
targetSdkVersion project.targetSdkVersion
|
targetSdkVersion project.targetSdkVersion
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
// use kotlin only in tests (for now)
|
||||||
|
test.java.srcDirs += 'src/test/kotlin'
|
||||||
|
|
||||||
|
// use main assets and resources in test
|
||||||
|
test.assets.srcDirs += 'src/main/assets'
|
||||||
|
test.resources.srcDirs += 'src/main/resoures'
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled false // See https://stackoverflow.com/questions/40232404/google-play-apk-and-android-studio-apk-usb-debug-behaving-differently - proguard.cfg modification alone insufficient.
|
minifyEnabled false // See https://stackoverflow.com/questions/40232404/google-play-apk-and-android-studio-apk-usb-debug-behaving-differently - proguard.cfg modification alone insufficient.
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
|
applicationIdSuffix ".debug"
|
||||||
testCoverageEnabled true
|
testCoverageEnabled true
|
||||||
|
versionNameSuffix "-debug-" + getBranchName() + "~" + getBuildVersion()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flavorDimensions 'tier'
|
||||||
productFlavors {
|
productFlavors {
|
||||||
prod {
|
prod {
|
||||||
buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.org/w/api.php\""
|
buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.org/w/api.php\""
|
||||||
|
|
@ -92,6 +126,7 @@ android {
|
||||||
buildConfigField "String", "EVENTLOG_WIKI", "\"commonswiki\""
|
buildConfigField "String", "EVENTLOG_WIKI", "\"commonswiki\""
|
||||||
buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\""
|
buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\""
|
||||||
buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Main_Page&welcome=yes\""
|
buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Main_Page&welcome=yes\""
|
||||||
|
dimension 'tier'
|
||||||
}
|
}
|
||||||
|
|
||||||
beta {
|
beta {
|
||||||
|
|
@ -105,6 +140,7 @@ android {
|
||||||
buildConfigField "String", "EVENTLOG_WIKI", "\"commonswiki\""
|
buildConfigField "String", "EVENTLOG_WIKI", "\"commonswiki\""
|
||||||
buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\""
|
buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\""
|
||||||
buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Main_Page&welcome=yes\""
|
buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Main_Page&welcome=yes\""
|
||||||
|
dimension 'tier'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -122,5 +158,8 @@ android {
|
||||||
//FIXME: Temporary fix for https://github.com/commons-app/apps-android-commons/issues/709
|
//FIXME: Temporary fix for https://github.com/commons-app/apps-android-commons/issues/709
|
||||||
configurations.all {
|
configurations.all {
|
||||||
resolutionStrategy.force 'com.android.support:support-annotations:25.2.0'
|
resolutionStrategy.force 'com.android.support:support-annotations:25.2.0'
|
||||||
|
exclude module: 'httpclient'
|
||||||
|
exclude module: 'commons-logging'
|
||||||
}
|
}
|
||||||
|
buildToolsVersion buildToolsVersion
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
package fr.free.nrw.commons;
|
|
||||||
|
|
||||||
import android.support.test.espresso.assertion.ViewAssertions;
|
|
||||||
import android.support.test.filters.LargeTest;
|
|
||||||
import android.support.test.rule.ActivityTestRule;
|
|
||||||
import android.support.test.runner.AndroidJUnit4;
|
|
||||||
|
|
||||||
import org.junit.Rule;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.nearby.NearbyActivity;
|
|
||||||
|
|
||||||
import static android.support.test.espresso.Espresso.onView;
|
|
||||||
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
|
|
||||||
import static android.support.test.espresso.matcher.ViewMatchers.withText;
|
|
||||||
|
|
||||||
@LargeTest
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
|
||||||
public class NearbyActivityTest {
|
|
||||||
@Rule
|
|
||||||
public final ActivityTestRule<NearbyActivity> nearby =
|
|
||||||
new ActivityTestRule<>(NearbyActivity.class);
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testActivityLaunch() {
|
|
||||||
onView(withText(R.string.title_activity_nearby))
|
|
||||||
.check(ViewAssertions.matches(isDisplayed()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package fr.free.nrw.commons.upload;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.support.test.InstrumentationRegistry;
|
||||||
|
import android.support.test.runner.AndroidJUnit4;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.BuildConfig;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.is;
|
||||||
|
import static org.junit.Assert.assertThat;
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class FileUtilsTest {
|
||||||
|
@Test
|
||||||
|
public void isSelfOwned() throws Exception {
|
||||||
|
Uri uri = Uri.parse("content://" + BuildConfig.APPLICATION_ID + ".provider/document/1");
|
||||||
|
boolean selfOwned = FileUtils.isSelfOwned(InstrumentationRegistry.getTargetContext(), uri);
|
||||||
|
assertThat(selfOwned, is(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void isNotSelfOwned() throws Exception {
|
||||||
|
Uri uri = Uri.parse("content://com.android.providers.media.documents/document/1");
|
||||||
|
boolean selfOwned = FileUtils.isSelfOwned(InstrumentationRegistry.getTargetContext(), uri);
|
||||||
|
assertThat(selfOwned, is(false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,24 +2,28 @@
|
||||||
package="fr.free.nrw.commons">
|
package="fr.free.nrw.commons">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
|
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
||||||
<uses-permission android:name="android.permission.READ_SYNC_STATS"/>
|
<uses-permission android:name="android.permission.READ_SYNC_STATS" />
|
||||||
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/>
|
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
|
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
|
||||||
<uses-permission android:name="android.permission.GET_ACCOUNTS"/>
|
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||||
<uses-permission android:name="android.permission.USE_CREDENTIALS"/>
|
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
|
||||||
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS"/>
|
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
|
||||||
<uses-permission android:name="android.permission.MANAGE_DOCUMENTS"/>
|
<uses-permission android:name="android.permission.MANAGE_DOCUMENTS" />
|
||||||
<uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS"/>
|
<uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" />
|
||||||
|
<uses-permission android:name="android.permission.READ_LOGS"/>
|
||||||
|
|
||||||
|
<!-- Needed only if your app targets Android 5.0 (API level 21) or higher. -->
|
||||||
|
<uses-feature android:name="android.hardware.location.gps" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".CommonsApplication"
|
android:name=".CommonsApplication"
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/Theme.AppCompat"
|
android:theme="@style/LightAppTheme"
|
||||||
android:supportsRtl="true" >
|
android:supportsRtl="true" >
|
||||||
<activity android:name="org.acra.CrashReportDialog"
|
<activity android:name="org.acra.CrashReportDialog"
|
||||||
android:theme="@android:style/Theme.Dialog"
|
android:theme="@android:style/Theme.Dialog"
|
||||||
|
|
@ -27,23 +31,19 @@
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:finishOnTaskLaunch="true" />
|
android:finishOnTaskLaunch="true" />
|
||||||
|
|
||||||
<activity
|
<activity android:name=".auth.LoginActivity">
|
||||||
android:name=".auth.LoginActivity"
|
|
||||||
>
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
|
||||||
android:name=".WelcomeActivity"
|
<activity android:name=".WelcomeActivity" />
|
||||||
>
|
|
||||||
</activity>
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".upload.ShareActivity"
|
android:name=".upload.ShareActivity"
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name">
|
||||||
>
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
@ -51,11 +51,11 @@
|
||||||
<data android:mimeType="audio/ogg" />
|
<data android:mimeType="audio/ogg" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".upload.MultipleShareActivity"
|
android:name=".upload.MultipleShareActivity"
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name">
|
||||||
>
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
@ -67,31 +67,36 @@
|
||||||
<activity
|
<activity
|
||||||
android:name=".contributions.ContributionsActivity"
|
android:name=".contributions.ContributionsActivity"
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name" />
|
||||||
>
|
|
||||||
</activity>
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".settings.SettingsActivity"
|
android:name=".settings.SettingsActivity"
|
||||||
android:label="@string/title_activity_settings"
|
android:label="@string/title_activity_settings" />
|
||||||
/>
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".AboutActivity"
|
android:name=".AboutActivity"
|
||||||
android:label="@string/title_activity_about"
|
android:label="@string/title_activity_about"
|
||||||
android:parentActivityName=".contributions.ContributionsActivity" />
|
android:parentActivityName=".contributions.ContributionsActivity" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".auth.SignupActivity"
|
android:name=".auth.SignupActivity"
|
||||||
android:label="@string/title_activity_signup"/>
|
android:label="@string/title_activity_signup" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".nearby.NearbyActivity"
|
android:name=".nearby.NearbyActivity"
|
||||||
android:label="@string/title_activity_nearby"
|
android:label="@string/title_activity_nearby"
|
||||||
android:parentActivityName=".contributions.ContributionsActivity" />
|
android:parentActivityName=".contributions.ContributionsActivity" />
|
||||||
|
|
||||||
<service android:name=".upload.UploadService" >
|
<activity
|
||||||
</service>
|
android:name=".notification.NotificationActivity"
|
||||||
|
android:label="@string/navigation_item_notification" />
|
||||||
|
|
||||||
|
<service android:name=".upload.UploadService" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".auth.WikiAccountAuthenticatorService"
|
android:name=".auth.WikiAccountAuthenticatorService"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:process=":auth" >
|
android:process=":auth">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.accounts.AccountAuthenticator" />
|
<action android:name="android.accounts.AccountAuthenticator" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
@ -105,8 +110,7 @@
|
||||||
android:name=".contributions.ContributionsSyncService"
|
android:name=".contributions.ContributionsSyncService"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action
|
<action android:name="android.content.SyncAdapter" />
|
||||||
android:name="android.content.SyncAdapter" />
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.content.SyncAdapter"
|
android:name="android.content.SyncAdapter"
|
||||||
|
|
@ -117,8 +121,7 @@
|
||||||
android:name=".modifications.ModificationsSyncService"
|
android:name=".modifications.ModificationsSyncService"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action
|
<action android:name="android.content.SyncAdapter" />
|
||||||
android:name="android.content.SyncAdapter" />
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.content.SyncAdapter"
|
android:name="android.content.SyncAdapter"
|
||||||
|
|
@ -132,31 +135,29 @@
|
||||||
android:grantUriPermissions="true">
|
android:grantUriPermissions="true">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
android:resource="@xml/provider_paths"/>
|
android:resource="@xml/provider_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name=".contributions.ContributionsContentProvider"
|
android:name=".contributions.ContributionsContentProvider"
|
||||||
android:label="@string/provider_contributions"
|
|
||||||
android:syncable="true"
|
|
||||||
android:authorities="fr.free.nrw.commons.contributions.contentprovider"
|
android:authorities="fr.free.nrw.commons.contributions.contentprovider"
|
||||||
android:exported="false">
|
android:exported="false"
|
||||||
</provider>
|
android:label="@string/provider_contributions"
|
||||||
|
android:syncable="true" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name=".modifications.ModificationsContentProvider"
|
android:name=".modifications.ModificationsContentProvider"
|
||||||
android:label="@string/provider_modifications"
|
|
||||||
android:syncable="true"
|
|
||||||
android:authorities="fr.free.nrw.commons.modifications.contentprovider"
|
android:authorities="fr.free.nrw.commons.modifications.contentprovider"
|
||||||
android:exported="false">
|
android:exported="false"
|
||||||
</provider>
|
android:label="@string/provider_modifications"
|
||||||
|
android:syncable="true" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name=".category.CategoryContentProvider"
|
android:name=".category.CategoryContentProvider"
|
||||||
android:label="@string/provider_categories"
|
|
||||||
android:syncable="false"
|
|
||||||
android:authorities="fr.free.nrw.commons.categories.contentprovider"
|
android:authorities="fr.free.nrw.commons.categories.contentprovider"
|
||||||
android:exported="false">
|
android:exported="false"
|
||||||
</provider>
|
android:label="@string/provider_categories"
|
||||||
|
android:syncable="false" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
|
|
||||||
26
app/src/main/assets/mapstyle.json
Normal file
26
app/src/main/assets/mapstyle.json
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"version": 8,
|
||||||
|
"sources": {
|
||||||
|
"wikimedia-osm": {
|
||||||
|
"type": "raster",
|
||||||
|
"tiles": [
|
||||||
|
"https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png"
|
||||||
|
],
|
||||||
|
"tileSize": 128
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"id": "background",
|
||||||
|
"type": "background",
|
||||||
|
"paint": {
|
||||||
|
"background-color": "#606060"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "osm",
|
||||||
|
"type": "raster",
|
||||||
|
"source": "wikimedia-osm"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -1,19 +1,29 @@
|
||||||
package fr.free.nrw.commons;
|
package fr.free.nrw.commons;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import butterknife.BindView;
|
import butterknife.BindView;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
|
import butterknife.OnClick;
|
||||||
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
||||||
import fr.free.nrw.commons.ui.widget.HtmlTextView;
|
import fr.free.nrw.commons.ui.widget.HtmlTextView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents about screen of this app
|
||||||
|
*/
|
||||||
public class AboutActivity extends NavigationBaseActivity {
|
public class AboutActivity extends NavigationBaseActivity {
|
||||||
@BindView(R.id.about_version) TextView versionText;
|
@BindView(R.id.about_version) TextView versionText;
|
||||||
@BindView(R.id.about_license) HtmlTextView aboutLicenseText;
|
@BindView(R.id.about_license) HtmlTextView aboutLicenseText;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method helps in the creation About screen
|
||||||
|
*
|
||||||
|
* @param savedInstanceState Data bundle
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
@ -21,15 +31,38 @@ public class AboutActivity extends NavigationBaseActivity {
|
||||||
|
|
||||||
ButterKnife.bind(this);
|
ButterKnife.bind(this);
|
||||||
|
|
||||||
String aboutText = getString(R.string.about_license, getString(R.string.trademarked_name));
|
String aboutText = getString(R.string.about_license);
|
||||||
aboutLicenseText.setHtmlText(aboutText);
|
aboutLicenseText.setHtmlText(aboutText);
|
||||||
|
|
||||||
versionText.setText(BuildConfig.VERSION_NAME);
|
versionText.setText(BuildConfig.VERSION_NAME);
|
||||||
initDrawer();
|
initDrawer();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void startYourself(Context context) {
|
@OnClick(R.id.facebook_launch_icon)
|
||||||
Intent settingsIntent = new Intent(context, AboutActivity.class);
|
public void launchFacebook(View view) {
|
||||||
context.startActivity(settingsIntent);
|
|
||||||
|
Intent intent;
|
||||||
|
try {
|
||||||
|
intent = new Intent(Intent.ACTION_VIEW, Uri.parse("fb://page/" + "1921335171459985"));
|
||||||
|
intent.setPackage("com.facebook.katana");
|
||||||
|
startActivity(intent);
|
||||||
|
} catch (Exception e) {
|
||||||
|
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://www.facebook.com/" + "1921335171459985")));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnClick(R.id.github_launch_icon)
|
||||||
|
public void launchGithub(View view) {
|
||||||
|
|
||||||
|
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/commons-app/apps-android-commons\\"));
|
||||||
|
startActivity(browserIntent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnClick(R.id.website_launch_icon)
|
||||||
|
public void launchWebsite(View view) {
|
||||||
|
|
||||||
|
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://commons-app.github.io/\\"));
|
||||||
|
startActivity(browserIntent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,40 +1,34 @@
|
||||||
package fr.free.nrw.commons;
|
package fr.free.nrw.commons;
|
||||||
|
|
||||||
import android.accounts.Account;
|
|
||||||
import android.accounts.AccountManager;
|
|
||||||
import android.accounts.AccountManagerCallback;
|
|
||||||
import android.accounts.AccountManagerFuture;
|
|
||||||
import android.accounts.AuthenticatorException;
|
|
||||||
import android.accounts.OperationCanceledException;
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.support.v4.util.LruCache;
|
|
||||||
|
|
||||||
import com.facebook.drawee.backends.pipeline.Fresco;
|
import com.facebook.drawee.backends.pipeline.Fresco;
|
||||||
import com.facebook.stetho.Stetho;
|
import com.facebook.stetho.Stetho;
|
||||||
import com.squareup.leakcanary.LeakCanary;
|
import com.squareup.leakcanary.LeakCanary;
|
||||||
|
import com.squareup.leakcanary.RefWatcher;
|
||||||
|
|
||||||
import org.acra.ACRA;
|
import org.acra.ACRA;
|
||||||
import org.acra.ReportingInteractionMode;
|
import org.acra.ReportingInteractionMode;
|
||||||
import org.acra.annotation.ReportsCrashes;
|
import org.acra.annotation.ReportsCrashes;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.auth.AccountUtil;
|
import javax.inject.Inject;
|
||||||
import fr.free.nrw.commons.caching.CacheController;
|
import javax.inject.Named;
|
||||||
import fr.free.nrw.commons.contributions.Contribution;
|
|
||||||
import fr.free.nrw.commons.data.Category;
|
import fr.free.nrw.commons.auth.SessionManager;
|
||||||
|
import fr.free.nrw.commons.category.CategoryDao;
|
||||||
|
import fr.free.nrw.commons.contributions.ContributionDao;
|
||||||
import fr.free.nrw.commons.data.DBOpenHelper;
|
import fr.free.nrw.commons.data.DBOpenHelper;
|
||||||
import fr.free.nrw.commons.modifications.ModifierSequence;
|
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||||
import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi;
|
import fr.free.nrw.commons.di.CommonsApplicationComponent;
|
||||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
|
||||||
import fr.free.nrw.commons.nearby.NearbyPlaces;
|
|
||||||
import fr.free.nrw.commons.utils.FileUtils;
|
import fr.free.nrw.commons.utils.FileUtils;
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.schedulers.Schedulers;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
// TODO: Use ProGuard to rip out reporting when publishing
|
// TODO: Use ProGuard to rip out reporting when publishing
|
||||||
|
|
@ -48,86 +42,43 @@ import timber.log.Timber;
|
||||||
)
|
)
|
||||||
public class CommonsApplication extends Application {
|
public class CommonsApplication extends Application {
|
||||||
|
|
||||||
private Account currentAccount = null; // Unlike a savings account...
|
@Inject SessionManager sessionManager;
|
||||||
|
@Inject DBOpenHelper dbOpenHelper;
|
||||||
|
|
||||||
public static final Object[] EVENT_UPLOAD_ATTEMPT = {"MobileAppUploadAttempts", 5334329L};
|
@Inject @Named("default_preferences") SharedPreferences defaultPrefs;
|
||||||
public static final Object[] EVENT_LOGIN_ATTEMPT = {"MobileAppLoginAttempts", 5257721L};
|
@Inject @Named("application_preferences") SharedPreferences applicationPrefs;
|
||||||
public static final Object[] EVENT_SHARE_ATTEMPT = {"MobileAppShareAttempts", 5346170L};
|
@Inject @Named("prefs") SharedPreferences otherPrefs;
|
||||||
public static final Object[] EVENT_CATEGORIZATION_ATTEMPT = {"MobileAppCategorizationAttempts", 5359208L};
|
|
||||||
|
|
||||||
public static final String DEFAULT_EDIT_SUMMARY = "Uploaded using Android Commons app";
|
public static final String DEFAULT_EDIT_SUMMARY = "Uploaded using Android Commons app";
|
||||||
|
|
||||||
public static final String FEEDBACK_EMAIL = "commons-app-android@googlegroups.com";
|
public static final String FEEDBACK_EMAIL = "commons-app-android@googlegroups.com";
|
||||||
|
|
||||||
|
public static final String LOGS_PRIVATE_EMAIL = "commons-app-android-private@googlegroups.com";
|
||||||
|
|
||||||
public static final String FEEDBACK_EMAIL_SUBJECT = "Commons Android App (%s) Feedback";
|
public static final String FEEDBACK_EMAIL_SUBJECT = "Commons Android App (%s) Feedback";
|
||||||
|
|
||||||
private static CommonsApplication instance = null;
|
private RefWatcher refWatcher;
|
||||||
private MediaWikiApi api = null;
|
|
||||||
private LruCache<String, String> thumbnailUrlCache = new LruCache<>(1024);
|
|
||||||
private CacheController cacheData = null;
|
|
||||||
private DBOpenHelper dbOpenHelper = null;
|
|
||||||
private NearbyPlaces nearbyPlaces = null;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This should not be called by ANY application code (other than the magic Android glue)
|
* Used to declare and initialize various components and dependencies
|
||||||
* Use CommonsApplication.getInstance() instead to get the singleton.
|
|
||||||
*/
|
*/
|
||||||
public CommonsApplication() {
|
|
||||||
CommonsApplication.instance = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static CommonsApplication getInstance() {
|
|
||||||
if (instance == null) {
|
|
||||||
instance = new CommonsApplication();
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MediaWikiApi getMWApi() {
|
|
||||||
if (api == null) {
|
|
||||||
api = new ApacheHttpClientMediaWikiApi(BuildConfig.WIKIMEDIA_API_HOST);
|
|
||||||
}
|
|
||||||
return api;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CacheController getCacheData() {
|
|
||||||
if (cacheData == null) {
|
|
||||||
cacheData = new CacheController();
|
|
||||||
}
|
|
||||||
return cacheData;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LruCache<String, String> getThumbnailUrlCache() {
|
|
||||||
return thumbnailUrlCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized DBOpenHelper getDBOpenHelper() {
|
|
||||||
if (dbOpenHelper == null) {
|
|
||||||
dbOpenHelper = new DBOpenHelper(this);
|
|
||||||
}
|
|
||||||
return dbOpenHelper;
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized NearbyPlaces getNearbyPlaces() {
|
|
||||||
if (nearbyPlaces == null) {
|
|
||||||
nearbyPlaces = new NearbyPlaces();
|
|
||||||
}
|
|
||||||
return nearbyPlaces;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
if (LeakCanary.isInAnalyzerProcess(this)) {
|
|
||||||
// This process is dedicated to LeakCanary for heap analysis.
|
ApplicationlessInjection
|
||||||
// You should not init your app in this process.
|
.getInstance(this)
|
||||||
|
.getCommonsApplicationComponent()
|
||||||
|
.inject(this);
|
||||||
|
|
||||||
|
Fresco.initialize(this);
|
||||||
|
if (setupLeakCanary() == RefWatcher.DISABLED) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
LeakCanary.install(this);
|
|
||||||
|
|
||||||
Timber.plant(new Timber.DebugTree());
|
Timber.plant(new Timber.DebugTree());
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (!BuildConfig.DEBUG) {
|
if (!BuildConfig.DEBUG) {
|
||||||
ACRA.init(this);
|
ACRA.init(this);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -136,52 +87,36 @@ public class CommonsApplication extends Application {
|
||||||
|
|
||||||
// Fire progress callbacks for every 3% of uploaded content
|
// Fire progress callbacks for every 3% of uploaded content
|
||||||
System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0");
|
System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0");
|
||||||
|
}
|
||||||
|
|
||||||
Fresco.initialize(this);
|
|
||||||
|
|
||||||
//For caching area -> categories
|
/**
|
||||||
cacheData = new CacheController();
|
* Helps in setting up LeakCanary library
|
||||||
|
* @return instance of LeakCanary
|
||||||
|
*/
|
||||||
|
protected RefWatcher setupLeakCanary() {
|
||||||
|
if (LeakCanary.isInAnalyzerProcess(this)) {
|
||||||
|
return RefWatcher.DISABLED;
|
||||||
|
}
|
||||||
|
return LeakCanary.install(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Account|null
|
* Provides a way to get member refWatcher
|
||||||
|
*
|
||||||
|
* @param context Application context
|
||||||
|
* @return application member refWatcher
|
||||||
*/
|
*/
|
||||||
public Account getCurrentAccount() {
|
public static RefWatcher getRefWatcher(Context context) {
|
||||||
if (currentAccount == null) {
|
CommonsApplication application = (CommonsApplication) context.getApplicationContext();
|
||||||
AccountManager accountManager = AccountManager.get(this);
|
return application.refWatcher;
|
||||||
Account[] allAccounts = accountManager.getAccountsByType(AccountUtil.accountType());
|
|
||||||
if (allAccounts.length != 0) {
|
|
||||||
currentAccount = allAccounts[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return currentAccount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean revalidateAuthToken() {
|
|
||||||
AccountManager accountManager = AccountManager.get(this);
|
|
||||||
Account curAccount = getCurrentAccount();
|
|
||||||
|
|
||||||
if (curAccount == null) {
|
|
||||||
return false; // This should never happen
|
|
||||||
}
|
|
||||||
|
|
||||||
accountManager.invalidateAuthToken(AccountUtil.accountType(), getMWApi().getAuthCookie());
|
|
||||||
try {
|
|
||||||
String authCookie = accountManager.blockingGetAuthToken(curAccount, "", false);
|
|
||||||
getMWApi().setAuthCookie(authCookie);
|
|
||||||
return true;
|
|
||||||
} catch (OperationCanceledException | NullPointerException | IOException | AuthenticatorException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean deviceHasCamera() {
|
|
||||||
PackageManager pm = getPackageManager();
|
|
||||||
return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA) ||
|
|
||||||
pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* clears data of current application
|
||||||
|
* @param context Application context
|
||||||
|
* @param logoutListener Implementation of interface LogoutListener
|
||||||
|
*/
|
||||||
public void clearApplicationData(Context context, LogoutListener logoutListener) {
|
public void clearApplicationData(Context context, LogoutListener logoutListener) {
|
||||||
File cacheDirectory = context.getCacheDir();
|
File cacheDirectory = context.getCacheDir();
|
||||||
File applicationDirectory = new File(cacheDirectory.getParent());
|
File applicationDirectory = new File(cacheDirectory.getParent());
|
||||||
|
|
@ -194,70 +129,37 @@ public class CommonsApplication extends Application {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AccountManager accountManager = AccountManager.get(this);
|
sessionManager.clearAllAccounts()
|
||||||
Account[] allAccounts = accountManager.getAccountsByType(AccountUtil.accountType());
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
AccountManagerCallback<Boolean> amCallback = new AccountManagerCallback<Boolean>() {
|
.subscribe(() -> {
|
||||||
|
|
||||||
private int index = 0;
|
|
||||||
|
|
||||||
void setIndex(int index) {
|
|
||||||
this.index = index;
|
|
||||||
}
|
|
||||||
|
|
||||||
int getIndex() {
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run(AccountManagerFuture<Boolean> accountManagerFuture) {
|
|
||||||
setIndex(getIndex() + 1);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (accountManagerFuture != null && accountManagerFuture.getResult()) {
|
|
||||||
Timber.d("Account removed successfully.");
|
|
||||||
}
|
|
||||||
} catch (OperationCanceledException | IOException | AuthenticatorException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getIndex() == allAccounts.length) {
|
|
||||||
Timber.d("All accounts have been removed");
|
Timber.d("All accounts have been removed");
|
||||||
//TODO: fix preference manager
|
//TODO: fix preference manager
|
||||||
PreferenceManager.getDefaultSharedPreferences(getInstance())
|
defaultPrefs.edit().clear().apply();
|
||||||
.edit().clear().commit();
|
applicationPrefs.edit().clear().apply();
|
||||||
SharedPreferences preferences = context
|
applicationPrefs.edit().putBoolean("firstrun", false).apply();
|
||||||
.getSharedPreferences("fr.free.nrw.commons", MODE_PRIVATE);
|
otherPrefs.edit().clear().apply();
|
||||||
preferences.edit().clear().commit();
|
|
||||||
context.getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
|
||||||
.edit().clear().commit();
|
|
||||||
preferences.edit().putBoolean("firstrun", false).apply();
|
|
||||||
updateAllDatabases();
|
updateAllDatabases();
|
||||||
currentAccount = null;
|
|
||||||
|
|
||||||
logoutListener.onLogoutComplete();
|
logoutListener.onLogoutComplete();
|
||||||
}
|
});
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for (Account account : allAccounts) {
|
|
||||||
accountManager.removeAccount(account, amCallback, null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes all tables and re-creates them.
|
* Deletes all tables and re-creates them.
|
||||||
*/
|
*/
|
||||||
public void updateAllDatabases() {
|
private void updateAllDatabases() {
|
||||||
DBOpenHelper dbOpenHelper = CommonsApplication.getInstance().getDBOpenHelper();
|
|
||||||
dbOpenHelper.getReadableDatabase().close();
|
dbOpenHelper.getReadableDatabase().close();
|
||||||
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
|
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
|
||||||
|
|
||||||
ModifierSequence.Table.onDelete(db);
|
ModifierSequenceDao.Table.onDelete(db);
|
||||||
Category.Table.onDelete(db);
|
CategoryDao.Table.onDelete(db);
|
||||||
Contribution.Table.onDelete(db);
|
ContributionDao.Table.onDelete(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface used to get log-out events
|
||||||
|
*/
|
||||||
public interface LogoutListener {
|
public interface LogoutListener {
|
||||||
void onLogoutComplete();
|
void onLogoutComplete();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package fr.free.nrw.commons;
|
package fr.free.nrw.commons;
|
||||||
|
|
||||||
import android.app.Service;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Binder;
|
import android.os.Binder;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
|
@ -9,7 +8,9 @@ import android.os.IBinder;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
|
|
||||||
public abstract class HandlerService<T> extends Service {
|
import fr.free.nrw.commons.di.CommonsDaggerService;
|
||||||
|
|
||||||
|
public abstract class HandlerService<T> extends CommonsDaggerService {
|
||||||
private volatile Looper threadLooper;
|
private volatile Looper threadLooper;
|
||||||
private volatile ServiceHandler threadHandler;
|
private volatile ServiceHandler threadHandler;
|
||||||
private String serviceName;
|
private String serviceName;
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,25 @@ package fr.free.nrw.commons;
|
||||||
|
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* represents Licence object
|
||||||
|
*/
|
||||||
public class License {
|
public class License {
|
||||||
private String key;
|
private String key;
|
||||||
private String template;
|
private String template;
|
||||||
private String url;
|
private String url;
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of License.
|
||||||
|
*
|
||||||
|
* @param key license key
|
||||||
|
* @param template license template
|
||||||
|
* @param url license URL
|
||||||
|
* @param name licence name
|
||||||
|
*
|
||||||
|
* @throws RuntimeException if License.key or Licence.template is null
|
||||||
|
*/
|
||||||
public License(String key, String template, String url, String name) {
|
public License(String key, String template, String url, String name) {
|
||||||
if (key == null) {
|
if (key == null) {
|
||||||
throw new RuntimeException("License.key must not be null");
|
throw new RuntimeException("License.key must not be null");
|
||||||
|
|
@ -21,10 +34,18 @@ public class License {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the license key.
|
||||||
|
* @return license key as a String.
|
||||||
|
*/
|
||||||
public String getKey() {
|
public String getKey() {
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the license template.
|
||||||
|
* @return license template as a String.
|
||||||
|
*/
|
||||||
public String getTemplate() {
|
public String getTemplate() {
|
||||||
return template;
|
return template;
|
||||||
}
|
}
|
||||||
|
|
@ -38,6 +59,12 @@ public class License {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the license URL
|
||||||
|
*
|
||||||
|
* @param language license language
|
||||||
|
* @return URL
|
||||||
|
*/
|
||||||
public @Nullable String getUrl(String language) {
|
public @Nullable String getUrl(String language) {
|
||||||
if (url == null) {
|
if (url == null) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -5,21 +5,31 @@ import android.content.res.Resources;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
import org.xmlpull.v1.XmlPullParser;
|
import org.xmlpull.v1.XmlPullParser;
|
||||||
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a list of Licenses
|
||||||
|
*/
|
||||||
public class LicenseList {
|
public class LicenseList {
|
||||||
private Map<String, License> licenses = new HashMap<>();
|
private Map<String, License> licenses = new HashMap<>();
|
||||||
private Resources res;
|
private Resources res;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs new instance of LicenceList
|
||||||
|
*
|
||||||
|
* @param activity License activity
|
||||||
|
*/
|
||||||
public LicenseList(Activity activity) {
|
public LicenseList(Activity activity) {
|
||||||
res = activity.getResources();
|
res = activity.getResources();
|
||||||
XmlPullParser parser = res.getXml(R.xml.wikimedia_licenses);
|
XmlPullParser parser = res.getXml(R.xml.wikimedia_licenses);
|
||||||
String namespace = "https://www.mediawiki.org/wiki/Extension:UploadWizard/xmlns/licenses";
|
String namespace = "https://www.mediawiki.org/wiki/Extension:UploadWizard/xmlns/licenses";
|
||||||
while (Utils.xmlFastForward(parser, namespace, "license")) {
|
while (xmlFastForward(parser, namespace, "license")) {
|
||||||
String id = parser.getAttributeValue(null, "id");
|
String id = parser.getAttributeValue(null, "id");
|
||||||
String template = parser.getAttributeValue(null, "template");
|
String template = parser.getAttributeValue(null, "template");
|
||||||
String url = parser.getAttributeValue(null, "url");
|
String url = parser.getAttributeValue(null, "url");
|
||||||
|
|
@ -29,14 +39,28 @@ public class LicenseList {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a collection of licenses
|
||||||
|
* @return License values
|
||||||
|
*/
|
||||||
public Collection<License> values() {
|
public Collection<License> values() {
|
||||||
return licenses.values();
|
return licenses.values();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets license
|
||||||
|
* @param key License key
|
||||||
|
* @return License that matches key
|
||||||
|
*/
|
||||||
public License get(String key) {
|
public License get(String key) {
|
||||||
return licenses.get(key);
|
return licenses.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a license from template
|
||||||
|
* @param template License template
|
||||||
|
* @return null
|
||||||
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
License licenseForTemplate(String template) {
|
License licenseForTemplate(String template) {
|
||||||
String ucTemplate = new PageTitle(template).getDisplayText();
|
String ucTemplate = new PageTitle(template).getDisplayText();
|
||||||
|
|
@ -48,6 +72,11 @@ public class LicenseList {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets template name id
|
||||||
|
* @param template License template
|
||||||
|
* @return name id of template
|
||||||
|
*/
|
||||||
private String nameIdForTemplate(String template) {
|
private String nameIdForTemplate(String template) {
|
||||||
// hack :D (converts dashes and periods to underscores)
|
// hack :D (converts dashes and periods to underscores)
|
||||||
// cc-by-sa-3.0 -> cc_by_sa_3_0
|
// cc-by-sa-3.0 -> cc_by_sa_3_0
|
||||||
|
|
@ -55,9 +84,44 @@ public class LicenseList {
|
||||||
"_").replace(".", "_");
|
"_").replace(".", "_");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets name of given template
|
||||||
|
* @param template License template
|
||||||
|
* @return name of template
|
||||||
|
*/
|
||||||
private String nameForTemplate(String template) {
|
private String nameForTemplate(String template) {
|
||||||
int nameId = res.getIdentifier("fr.free.nrw.commons:string/"
|
int nameId = res.getIdentifier("fr.free.nrw.commons:string/"
|
||||||
+ nameIdForTemplate(template), null, null);
|
+ nameIdForTemplate(template), null, null);
|
||||||
return (nameId != 0) ? res.getString(nameId) : template;
|
return (nameId != 0) ? res.getString(nameId) : template;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fast-forward an XmlPullParser to the next instance of the given element
|
||||||
|
* in the input stream (namespaced).
|
||||||
|
*
|
||||||
|
* @param parser
|
||||||
|
* @param namespace
|
||||||
|
* @param element
|
||||||
|
* @return true on match, false on failure
|
||||||
|
*/
|
||||||
|
private boolean xmlFastForward(XmlPullParser parser, String namespace, String element) {
|
||||||
|
try {
|
||||||
|
while (parser.next() != XmlPullParser.END_DOCUMENT) {
|
||||||
|
if (parser.getEventType() == XmlPullParser.START_TAG &&
|
||||||
|
parser.getNamespace().equals(namespace) &&
|
||||||
|
parser.getName().equals(element)) {
|
||||||
|
// We found it!
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (XmlPullParserException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -47,16 +47,35 @@ public class Media implements Parcelable {
|
||||||
private HashMap<String, Object> tags = new HashMap<>();
|
private HashMap<String, Object> tags = new HashMap<>();
|
||||||
private @Nullable LatLng coordinates;
|
private @Nullable LatLng coordinates;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides local constructor
|
||||||
|
*/
|
||||||
protected Media() {
|
protected Media() {
|
||||||
this.categories = new ArrayList<>();
|
this.categories = new ArrayList<>();
|
||||||
this.descriptions = new HashMap<>();
|
this.descriptions = new HashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a minimal constructor
|
||||||
|
*
|
||||||
|
* @param filename Media filename
|
||||||
|
*/
|
||||||
public Media(String filename) {
|
public Media(String filename) {
|
||||||
this();
|
this();
|
||||||
this.filename = filename;
|
this.filename = filename;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide Media constructor
|
||||||
|
* @param localUri Media URI
|
||||||
|
* @param imageUrl Media image URL
|
||||||
|
* @param filename Media filename
|
||||||
|
* @param description Media description
|
||||||
|
* @param dataLength Media date length
|
||||||
|
* @param dateCreated Media creation date
|
||||||
|
* @param dateUploaded Media date uploaded
|
||||||
|
* @param creator Media creator
|
||||||
|
*/
|
||||||
public Media(Uri localUri, String imageUrl, String filename, String description,
|
public Media(Uri localUri, String imageUrl, String filename, String description,
|
||||||
long dataLength, Date dateCreated, @Nullable Date dateUploaded, String creator) {
|
long dataLength, Date dateCreated, @Nullable Date dateUploaded, String creator) {
|
||||||
this();
|
this();
|
||||||
|
|
@ -90,19 +109,33 @@ public class Media implements Parcelable {
|
||||||
descriptions = in.readHashMap(ClassLoader.getSystemClassLoader());
|
descriptions = in.readHashMap(ClassLoader.getSystemClassLoader());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets tag of media
|
||||||
|
* @param key Media key
|
||||||
|
* @return Media tag
|
||||||
|
*/
|
||||||
public Object getTag(String key) {
|
public Object getTag(String key) {
|
||||||
return tags.get(key);
|
return tags.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifies( or creates a) tag of media
|
||||||
|
* @param key Media key
|
||||||
|
* @param value Media value
|
||||||
|
*/
|
||||||
public void setTag(String key, Object value) {
|
public void setTag(String key, Object value) {
|
||||||
tags.put(key, value);
|
tags.put(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets media display title
|
||||||
|
* @return Media title
|
||||||
|
*/
|
||||||
public String getDisplayTitle() {
|
public String getDisplayTitle() {
|
||||||
if (filename == null) {
|
if (filename == null) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
// FIXME: Gross hack bercause my regex skills suck maybe or I am too lazy who knows
|
// FIXME: Gross hack because my regex skills suck maybe or I am too lazy who knows
|
||||||
String title = getFilePageTitle().getDisplayText().replaceFirst("^File:", "");
|
String title = getFilePageTitle().getDisplayText().replaceFirst("^File:", "");
|
||||||
Matcher matcher = displayTitlePattern.matcher(title);
|
Matcher matcher = displayTitlePattern.matcher(title);
|
||||||
if (matcher.matches()) {
|
if (matcher.matches()) {
|
||||||
|
|
@ -112,109 +145,215 @@ public class Media implements Parcelable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets file page title
|
||||||
|
* @return New media page title
|
||||||
|
*/
|
||||||
public PageTitle getFilePageTitle() {
|
public PageTitle getFilePageTitle() {
|
||||||
return new PageTitle("File:" + getFilename().replaceFirst("^File:", ""));
|
return new PageTitle("File:" + getFilename().replaceFirst("^File:", ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets local URI
|
||||||
|
* @return Media local URI
|
||||||
|
*/
|
||||||
public Uri getLocalUri() {
|
public Uri getLocalUri() {
|
||||||
return localUri;
|
return localUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets image URL
|
||||||
|
* can be null.
|
||||||
|
* @return Image URL
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
public String getImageUrl() {
|
public String getImageUrl() {
|
||||||
if (imageUrl == null) {
|
if (imageUrl == null && this.getFilename() != null) {
|
||||||
imageUrl = Utils.makeThumbBaseUrl(this.getFilename());
|
imageUrl = Utils.makeThumbBaseUrl(this.getFilename());
|
||||||
}
|
}
|
||||||
return imageUrl;
|
return imageUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the name of the file.
|
||||||
|
* @return file name as a string
|
||||||
|
*/
|
||||||
public String getFilename() {
|
public String getFilename() {
|
||||||
return filename;
|
return filename;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the name of the file.
|
||||||
|
* @param filename the new name of the file
|
||||||
|
*/
|
||||||
public void setFilename(String filename) {
|
public void setFilename(String filename) {
|
||||||
this.filename = filename;
|
this.filename = filename;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the file description.
|
||||||
|
* @return file description as a string
|
||||||
|
*/
|
||||||
public String getDescription() {
|
public String getDescription() {
|
||||||
return description;
|
return description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the file description.
|
||||||
|
* @param description the new description of the file
|
||||||
|
*/
|
||||||
public void setDescription(String description) {
|
public void setDescription(String description) {
|
||||||
this.description = description;
|
this.description = description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the datalength of the file.
|
||||||
|
* @return file datalength as a long
|
||||||
|
*/
|
||||||
public long getDataLength() {
|
public long getDataLength() {
|
||||||
return dataLength;
|
return dataLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the datalength of the file.
|
||||||
|
* @param dataLength as a long
|
||||||
|
*/
|
||||||
public void setDataLength(long dataLength) {
|
public void setDataLength(long dataLength) {
|
||||||
this.dataLength = dataLength;
|
this.dataLength = dataLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the creation date of the file.
|
||||||
|
* @return creation date as a Date
|
||||||
|
*/
|
||||||
public Date getDateCreated() {
|
public Date getDateCreated() {
|
||||||
return dateCreated;
|
return dateCreated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the creation date of the file.
|
||||||
|
* @param date creation date as a Date
|
||||||
|
*/
|
||||||
public void setDateCreated(Date date) {
|
public void setDateCreated(Date date) {
|
||||||
this.dateCreated = date;
|
this.dateCreated = date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the upload date of the file.
|
||||||
|
* Can be null.
|
||||||
|
* @return upload date as a Date
|
||||||
|
*/
|
||||||
public @Nullable
|
public @Nullable
|
||||||
Date getDateUploaded() {
|
Date getDateUploaded() {
|
||||||
return dateUploaded;
|
return dateUploaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the name of the creator of the file.
|
||||||
|
* @return creator name as a String
|
||||||
|
*/
|
||||||
public String getCreator() {
|
public String getCreator() {
|
||||||
return creator;
|
return creator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the creator name of the file.
|
||||||
|
* @param creator creator name as a string
|
||||||
|
*/
|
||||||
public void setCreator(String creator) {
|
public void setCreator(String creator) {
|
||||||
this.creator = creator;
|
this.creator = creator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the width of the media.
|
||||||
|
* @return file width as an int
|
||||||
|
*/
|
||||||
public int getWidth() {
|
public int getWidth() {
|
||||||
return width;
|
return width;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the width of the media.
|
||||||
|
* @param width file width as an int
|
||||||
|
*/
|
||||||
public void setWidth(int width) {
|
public void setWidth(int width) {
|
||||||
this.width = width;
|
this.width = width;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the height of the media.
|
||||||
|
* @return file height as an int
|
||||||
|
*/
|
||||||
public int getHeight() {
|
public int getHeight() {
|
||||||
return height;
|
return height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the height of the media.
|
||||||
|
* @param height file height as an int
|
||||||
|
*/
|
||||||
public void setHeight(int height) {
|
public void setHeight(int height) {
|
||||||
this.height = height;
|
this.height = height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the license name of the file.
|
||||||
|
* @return license as a String
|
||||||
|
*/
|
||||||
public String getLicense() {
|
public String getLicense() {
|
||||||
return license;
|
return license;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the license name of the file.
|
||||||
|
* @param license license name as a String
|
||||||
|
*/
|
||||||
public void setLicense(String license) {
|
public void setLicense(String license) {
|
||||||
this.license = license;
|
this.license = license;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the coordinates of where the file was created.
|
||||||
|
* @return file coordinates as a LatLng
|
||||||
|
*/
|
||||||
public @Nullable
|
public @Nullable
|
||||||
LatLng getCoordinates() {
|
LatLng getCoordinates() {
|
||||||
return coordinates;
|
return coordinates;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the coordinates of where the file was created.
|
||||||
|
* @param coordinates file coordinates as a LatLng
|
||||||
|
*/
|
||||||
public void setCoordinates(@Nullable LatLng coordinates) {
|
public void setCoordinates(@Nullable LatLng coordinates) {
|
||||||
this.coordinates = coordinates;
|
this.coordinates = coordinates;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the categories the file falls under.
|
||||||
|
* @return file categories as an ArrayList of Strings
|
||||||
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public ArrayList<String> getCategories() {
|
public ArrayList<String> getCategories() {
|
||||||
return (ArrayList<String>) categories.clone(); // feels dirty
|
return (ArrayList<String>) categories.clone(); // feels dirty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the categories the file falls under.
|
||||||
|
* </p>
|
||||||
|
* Does not append: i.e. will clear the current categories
|
||||||
|
* and then add the specified ones.
|
||||||
|
* @param categories file categories as a list of Strings
|
||||||
|
*/
|
||||||
public void setCategories(List<String> categories) {
|
public void setCategories(List<String> categories) {
|
||||||
this.categories.clear();
|
this.categories.clear();
|
||||||
this.categories.addAll(categories);
|
this.categories.addAll(categories);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifies (or sets) media descriptions
|
||||||
|
* @param descriptions Media descriptions
|
||||||
|
*/
|
||||||
void setDescriptions(Map<String, String> descriptions) {
|
void setDescriptions(Map<String, String> descriptions) {
|
||||||
for (String key : this.descriptions.keySet()) {
|
for (String key : this.descriptions.keySet()) {
|
||||||
this.descriptions.remove(key);
|
this.descriptions.remove(key);
|
||||||
|
|
@ -224,6 +363,11 @@ public class Media implements Parcelable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets media description in preferred language
|
||||||
|
* @param preferredLanguage Language preferred
|
||||||
|
* @return Description in preferred language
|
||||||
|
*/
|
||||||
public String getDescription(String preferredLanguage) {
|
public String getDescription(String preferredLanguage) {
|
||||||
if (descriptions.containsKey(preferredLanguage)) {
|
if (descriptions.containsKey(preferredLanguage)) {
|
||||||
// See if the requested language is there.
|
// See if the requested language is there.
|
||||||
|
|
@ -240,11 +384,21 @@ public class Media implements Parcelable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method of Parcelable interface
|
||||||
|
* @return zero
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public int describeContents() {
|
public int describeContents() {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a way to transfer information between two or more
|
||||||
|
* activities.
|
||||||
|
* @param parcel Instance of Parcel
|
||||||
|
* @param flags Parcel flag
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void writeToParcel(Parcel parcel, int flags) {
|
public void writeToParcel(Parcel parcel, int flags) {
|
||||||
parcel.writeParcelable(localUri, flags);
|
parcel.writeParcelable(localUri, flags);
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,12 @@ import org.xml.sax.SAXException;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
import javax.xml.parsers.DocumentBuilder;
|
import javax.xml.parsers.DocumentBuilder;
|
||||||
import javax.xml.parsers.DocumentBuilderFactory;
|
import javax.xml.parsers.DocumentBuilderFactory;
|
||||||
import javax.xml.parsers.ParserConfigurationException;
|
import javax.xml.parsers.ParserConfigurationException;
|
||||||
|
|
@ -33,46 +33,39 @@ import timber.log.Timber;
|
||||||
* which are not intrinsic to the media and may change due to editing.
|
* which are not intrinsic to the media and may change due to editing.
|
||||||
*/
|
*/
|
||||||
public class MediaDataExtractor {
|
public class MediaDataExtractor {
|
||||||
|
private final MediaWikiApi mediaWikiApi;
|
||||||
private boolean fetched;
|
private boolean fetched;
|
||||||
|
|
||||||
private String filename;
|
|
||||||
private ArrayList<String> categories;
|
private ArrayList<String> categories;
|
||||||
private Map<String, String> descriptions;
|
private Map<String, String> descriptions;
|
||||||
private Date date;
|
|
||||||
private String license;
|
private String license;
|
||||||
private @Nullable LatLng coordinates;
|
private @Nullable LatLng coordinates;
|
||||||
private LicenseList licenseList;
|
|
||||||
|
|
||||||
/**
|
@Inject
|
||||||
* @param filename of the target media object, should include 'File:' prefix
|
public MediaDataExtractor(MediaWikiApi mwApi) {
|
||||||
*/
|
this.categories = new ArrayList<>();
|
||||||
public MediaDataExtractor(String filename, LicenseList licenseList) {
|
this.descriptions = new HashMap<>();
|
||||||
this.filename = filename;
|
this.fetched = false;
|
||||||
categories = new ArrayList<>();
|
this.mediaWikiApi = mwApi;
|
||||||
descriptions = new HashMap<>();
|
|
||||||
fetched = false;
|
|
||||||
this.licenseList = licenseList;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* Actually fetch the data over the network.
|
* Actually fetch the data over the network.
|
||||||
* todo: use local caching?
|
* todo: use local caching?
|
||||||
*
|
*
|
||||||
* Warning: synchronous i/o, call on a background thread
|
* Warning: synchronous i/o, call on a background thread
|
||||||
*/
|
*/
|
||||||
public void fetch() throws IOException {
|
public void fetch(String filename, LicenseList licenseList) throws IOException {
|
||||||
if (fetched) {
|
if (fetched) {
|
||||||
throw new IllegalStateException("Tried to call MediaDataExtractor.fetch() again.");
|
throw new IllegalStateException("Tried to call MediaDataExtractor.fetch() again.");
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
|
MediaResult result = mediaWikiApi.fetchMediaByFilename(filename);
|
||||||
MediaResult result = api.fetchMediaByFilename(filename);
|
|
||||||
|
|
||||||
// In-page category links are extracted from source, as XML doesn't cover [[links]]
|
// In-page category links are extracted from source, as XML doesn't cover [[links]]
|
||||||
extractCategories(result.getWikiSource());
|
extractCategories(result.getWikiSource());
|
||||||
|
|
||||||
// Description template info is extracted from preprocessor XML
|
// Description template info is extracted from preprocessor XML
|
||||||
processWikiParseTree(result.getParseTreeXmlSource());
|
processWikiParseTree(result.getParseTreeXmlSource(), licenseList);
|
||||||
fetched = true;
|
fetched = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,7 +84,7 @@ public class MediaDataExtractor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processWikiParseTree(String source) throws IOException {
|
private void processWikiParseTree(String source, LicenseList licenseList) throws IOException {
|
||||||
Document doc;
|
Document doc;
|
||||||
try {
|
try {
|
||||||
DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
|
DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
|
||||||
|
|
@ -153,7 +146,7 @@ public class MediaDataExtractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Node findTemplate(Element parentNode, String title_) throws IOException {
|
private Node findTemplate(Element parentNode, String title_) throws IOException {
|
||||||
String title= new PageTitle(title_).getDisplayText();
|
String title = new PageTitle(title_).getDisplayText();
|
||||||
NodeList nodes = parentNode.getChildNodes();
|
NodeList nodes = parentNode.getChildNodes();
|
||||||
for (int i = 0, length = nodes.getLength(); i < length; i++) {
|
for (int i = 0, length = nodes.getLength(); i < length; i++) {
|
||||||
Node node = nodes.item(i);
|
Node node = nodes.item(i);
|
||||||
|
|
@ -179,7 +172,7 @@ public class MediaDataExtractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static abstract class TemplateChildNodeComparator {
|
private static abstract class TemplateChildNodeComparator {
|
||||||
abstract public boolean match(Node node);
|
public abstract boolean match(Node node);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Node findTemplateParameter(Node templateNode, String name) throws IOException {
|
private Node findTemplateParameter(Node templateNode, String name) throws IOException {
|
||||||
|
|
@ -290,7 +283,7 @@ public class MediaDataExtractor {
|
||||||
/**
|
/**
|
||||||
* Take our metadata and inject it into a live Media object.
|
* Take our metadata and inject it into a live Media object.
|
||||||
* Media object might contain stale or cached data, or emptiness.
|
* Media object might contain stale or cached data, or emptiness.
|
||||||
* @param media
|
* @param media Media object to inject into
|
||||||
*/
|
*/
|
||||||
public void fill(Media media) {
|
public void fill(Media media) {
|
||||||
if (!fetched) {
|
if (!fetched) {
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,17 @@ import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
|
|
||||||
class MediaThumbnailFetchTask extends AsyncTask<String, String, String> {
|
class MediaThumbnailFetchTask extends AsyncTask<String, String, String> {
|
||||||
protected final Media media;
|
protected final Media media;
|
||||||
|
private MediaWikiApi mediaWikiApi;
|
||||||
|
|
||||||
public MediaThumbnailFetchTask(@NonNull Media media) {
|
public MediaThumbnailFetchTask(@NonNull Media media, MediaWikiApi mwApi) {
|
||||||
this.media = media;
|
this.media = media;
|
||||||
|
this.mediaWikiApi = mwApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String doInBackground(String... params) {
|
protected String doInBackground(String... params) {
|
||||||
try {
|
try {
|
||||||
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
|
return mediaWikiApi.findThumbnailByFilename(params[0]);
|
||||||
return api.findThumbnailByFilename(params[0]);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// Do something better!
|
// Do something better!
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import android.content.Context;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.graphics.drawable.VectorDrawableCompat;
|
import android.support.graphics.drawable.VectorDrawableCompat;
|
||||||
|
import android.support.v4.util.LruCache;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
@ -11,9 +12,16 @@ import android.widget.Toast;
|
||||||
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
|
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
|
||||||
import com.facebook.drawee.view.SimpleDraweeView;
|
import com.facebook.drawee.view.SimpleDraweeView;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||||
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
public class MediaWikiImageView extends SimpleDraweeView {
|
public class MediaWikiImageView extends SimpleDraweeView {
|
||||||
|
@Inject MediaWikiApi mwApi;
|
||||||
|
@Inject LruCache<String, String> thumbnailUrlCache;
|
||||||
|
|
||||||
private ThumbnailFetchTask currentThumbnailTask;
|
private ThumbnailFetchTask currentThumbnailTask;
|
||||||
|
|
||||||
public MediaWikiImageView(Context context) {
|
public MediaWikiImageView(Context context) {
|
||||||
|
|
@ -31,19 +39,23 @@ public class MediaWikiImageView extends SimpleDraweeView {
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the media. Fetches its thumbnail if necessary.
|
||||||
|
* @param media the new media
|
||||||
|
*/
|
||||||
public void setMedia(Media media) {
|
public void setMedia(Media media) {
|
||||||
if (currentThumbnailTask != null) {
|
if (currentThumbnailTask != null) {
|
||||||
currentThumbnailTask.cancel(true);
|
currentThumbnailTask.cancel(true);
|
||||||
}
|
}
|
||||||
if(media == null) {
|
if (media == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CommonsApplication.getInstance().getThumbnailUrlCache().get(media.getFilename()) != null) {
|
if (thumbnailUrlCache.get(media.getFilename()) != null) {
|
||||||
setImageUrl(CommonsApplication.getInstance().getThumbnailUrlCache().get(media.getFilename()));
|
setImageUrl(thumbnailUrlCache.get(media.getFilename()));
|
||||||
} else {
|
} else {
|
||||||
setImageUrl(null);
|
setImageUrl(null);
|
||||||
currentThumbnailTask = new ThumbnailFetchTask(media);
|
currentThumbnailTask = new ThumbnailFetchTask(media, mwApi);
|
||||||
currentThumbnailTask.execute(media.getFilename());
|
currentThumbnailTask.execute(media.getFilename());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -56,7 +68,15 @@ public class MediaWikiImageView extends SimpleDraweeView {
|
||||||
super.onDetachedFromWindow();
|
super.onDetachedFromWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes MediaWikiImageView.
|
||||||
|
*/
|
||||||
private void init() {
|
private void init() {
|
||||||
|
ApplicationlessInjection
|
||||||
|
.getInstance(getContext()
|
||||||
|
.getApplicationContext())
|
||||||
|
.getCommonsApplicationComponent()
|
||||||
|
.inject(this);
|
||||||
setHierarchy(GenericDraweeHierarchyBuilder
|
setHierarchy(GenericDraweeHierarchyBuilder
|
||||||
.newInstance(getResources())
|
.newInstance(getResources())
|
||||||
.setPlaceholderImage(VectorDrawableCompat.create(getResources(),
|
.setPlaceholderImage(VectorDrawableCompat.create(getResources(),
|
||||||
|
|
@ -66,13 +86,17 @@ public class MediaWikiImageView extends SimpleDraweeView {
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the image from the URL.
|
||||||
|
* @param url the URL of the image
|
||||||
|
*/
|
||||||
private void setImageUrl(@Nullable String url) {
|
private void setImageUrl(@Nullable String url) {
|
||||||
setImageURI(url);
|
setImageURI(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ThumbnailFetchTask extends MediaThumbnailFetchTask {
|
private class ThumbnailFetchTask extends MediaThumbnailFetchTask {
|
||||||
ThumbnailFetchTask(@NonNull Media media) {
|
ThumbnailFetchTask(@NonNull Media media, @NonNull MediaWikiApi mwApi) {
|
||||||
super(media);
|
super(media, mwApi);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -85,7 +109,7 @@ public class MediaWikiImageView extends SimpleDraweeView {
|
||||||
} else {
|
} else {
|
||||||
// only cache meaningful thumbnails received from network.
|
// only cache meaningful thumbnails received from network.
|
||||||
try {
|
try {
|
||||||
CommonsApplication.getInstance().getThumbnailUrlCache().put(media.getFilename(), result);
|
thumbnailUrlCache.put(media.getFilename(), result);
|
||||||
} catch (NullPointerException npe) {
|
} catch (NullPointerException npe) {
|
||||||
Timber.e("error when adding pic to cache " + npe);
|
Timber.e("error when adding pic to cache " + npe);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,12 @@ public class PageTitle {
|
||||||
return titleKey;
|
return titleKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the canonicalized title for displaying (such as "File:My example.jpg").
|
||||||
|
* </p>
|
||||||
|
* Essentially equivalent to getPrefixedText
|
||||||
|
* @return canonical title as a String
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return getPrefixedText();
|
return getPrefixedText();
|
||||||
|
|
|
||||||
|
|
@ -1,99 +1,26 @@
|
||||||
package fr.free.nrw.commons;
|
package fr.free.nrw.commons;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Build;
|
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.text.Html;
|
import android.support.annotation.NonNull;
|
||||||
import android.text.Spanned;
|
|
||||||
|
|
||||||
import org.apache.commons.codec.binary.Hex;
|
import org.apache.commons.codec.binary.Hex;
|
||||||
import org.apache.commons.codec.digest.DigestUtils;
|
import org.apache.commons.codec.digest.DigestUtils;
|
||||||
import org.w3c.dom.Node;
|
|
||||||
import org.xmlpull.v1.XmlPullParser;
|
|
||||||
import org.xmlpull.v1.XmlPullParserException;
|
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
import java.io.BufferedReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStreamReader;
|
||||||
import java.io.StringWriter;
|
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.math.BigInteger;
|
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.text.ParseException;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.TimeZone;
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import javax.xml.transform.Transformer;
|
|
||||||
import javax.xml.transform.TransformerConfigurationException;
|
|
||||||
import javax.xml.transform.TransformerException;
|
|
||||||
import javax.xml.transform.TransformerFactory;
|
|
||||||
import javax.xml.transform.TransformerFactoryConfigurationError;
|
|
||||||
import javax.xml.transform.dom.DOMSource;
|
|
||||||
import javax.xml.transform.stream.StreamResult;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.settings.Prefs;
|
import fr.free.nrw.commons.settings.Prefs;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
public class Utils {
|
public class Utils {
|
||||||
|
|
||||||
// Get SHA1 of file from input stream
|
|
||||||
public static String getSHA1(InputStream is) {
|
|
||||||
|
|
||||||
MessageDigest digest;
|
|
||||||
try {
|
|
||||||
digest = MessageDigest.getInstance("SHA1");
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
Timber.e(e, "Exception while getting Digest");
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] buffer = new byte[8192];
|
|
||||||
int read;
|
|
||||||
try {
|
|
||||||
while ((read = is.read(buffer)) > 0) {
|
|
||||||
digest.update(buffer, 0, read);
|
|
||||||
}
|
|
||||||
byte[] md5sum = digest.digest();
|
|
||||||
BigInteger bigInt = new BigInteger(1, md5sum);
|
|
||||||
String output = bigInt.toString(16);
|
|
||||||
// Fill to 40 chars
|
|
||||||
output = String.format("%40s", output).replace(' ', '0');
|
|
||||||
Timber.i("File SHA1: %s", output);
|
|
||||||
|
|
||||||
return output;
|
|
||||||
} catch (IOException e) {
|
|
||||||
Timber.e(e, "IO Exception");
|
|
||||||
return "";
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
is.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
Timber.e(e, "Exception on closing MD5 input stream");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fix Html.fromHtml is deprecated problem
|
|
||||||
*
|
|
||||||
* @param source provided Html string
|
|
||||||
* @return returned Spanned of appropriate method according to version check
|
|
||||||
*/
|
|
||||||
public static Spanned fromHtml(String source) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
||||||
return Html.fromHtml(source, Html.FROM_HTML_MODE_LEGACY);
|
|
||||||
} else {
|
|
||||||
//noinspection deprecation
|
|
||||||
return Html.fromHtml(source);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Strips localization symbols from a string.
|
* Strips localization symbols from a string.
|
||||||
* Removes the suffix after "@" and quotes.
|
* Removes the suffix after "@" and quotes.
|
||||||
|
|
@ -110,49 +37,23 @@ public class Utils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Date parseMWDate(String mwDate) {
|
/**
|
||||||
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH); // Assuming MW always gives me UTC
|
* Creates an URL for thumbnail
|
||||||
try {
|
*
|
||||||
return isoFormat.parse(mwDate);
|
* @param filename Thumbnail file name
|
||||||
} catch (ParseException e) {
|
* @return URL of thumbnail
|
||||||
throw new RuntimeException(e);
|
*/
|
||||||
}
|
public static String makeThumbBaseUrl(@NonNull String filename) {
|
||||||
}
|
|
||||||
|
|
||||||
public static String toMWDate(Date date) {
|
|
||||||
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH); // Assuming MW always gives me UTC
|
|
||||||
isoFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
|
||||||
return isoFormat.format(date);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String makeThumbBaseUrl(String filename) {
|
|
||||||
String name = new PageTitle(filename).getPrefixedText();
|
String name = new PageTitle(filename).getPrefixedText();
|
||||||
String sha = new String(Hex.encodeHex(DigestUtils.md5(name)));
|
String sha = new String(Hex.encodeHex(DigestUtils.md5(name)));
|
||||||
return String.format("%s/%s/%s/%s", BuildConfig.IMAGE_URL_BASE, sha.substring(0, 1), sha.substring(0, 2), urlEncode(name));
|
return String.format("%s/%s/%s/%s", BuildConfig.IMAGE_URL_BASE, sha.substring(0, 1), sha.substring(0, 2), urlEncode(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getStringFromDOM(Node dom) {
|
/**
|
||||||
Transformer transformer = null;
|
* URL Encode an URL in UTF-8 format
|
||||||
try {
|
* @param url Unformatted URL
|
||||||
transformer = TransformerFactory.newInstance().newTransformer();
|
* @return Encoded URL
|
||||||
} catch (TransformerConfigurationException | TransformerFactoryConfigurationError e) {
|
*/
|
||||||
// TODO Auto-generated catch block
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
StringWriter outputStream = new StringWriter();
|
|
||||||
DOMSource domSource = new DOMSource(dom);
|
|
||||||
StreamResult strResult = new StreamResult(outputStream);
|
|
||||||
|
|
||||||
try {
|
|
||||||
transformer.transform(domSource, strResult);
|
|
||||||
} catch (TransformerException e) {
|
|
||||||
// TODO Auto-generated catch block
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
return outputStream.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String urlEncode(String url) {
|
public static String urlEncode(String url) {
|
||||||
try {
|
try {
|
||||||
return URLEncoder.encode(url, "utf-8");
|
return URLEncoder.encode(url, "utf-8");
|
||||||
|
|
@ -161,39 +62,21 @@ public class Utils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static long countBytes(InputStream stream) throws IOException {
|
/**
|
||||||
long count = 0;
|
* Capitalizes the first character of a string.
|
||||||
BufferedInputStream bis = new BufferedInputStream(stream);
|
*
|
||||||
while (bis.read() != -1) {
|
* @param string String to alter
|
||||||
count++;
|
* @return string with capitalized first character
|
||||||
}
|
*/
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String capitalize(String string) {
|
public static String capitalize(String string) {
|
||||||
return string.substring(0, 1).toUpperCase(Locale.getDefault()) + string.substring(1);
|
return string.substring(0, 1).toUpperCase(Locale.getDefault()) + string.substring(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String licenseTemplateFor(String license) {
|
/**
|
||||||
switch (license) {
|
* Generates licence name with given ID
|
||||||
case Prefs.Licenses.CC_BY_3:
|
* @param license License ID
|
||||||
return "{{self|cc-by-3.0}}";
|
* @return Name of license
|
||||||
case Prefs.Licenses.CC_BY_4:
|
*/
|
||||||
return "{{self|cc-by-4.0}}";
|
|
||||||
case Prefs.Licenses.CC_BY_SA_3:
|
|
||||||
return "{{self|cc-by-sa-3.0}}";
|
|
||||||
case Prefs.Licenses.CC_BY_SA_4:
|
|
||||||
return "{{self|cc-by-sa-4.0}}";
|
|
||||||
case Prefs.Licenses.CC0:
|
|
||||||
return "{{self|cc-zero}}";
|
|
||||||
case Prefs.Licenses.CC_BY:
|
|
||||||
return "{{self|cc-by-3.0}}";
|
|
||||||
case Prefs.Licenses.CC_BY_SA:
|
|
||||||
return "{{self|cc-by-sa-3.0}}";
|
|
||||||
}
|
|
||||||
throw new RuntimeException("Unrecognized license value: " + license);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int licenseNameFor(String license) {
|
public static int licenseNameFor(String license) {
|
||||||
switch (license) {
|
switch (license) {
|
||||||
case Prefs.Licenses.CC_BY_3:
|
case Prefs.Licenses.CC_BY_3:
|
||||||
|
|
@ -214,51 +97,12 @@ public class Utils {
|
||||||
throw new RuntimeException("Unrecognized license value: " + license);
|
throw new RuntimeException("Unrecognized license value: " + license);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String licenseUrlFor(String license) {
|
|
||||||
switch (license) {
|
|
||||||
case Prefs.Licenses.CC_BY_3:
|
|
||||||
return "https://creativecommons.org/licenses/by/3.0/";
|
|
||||||
case Prefs.Licenses.CC_BY_4:
|
|
||||||
return "https://creativecommons.org/licenses/by/4.0/";
|
|
||||||
case Prefs.Licenses.CC_BY_SA_3:
|
|
||||||
return "https://creativecommons.org/licenses/by-sa/3.0/";
|
|
||||||
case Prefs.Licenses.CC_BY_SA_4:
|
|
||||||
return "https://creativecommons.org/licenses/by-sa/4.0/";
|
|
||||||
case Prefs.Licenses.CC0:
|
|
||||||
return "https://creativecommons.org/publicdomain/zero/1.0/";
|
|
||||||
}
|
|
||||||
throw new RuntimeException("Unrecognized license value: " + license);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fast-forward an XmlPullParser to the next instance of the given element
|
* Fixing incorrect extension
|
||||||
* in the input stream (namespaced).
|
* @param title File name
|
||||||
*
|
* @param extension Correct extension
|
||||||
* @param parser
|
* @return File with correct extension
|
||||||
* @param namespace
|
|
||||||
* @param element
|
|
||||||
* @return true on match, false on failure
|
|
||||||
*/
|
*/
|
||||||
public static boolean xmlFastForward(XmlPullParser parser, String namespace, String element) {
|
|
||||||
try {
|
|
||||||
while (parser.next() != XmlPullParser.END_DOCUMENT) {
|
|
||||||
if (parser.getEventType() == XmlPullParser.START_TAG
|
|
||||||
&& parser.getNamespace().equals(namespace)
|
|
||||||
&& parser.getName().equals(element)) {
|
|
||||||
// We found it!
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} catch (XmlPullParserException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String fixExtension(String title, String extension) {
|
public static String fixExtension(String title, String extension) {
|
||||||
Pattern jpegPattern = Pattern.compile("\\.jpeg$", Pattern.CASE_INSENSITIVE);
|
Pattern jpegPattern = Pattern.compile("\\.jpeg$", Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
|
@ -274,11 +118,45 @@ public class Utils {
|
||||||
return title;
|
return title;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isNullOrWhiteSpace(String value) {
|
/**
|
||||||
return value == null || value.trim().isEmpty();
|
* Tells whether dark theme is active or not
|
||||||
}
|
* @param context Activity context
|
||||||
|
* @return The state of dark theme
|
||||||
|
*/
|
||||||
public static boolean isDarkTheme(Context context) {
|
public static boolean isDarkTheme(Context context) {
|
||||||
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean("theme", false);
|
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean("theme", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will be used to fetch the logs generated by the app ever since the beginning of times....
|
||||||
|
* i.e. since the time the app started.
|
||||||
|
*
|
||||||
|
* @return String containing all the logs since the time the app started
|
||||||
|
*/
|
||||||
|
public static String getAppLogs() {
|
||||||
|
final String processId = Integer.toString(android.os.Process.myPid());
|
||||||
|
|
||||||
|
StringBuilder stringBuilder = new StringBuilder();
|
||||||
|
|
||||||
|
try {
|
||||||
|
String[] command = new String[] {"logcat","-d","-v","threadtime"};
|
||||||
|
|
||||||
|
Process process = Runtime.getRuntime().exec(command);
|
||||||
|
|
||||||
|
BufferedReader bufferedReader = new BufferedReader(
|
||||||
|
new InputStreamReader(process.getInputStream())
|
||||||
|
);
|
||||||
|
|
||||||
|
String line;
|
||||||
|
while ((line = bufferedReader.readLine()) != null) {
|
||||||
|
if (line.contains(processId)) {
|
||||||
|
stringBuilder.append(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
Timber.e("getAppLogs failed", ioe);
|
||||||
|
}
|
||||||
|
|
||||||
|
return stringBuilder.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,11 @@ public class WelcomeActivity extends BaseActivity {
|
||||||
|
|
||||||
private WelcomePagerAdapter adapter = new WelcomePagerAdapter();
|
private WelcomePagerAdapter adapter = new WelcomePagerAdapter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialises exiting fields and dependencies
|
||||||
|
*
|
||||||
|
* @param savedInstanceState WelcomeActivity bundled data
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
@ -30,12 +35,20 @@ public class WelcomeActivity extends BaseActivity {
|
||||||
adapter.setCallback(this::finish);
|
adapter.setCallback(this::finish);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* References WelcomePageAdapter to null before the activity is destroyed
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
adapter.setCallback(null);
|
adapter.setCallback(null);
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a way to change current activity to WelcomeActivity
|
||||||
|
*
|
||||||
|
* @param context Activity context
|
||||||
|
*/
|
||||||
public static void startYourself(Context context) {
|
public static void startYourself(Context context) {
|
||||||
Intent welcomeIntent = new Intent(context, WelcomeActivity.class);
|
Intent welcomeIntent = new Intent(context, WelcomeActivity.class);
|
||||||
context.startActivity(welcomeIntent);
|
context.startActivity(welcomeIntent);
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,6 @@ import butterknife.ButterKnife;
|
||||||
import butterknife.OnClick;
|
import butterknife.OnClick;
|
||||||
|
|
||||||
public class WelcomePagerAdapter extends PagerAdapter {
|
public class WelcomePagerAdapter extends PagerAdapter {
|
||||||
private static final int PAGE_FINAL = 4;
|
|
||||||
private Callback callback;
|
|
||||||
|
|
||||||
public interface Callback {
|
|
||||||
void onYesClicked();
|
|
||||||
}
|
|
||||||
|
|
||||||
static final int[] PAGE_LAYOUTS = new int[]{
|
static final int[] PAGE_LAYOUTS = new int[]{
|
||||||
R.layout.welcome_wikipedia,
|
R.layout.welcome_wikipedia,
|
||||||
R.layout.welcome_do_upload,
|
R.layout.welcome_do_upload,
|
||||||
|
|
@ -24,16 +17,34 @@ public class WelcomePagerAdapter extends PagerAdapter {
|
||||||
R.layout.welcome_image_details,
|
R.layout.welcome_image_details,
|
||||||
R.layout.welcome_final
|
R.layout.welcome_final
|
||||||
};
|
};
|
||||||
|
private static final int PAGE_FINAL = 4;
|
||||||
|
private Callback callback;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes callback to provided one
|
||||||
|
*
|
||||||
|
* @param callback New callback
|
||||||
|
* it can be null.
|
||||||
|
*/
|
||||||
public void setCallback(@Nullable Callback callback) {
|
public void setCallback(@Nullable Callback callback) {
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets total number of layouts
|
||||||
|
* @return Number of layouts
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public int getCount() {
|
public int getCount() {
|
||||||
return PAGE_LAYOUTS.length;
|
return PAGE_LAYOUTS.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares given view with provided object
|
||||||
|
* @param view Adapter view
|
||||||
|
* @param object Adapter object
|
||||||
|
* @return Equality between view and object
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean isViewFromObject(View view, Object object) {
|
public boolean isViewFromObject(View view, Object object) {
|
||||||
return (view == object);
|
return (view == object);
|
||||||
|
|
@ -52,16 +63,29 @@ public class WelcomePagerAdapter extends PagerAdapter {
|
||||||
return layout;
|
return layout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a way to remove an item from container
|
||||||
|
* @param container Adapter view group container
|
||||||
|
* @param position Index of item
|
||||||
|
* @param obj Adapter object
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void destroyItem(ViewGroup container, int position, Object obj) {
|
public void destroyItem(ViewGroup container, int position, Object obj) {
|
||||||
container.removeView((View) obj);
|
container.removeView((View) obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interface Callback {
|
||||||
|
void onYesClicked();
|
||||||
|
}
|
||||||
|
|
||||||
class ViewHolder {
|
class ViewHolder {
|
||||||
ViewHolder(View view) {
|
ViewHolder(View view) {
|
||||||
ButterKnife.bind(this, view);
|
ButterKnife.bind(this, view);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers on click callback on button click
|
||||||
|
*/
|
||||||
@OnClick(R.id.welcomeYesButton)
|
@OnClick(R.id.welcomeYesButton)
|
||||||
void onClicked() {
|
void onClicked() {
|
||||||
if (callback != null) {
|
if (callback != null) {
|
||||||
|
|
|
||||||
|
|
@ -4,21 +4,33 @@ import android.accounts.Account;
|
||||||
import android.accounts.AccountAuthenticatorResponse;
|
import android.accounts.AccountAuthenticatorResponse;
|
||||||
import android.accounts.AccountManager;
|
import android.accounts.AccountManager;
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
|
|
||||||
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
|
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
import static android.accounts.AccountManager.ERROR_CODE_REMOTE_EXCEPTION;
|
||||||
|
import static android.accounts.AccountManager.KEY_ACCOUNT_NAME;
|
||||||
|
import static android.accounts.AccountManager.KEY_ACCOUNT_TYPE;
|
||||||
|
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.CONTRIBUTION_AUTHORITY;
|
||||||
|
import static fr.free.nrw.commons.modifications.ModificationsContentProvider.MODIFICATIONS_AUTHORITY;
|
||||||
|
|
||||||
public class AccountUtil {
|
public class AccountUtil {
|
||||||
|
|
||||||
public static void createAccount(@Nullable AccountAuthenticatorResponse response,
|
public static final String ACCOUNT_TYPE = "fr.free.nrw.commons";
|
||||||
|
public static final String AUTH_COOKIE = "authCookie";
|
||||||
|
public static final String AUTH_TOKEN_TYPE = "CommonsAndroid";
|
||||||
|
private final Context context;
|
||||||
|
|
||||||
|
public AccountUtil(Context context) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void createAccount(@Nullable AccountAuthenticatorResponse response,
|
||||||
String username, String password) {
|
String username, String password) {
|
||||||
|
|
||||||
Account account = new Account(username, accountType());
|
Account account = new Account(username, ACCOUNT_TYPE);
|
||||||
boolean created = accountManager().addAccountExplicitly(account, password, null);
|
boolean created = accountManager().addAccountExplicitly(account, password, null);
|
||||||
|
|
||||||
Timber.d("account creation " + (created ? "successful" : "failure"));
|
Timber.d("account creation " + (created ? "successful" : "failure"));
|
||||||
|
|
@ -26,8 +38,8 @@ public class AccountUtil {
|
||||||
if (created) {
|
if (created) {
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
Bundle bundle = new Bundle();
|
Bundle bundle = new Bundle();
|
||||||
bundle.putString(AccountManager.KEY_ACCOUNT_NAME, username);
|
bundle.putString(KEY_ACCOUNT_NAME, username);
|
||||||
bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, accountType());
|
bundle.putString(KEY_ACCOUNT_TYPE, ACCOUNT_TYPE);
|
||||||
|
|
||||||
|
|
||||||
response.onResult(bundle);
|
response.onResult(bundle);
|
||||||
|
|
@ -35,28 +47,18 @@ public class AccountUtil {
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
response.onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, "");
|
response.onError(ERROR_CODE_REMOTE_EXCEPTION, "");
|
||||||
}
|
}
|
||||||
Timber.d("account creation failure");
|
Timber.d("account creation failure");
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: If the user turns it off, it shouldn't be auto turned back on
|
// FIXME: If the user turns it off, it shouldn't be auto turned back on
|
||||||
ContentResolver.setSyncAutomatically(account, ContributionsContentProvider.AUTHORITY, true); // Enable sync by default!
|
ContentResolver.setSyncAutomatically(account, CONTRIBUTION_AUTHORITY, true); // Enable sync by default!
|
||||||
ContentResolver.setSyncAutomatically(account, ModificationsContentProvider.AUTHORITY, true); // Enable sync by default!
|
ContentResolver.setSyncAutomatically(account, MODIFICATIONS_AUTHORITY, true); // Enable sync by default!
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
private AccountManager accountManager() {
|
||||||
public static String accountType() {
|
return AccountManager.get(context);
|
||||||
return "fr.free.nrw.commons";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static AccountManager accountManager() {
|
|
||||||
return AccountManager.get(app());
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private static CommonsApplication app() {
|
|
||||||
return CommonsApplication.getInstance();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,84 +1,45 @@
|
||||||
package fr.free.nrw.commons.auth;
|
package fr.free.nrw.commons.auth;
|
||||||
|
|
||||||
import android.accounts.Account;
|
|
||||||
import android.accounts.AccountManager;
|
|
||||||
import android.accounts.AccountManagerFuture;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
||||||
import io.reactivex.Single;
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import static fr.free.nrw.commons.auth.AccountUtil.AUTH_COOKIE;
|
||||||
import io.reactivex.schedulers.Schedulers;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
public abstract class AuthenticatedActivity extends NavigationBaseActivity {
|
public abstract class AuthenticatedActivity extends NavigationBaseActivity {
|
||||||
|
|
||||||
private String accountType;
|
@Inject SessionManager sessionManager;
|
||||||
CommonsApplication app;
|
@Inject
|
||||||
|
MediaWikiApi mediaWikiApi;
|
||||||
private String authCookie;
|
private String authCookie;
|
||||||
|
|
||||||
public AuthenticatedActivity() {
|
|
||||||
this.accountType = AccountUtil.accountType();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void getAuthCookie(Account account, AccountManager accountManager) {
|
|
||||||
Single.fromCallable(() -> accountManager.blockingGetAuthToken(account, "", false))
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.doOnError(Timber::e)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(this::onAuthCookieAcquired, throwable -> onAuthFailure());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addAccount(AccountManager accountManager) {
|
|
||||||
Single.just(accountManager.addAccount(accountType, null, null, null, AuthenticatedActivity.this, null, null))
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.map(AccountManagerFuture::getResult)
|
|
||||||
.doOnEvent((bundle, throwable) -> {
|
|
||||||
if (!bundle.containsKey(AccountManager.KEY_ACCOUNT_NAME)) {
|
|
||||||
throw new RuntimeException("Bundle doesn't contain account-name key: "
|
|
||||||
+ AccountManager.KEY_ACCOUNT_NAME);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map(bundle -> bundle.getString(AccountManager.KEY_ACCOUNT_NAME))
|
|
||||||
.doOnError(Timber::e)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(s -> {
|
|
||||||
Account[] allAccounts = accountManager.getAccountsByType(accountType);
|
|
||||||
Account curAccount = allAccounts[0];
|
|
||||||
getAuthCookie(curAccount, accountManager);
|
|
||||||
},
|
|
||||||
throwable -> onAuthFailure());
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void requestAuthToken() {
|
protected void requestAuthToken() {
|
||||||
if (authCookie != null) {
|
if (authCookie != null) {
|
||||||
onAuthCookieAcquired(authCookie);
|
onAuthCookieAcquired(authCookie);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
AccountManager accountManager = AccountManager.get(this);
|
authCookie = sessionManager.getAuthCookie();
|
||||||
Account curAccount = app.getCurrentAccount();
|
if (authCookie != null) {
|
||||||
if (curAccount == null) {
|
onAuthCookieAcquired(authCookie);
|
||||||
addAccount(accountManager);
|
|
||||||
} else {
|
|
||||||
getAuthCookie(curAccount, accountManager);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
app = CommonsApplication.getInstance();
|
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
authCookie = savedInstanceState.getString("authCookie");
|
authCookie = savedInstanceState.getString(AUTH_COOKIE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onSaveInstanceState(Bundle outState) {
|
protected void onSaveInstanceState(Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
outState.putString("authCookie", authCookie);
|
outState.putString(AUTH_COOKIE, authCookie);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract void onAuthCookieAcquired(String authCookie);
|
protected abstract void onAuthCookieAcquired(String authCookie);
|
||||||
|
|
|
||||||
|
|
@ -1,115 +1,118 @@
|
||||||
package fr.free.nrw.commons.auth;
|
package fr.free.nrw.commons.auth;
|
||||||
|
|
||||||
|
import android.accounts.Account;
|
||||||
import android.accounts.AccountAuthenticatorActivity;
|
import android.accounts.AccountAuthenticatorActivity;
|
||||||
|
import android.accounts.AccountAuthenticatorResponse;
|
||||||
|
import android.accounts.AccountManager;
|
||||||
import android.app.ProgressDialog;
|
import android.app.ProgressDialog;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.ColorRes;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.StringRes;
|
||||||
|
import android.support.design.widget.TextInputLayout;
|
||||||
import android.support.v4.app.NavUtils;
|
import android.support.v4.app.NavUtils;
|
||||||
|
import android.support.v4.content.ContextCompat;
|
||||||
|
import android.support.v7.app.AppCompatDelegate;
|
||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
import android.text.TextWatcher;
|
import android.text.TextWatcher;
|
||||||
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
|
||||||
|
import butterknife.BindView;
|
||||||
|
import butterknife.ButterKnife;
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
import fr.free.nrw.commons.BuildConfig;
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.WelcomeActivity;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.PageTitle;
|
import fr.free.nrw.commons.PageTitle;
|
||||||
|
import fr.free.nrw.commons.R;
|
||||||
|
import fr.free.nrw.commons.Utils;
|
||||||
|
import fr.free.nrw.commons.WelcomeActivity;
|
||||||
import fr.free.nrw.commons.contributions.ContributionsActivity;
|
import fr.free.nrw.commons.contributions.ContributionsActivity;
|
||||||
|
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||||
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
|
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
||||||
|
import io.reactivex.Observable;
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.schedulers.Schedulers;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
import static android.view.KeyEvent.KEYCODE_ENTER;
|
import static android.view.KeyEvent.KEYCODE_ENTER;
|
||||||
|
import static android.view.View.VISIBLE;
|
||||||
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
|
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
|
||||||
|
import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE;
|
||||||
|
import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE;
|
||||||
|
|
||||||
public class LoginActivity extends AccountAuthenticatorActivity {
|
public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
|
|
||||||
public static final String PARAM_USERNAME = "fr.free.nrw.commons.login.username";
|
public static final String PARAM_USERNAME = "fr.free.nrw.commons.login.username";
|
||||||
|
|
||||||
private SharedPreferences prefs = null;
|
@Inject MediaWikiApi mwApi;
|
||||||
|
@Inject AccountUtil accountUtil;
|
||||||
|
@Inject SessionManager sessionManager;
|
||||||
|
@Inject @Named("application_preferences") SharedPreferences prefs;
|
||||||
|
@Inject @Named("default_preferences") SharedPreferences defaultPrefs;
|
||||||
|
|
||||||
private Button loginButton;
|
@BindView(R.id.loginButton) Button loginButton;
|
||||||
private EditText usernameEdit;
|
@BindView(R.id.signupButton) Button signupButton;
|
||||||
private EditText passwordEdit;
|
@BindView(R.id.loginUsername) EditText usernameEdit;
|
||||||
private EditText twoFactorEdit;
|
@BindView(R.id.loginPassword) EditText passwordEdit;
|
||||||
|
@BindView(R.id.loginTwoFactor) EditText twoFactorEdit;
|
||||||
|
@BindView(R.id.error_message_container) ViewGroup errorMessageContainer;
|
||||||
|
@BindView(R.id.error_message) TextView errorMessage;
|
||||||
|
@BindView(R.id.two_factor_container)TextInputLayout twoFactorContainer;
|
||||||
ProgressDialog progressDialog;
|
ProgressDialog progressDialog;
|
||||||
|
private AppCompatDelegate delegate;
|
||||||
private LoginTextWatcher textWatcher = new LoginTextWatcher();
|
private LoginTextWatcher textWatcher = new LoginTextWatcher();
|
||||||
|
|
||||||
private CommonsApplication app;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
setTheme(Utils.isDarkTheme(this) ? R.style.DarkAppTheme : R.style.LightAppTheme);
|
||||||
|
getDelegate().installViewFactory();
|
||||||
|
getDelegate().onCreate(savedInstanceState);
|
||||||
|
|
||||||
app = CommonsApplication.getInstance();
|
super.onCreate(savedInstanceState);
|
||||||
|
ApplicationlessInjection
|
||||||
|
.getInstance(this.getApplicationContext())
|
||||||
|
.getCommonsApplicationComponent()
|
||||||
|
.inject(this);
|
||||||
|
|
||||||
setContentView(R.layout.activity_login);
|
setContentView(R.layout.activity_login);
|
||||||
|
|
||||||
loginButton = (Button) findViewById(R.id.loginButton);
|
ButterKnife.bind(this);
|
||||||
Button signupButton = (Button) findViewById(R.id.signupButton);
|
|
||||||
usernameEdit = (EditText) findViewById(R.id.loginUsername);
|
|
||||||
passwordEdit = (EditText) findViewById(R.id.loginPassword);
|
|
||||||
twoFactorEdit = (EditText) findViewById(R.id.loginTwoFactor);
|
|
||||||
|
|
||||||
prefs = getSharedPreferences("fr.free.nrw.commons", MODE_PRIVATE);
|
|
||||||
|
|
||||||
usernameEdit.addTextChangedListener(textWatcher);
|
usernameEdit.addTextChangedListener(textWatcher);
|
||||||
passwordEdit.addTextChangedListener(textWatcher);
|
passwordEdit.addTextChangedListener(textWatcher);
|
||||||
twoFactorEdit.addTextChangedListener(textWatcher);
|
twoFactorEdit.addTextChangedListener(textWatcher);
|
||||||
passwordEdit.setOnEditorActionListener(newLoginInputActionListener());
|
passwordEdit.setOnEditorActionListener(newLoginInputActionListener());
|
||||||
|
|
||||||
loginButton.setOnClickListener(this::performLogin);
|
loginButton.setOnClickListener(view -> performLogin());
|
||||||
signupButton.setOnClickListener(this::signUp);
|
signupButton.setOnClickListener(view -> signUp());
|
||||||
}
|
|
||||||
|
|
||||||
private class LoginTextWatcher implements TextWatcher {
|
|
||||||
@Override
|
|
||||||
public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTextChanged(CharSequence charSequence, int start, int count, int after) {
|
protected void onPostCreate(Bundle savedInstanceState) {
|
||||||
|
super.onPostCreate(savedInstanceState);
|
||||||
|
getDelegate().onPostCreate(savedInstanceState);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterTextChanged(Editable editable) {
|
|
||||||
if (usernameEdit.getText().length() != 0 && passwordEdit.getText().length() != 0 &&
|
|
||||||
(BuildConfig.DEBUG || twoFactorEdit.getText().length() != 0 || twoFactorEdit.getVisibility() != View.VISIBLE)) {
|
|
||||||
loginButton.setEnabled(true);
|
|
||||||
} else {
|
|
||||||
loginButton.setEnabled(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private TextView.OnEditorActionListener newLoginInputActionListener() {
|
|
||||||
return (textView, actionId, keyEvent) -> {
|
|
||||||
if (loginButton.isEnabled()) {
|
|
||||||
if (actionId == IME_ACTION_DONE) {
|
|
||||||
performLogin(textView);
|
|
||||||
return true;
|
|
||||||
} else if ((keyEvent != null) && keyEvent.getKeyCode() == KEYCODE_ENTER) {
|
|
||||||
performLogin(textView);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
if (prefs.getBoolean("firstrun", true)) {
|
if (prefs.getBoolean("firstrun", true)) {
|
||||||
WelcomeActivity.startYourself(this);
|
WelcomeActivity.startYourself(this);
|
||||||
prefs.edit().putBoolean("firstrun", false).apply();
|
prefs.edit().putBoolean("firstrun", false).apply();
|
||||||
}
|
}
|
||||||
if (app.getCurrentAccount() != null) {
|
if (sessionManager.getCurrentAccount() != null) {
|
||||||
startMainActivity();
|
startMainActivity();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -127,22 +130,113 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
usernameEdit.removeTextChangedListener(textWatcher);
|
usernameEdit.removeTextChangedListener(textWatcher);
|
||||||
passwordEdit.removeTextChangedListener(textWatcher);
|
passwordEdit.removeTextChangedListener(textWatcher);
|
||||||
twoFactorEdit.removeTextChangedListener(textWatcher);
|
twoFactorEdit.removeTextChangedListener(textWatcher);
|
||||||
|
delegate.onDestroy();
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void performLogin(View view) {
|
private void performLogin() {
|
||||||
Timber.d("Login to start!");
|
Timber.d("Login to start!");
|
||||||
LoginTask task = getLoginTask();
|
final String username = canonicializeUsername(usernameEdit.getText().toString());
|
||||||
task.execute();
|
final String password = passwordEdit.getText().toString();
|
||||||
|
String twoFactorCode = twoFactorEdit.getText().toString();
|
||||||
|
|
||||||
|
showLoggingProgressBar();
|
||||||
|
Observable.fromCallable(() -> login(username, password, twoFactorCode))
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(result -> handleLogin(username, password, result));
|
||||||
}
|
}
|
||||||
|
|
||||||
private LoginTask getLoginTask() {
|
private String login(String username, String password, String twoFactorCode) {
|
||||||
return new LoginTask(
|
try {
|
||||||
this,
|
if (twoFactorCode.isEmpty()) {
|
||||||
canonicializeUsername(usernameEdit.getText().toString()),
|
return mwApi.login(username, password);
|
||||||
passwordEdit.getText().toString(),
|
} else {
|
||||||
twoFactorEdit.getText().toString()
|
return mwApi.login(username, password, twoFactorCode);
|
||||||
);
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
// Do something better!
|
||||||
|
return "NetworkFailure";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleLogin(String username, String password, String result) {
|
||||||
|
Timber.d("Login done!");
|
||||||
|
if (result.equals("PASS")) {
|
||||||
|
handlePassResult(username, password);
|
||||||
|
} else {
|
||||||
|
handleOtherResults(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showLoggingProgressBar() {
|
||||||
|
progressDialog = new ProgressDialog(this);
|
||||||
|
progressDialog.setIndeterminate(true);
|
||||||
|
progressDialog.setTitle(getString(R.string.logging_in_title));
|
||||||
|
progressDialog.setMessage(getString(R.string.logging_in_message));
|
||||||
|
progressDialog.setCanceledOnTouchOutside(false);
|
||||||
|
progressDialog.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handlePassResult(String username, String password) {
|
||||||
|
showSuccessAndDismissDialog();
|
||||||
|
requestAuthToken();
|
||||||
|
AccountAuthenticatorResponse response = null;
|
||||||
|
|
||||||
|
Bundle extras = getIntent().getExtras();
|
||||||
|
if (extras != null) {
|
||||||
|
Timber.d("Bundle of extras: %s", extras);
|
||||||
|
response = extras.getParcelable(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE);
|
||||||
|
if (response != null) {
|
||||||
|
Bundle authResult = new Bundle();
|
||||||
|
authResult.putString(AccountManager.KEY_ACCOUNT_NAME, username);
|
||||||
|
authResult.putString(AccountManager.KEY_ACCOUNT_TYPE, ACCOUNT_TYPE);
|
||||||
|
response.onResult(authResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
accountUtil.createAccount(response, username, password);
|
||||||
|
startMainActivity();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void requestAuthToken() {
|
||||||
|
AccountManager accountManager = AccountManager.get(this);
|
||||||
|
Account curAccount = sessionManager.getCurrentAccount();
|
||||||
|
if (curAccount != null) {
|
||||||
|
accountManager.setAuthToken(curAccount, AUTH_TOKEN_TYPE, mwApi.getAuthCookie());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Match known failure message codes and provide messages.
|
||||||
|
*
|
||||||
|
* @param result String
|
||||||
|
*/
|
||||||
|
private void handleOtherResults(String result) {
|
||||||
|
if (result.equals("NetworkFailure")) {
|
||||||
|
// Matches NetworkFailure which is created by the doInBackground method
|
||||||
|
showMessageAndCancelDialog(R.string.login_failed_network);
|
||||||
|
} else if (result.toLowerCase().contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) {
|
||||||
|
// Matches nosuchuser, nosuchusershort, noname
|
||||||
|
showMessageAndCancelDialog(R.string.login_failed_username);
|
||||||
|
emptySensitiveEditFields();
|
||||||
|
} else if (result.toLowerCase().contains("wrongpassword".toLowerCase())) {
|
||||||
|
// Matches wrongpassword, wrongpasswordempty
|
||||||
|
showMessageAndCancelDialog(R.string.login_failed_password);
|
||||||
|
emptySensitiveEditFields();
|
||||||
|
} else if (result.toLowerCase().contains("throttle".toLowerCase())) {
|
||||||
|
// Matches unknown throttle error codes
|
||||||
|
showMessageAndCancelDialog(R.string.login_failed_throttled);
|
||||||
|
} else if (result.toLowerCase().contains("userblocked".toLowerCase())) {
|
||||||
|
// Matches login-userblocked
|
||||||
|
showMessageAndCancelDialog(R.string.login_failed_blocked);
|
||||||
|
} else if (result.equals("2FA")) {
|
||||||
|
askUserForTwoFactorAuth();
|
||||||
|
} else {
|
||||||
|
// Occurs with unhandled login failure codes
|
||||||
|
Timber.d("Login failed with reason: %s", result);
|
||||||
|
showMessageAndCancelDialog(R.string.login_failed_generic);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -154,6 +248,29 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
return new PageTitle(username).getText();
|
return new PageTitle(username).getText();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
delegate.onStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
delegate.onStop();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostResume() {
|
||||||
|
super.onPostResume();
|
||||||
|
getDelegate().onPostResume();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setContentView(View view, ViewGroup.LayoutParams params) {
|
||||||
|
getDelegate().setContentView(view, params);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
|
|
@ -164,36 +281,26 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* Called when Sign Up button is clicked.
|
@NonNull
|
||||||
* @param view View
|
public MenuInflater getMenuInflater() {
|
||||||
*/
|
return getDelegate().getMenuInflater();
|
||||||
public void signUp(View view) {
|
|
||||||
Intent intent = new Intent(this, SignupActivity.class);
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void askUserForTwoFactorAuth() {
|
public void askUserForTwoFactorAuth() {
|
||||||
if (BuildConfig.DEBUG) {
|
progressDialog.dismiss();
|
||||||
twoFactorEdit.setVisibility(View.VISIBLE);
|
twoFactorContainer.setVisibility(VISIBLE);
|
||||||
showUserToastAndCancelDialog(R.string.login_failed_2fa_needed);
|
twoFactorEdit.setVisibility(VISIBLE);
|
||||||
} else {
|
showMessageAndCancelDialog(R.string.login_failed_2fa_needed);
|
||||||
showUserToastAndCancelDialog(R.string.login_failed_2fa_not_supported);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void showUserToastAndCancelDialog(int resId) {
|
public void showMessageAndCancelDialog(@StringRes int resId) {
|
||||||
showUserToast(resId);
|
showMessage(resId, R.color.secondaryDarkColor);
|
||||||
progressDialog.cancel();
|
progressDialog.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showUserToast(int resId) {
|
public void showSuccessAndDismissDialog() {
|
||||||
Toast.makeText(this, resId, Toast.LENGTH_LONG).show();
|
showMessage(R.string.login_success, R.color.primaryDarkColor);
|
||||||
}
|
|
||||||
|
|
||||||
public void showSuccessToastAndDismissDialog() {
|
|
||||||
Toast successToast = Toast.makeText(this, R.string.login_success, Toast.LENGTH_SHORT);
|
|
||||||
successToast.show();
|
|
||||||
progressDialog.dismiss();
|
progressDialog.dismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -203,8 +310,57 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void startMainActivity() {
|
public void startMainActivity() {
|
||||||
ContributionsActivity.startYourself(this);
|
NavigationBaseActivity.startActivityWithFlags(this, ContributionsActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void signUp() {
|
||||||
|
Intent intent = new Intent(this, SignupActivity.class);
|
||||||
|
startActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextView.OnEditorActionListener newLoginInputActionListener() {
|
||||||
|
return (textView, actionId, keyEvent) -> {
|
||||||
|
if (loginButton.isEnabled()) {
|
||||||
|
if (actionId == IME_ACTION_DONE) {
|
||||||
|
performLogin();
|
||||||
|
return true;
|
||||||
|
} else if ((keyEvent != null) && keyEvent.getKeyCode() == KEYCODE_ENTER) {
|
||||||
|
performLogin();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showMessage(@StringRes int resId, @ColorRes int colorResId) {
|
||||||
|
errorMessage.setText(getString(resId));
|
||||||
|
errorMessage.setTextColor(ContextCompat.getColor(this, colorResId));
|
||||||
|
errorMessageContainer.setVisibility(VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AppCompatDelegate getDelegate() {
|
||||||
|
if (delegate == null) {
|
||||||
|
delegate = AppCompatDelegate.create(this, null);
|
||||||
|
}
|
||||||
|
return delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LoginTextWatcher implements TextWatcher {
|
||||||
|
@Override
|
||||||
|
public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(CharSequence charSequence, int start, int count, int after) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable editable) {
|
||||||
|
boolean enabled = usernameEdit.getText().length() != 0 && passwordEdit.getText().length() != 0
|
||||||
|
&& (BuildConfig.DEBUG || twoFactorEdit.getText().length() != 0 || twoFactorEdit.getVisibility() != VISIBLE);
|
||||||
|
loginButton.setEnabled(enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,125 +0,0 @@
|
||||||
package fr.free.nrw.commons.auth;
|
|
||||||
|
|
||||||
import android.accounts.AccountAuthenticatorResponse;
|
|
||||||
import android.accounts.AccountManager;
|
|
||||||
import android.app.ProgressDialog;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.os.Bundle;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.mwapi.EventLog;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
class LoginTask extends AsyncTask<String, String, String> {
|
|
||||||
|
|
||||||
private LoginActivity loginActivity;
|
|
||||||
private String username;
|
|
||||||
private String password;
|
|
||||||
private String twoFactorCode = "";
|
|
||||||
private CommonsApplication app;
|
|
||||||
|
|
||||||
public LoginTask(LoginActivity loginActivity, String username, String password, String twoFactorCode) {
|
|
||||||
this.loginActivity = loginActivity;
|
|
||||||
this.username = username;
|
|
||||||
this.password = password;
|
|
||||||
this.twoFactorCode = twoFactorCode;
|
|
||||||
app = CommonsApplication.getInstance();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPreExecute() {
|
|
||||||
super.onPreExecute();
|
|
||||||
loginActivity.progressDialog = new ProgressDialog(loginActivity);
|
|
||||||
loginActivity.progressDialog.setIndeterminate(true);
|
|
||||||
loginActivity.progressDialog.setTitle(loginActivity.getString(R.string.logging_in_title));
|
|
||||||
loginActivity.progressDialog.setMessage(loginActivity.getString(R.string.logging_in_message));
|
|
||||||
loginActivity.progressDialog.setCanceledOnTouchOutside(false);
|
|
||||||
loginActivity.progressDialog.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String doInBackground(String... params) {
|
|
||||||
try {
|
|
||||||
if (twoFactorCode.isEmpty()) {
|
|
||||||
return app.getMWApi().login(username, password);
|
|
||||||
} else {
|
|
||||||
return app.getMWApi().login(username, password, twoFactorCode);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
// Do something better!
|
|
||||||
return "NetworkFailure";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(String result) {
|
|
||||||
super.onPostExecute(result);
|
|
||||||
Timber.d("Login done!");
|
|
||||||
|
|
||||||
EventLog.schema(CommonsApplication.EVENT_LOGIN_ATTEMPT)
|
|
||||||
.param("username", username)
|
|
||||||
.param("result", result)
|
|
||||||
.log();
|
|
||||||
|
|
||||||
if (result.equals("PASS")) {
|
|
||||||
handlePassResult();
|
|
||||||
} else {
|
|
||||||
handleOtherResults(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handlePassResult() {
|
|
||||||
loginActivity.showSuccessToastAndDismissDialog();
|
|
||||||
|
|
||||||
AccountAuthenticatorResponse response = null;
|
|
||||||
|
|
||||||
Bundle extras = loginActivity.getIntent().getExtras();
|
|
||||||
if (extras != null) {
|
|
||||||
Timber.d("Bundle of extras: %s", extras);
|
|
||||||
response = extras.getParcelable(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE);
|
|
||||||
if (response != null) {
|
|
||||||
Bundle authResult = new Bundle();
|
|
||||||
authResult.putString(AccountManager.KEY_ACCOUNT_NAME, username);
|
|
||||||
authResult.putString(AccountManager.KEY_ACCOUNT_TYPE, AccountUtil.accountType());
|
|
||||||
response.onResult(authResult);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AccountUtil.createAccount(response, username, password);
|
|
||||||
loginActivity.startMainActivity();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Match known failure message codes and provide messages.
|
|
||||||
* @param result String
|
|
||||||
*/
|
|
||||||
private void handleOtherResults(String result) {
|
|
||||||
if (result.equals("NetworkFailure")) {
|
|
||||||
// Matches NetworkFailure which is created by the doInBackground method
|
|
||||||
loginActivity.showUserToastAndCancelDialog(R.string.login_failed_network);
|
|
||||||
} else if (result.toLowerCase().contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) {
|
|
||||||
// Matches nosuchuser, nosuchusershort, noname
|
|
||||||
loginActivity.showUserToastAndCancelDialog(R.string.login_failed_username);
|
|
||||||
loginActivity.emptySensitiveEditFields();
|
|
||||||
} else if (result.toLowerCase().contains("wrongpassword".toLowerCase())) {
|
|
||||||
// Matches wrongpassword, wrongpasswordempty
|
|
||||||
loginActivity.showUserToastAndCancelDialog(R.string.login_failed_password);
|
|
||||||
loginActivity.emptySensitiveEditFields();
|
|
||||||
} else if (result.toLowerCase().contains("throttle".toLowerCase())) {
|
|
||||||
// Matches unknown throttle error codes
|
|
||||||
loginActivity.showUserToastAndCancelDialog(R.string.login_failed_throttled);
|
|
||||||
} else if (result.toLowerCase().contains("userblocked".toLowerCase())) {
|
|
||||||
// Matches login-userblocked
|
|
||||||
loginActivity.showUserToastAndCancelDialog(R.string.login_failed_blocked);
|
|
||||||
} else if (result.equals("2FA")) {
|
|
||||||
loginActivity.askUserForTwoFactorAuth();
|
|
||||||
} else {
|
|
||||||
// Occurs with unhandled login failure codes
|
|
||||||
Timber.d("Login failed with reason: %s", result);
|
|
||||||
loginActivity.showUserToastAndCancelDialog(R.string.login_failed_generic);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
package fr.free.nrw.commons.auth;
|
||||||
|
|
||||||
|
import android.accounts.Account;
|
||||||
|
import android.accounts.AccountManager;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
|
import io.reactivex.Completable;
|
||||||
|
import io.reactivex.Observable;
|
||||||
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manage the current logged in user session.
|
||||||
|
*/
|
||||||
|
public class SessionManager {
|
||||||
|
private final Context context;
|
||||||
|
private final MediaWikiApi mediaWikiApi;
|
||||||
|
private Account currentAccount; // Unlike a savings account... ;-)
|
||||||
|
private SharedPreferences sharedPreferences;
|
||||||
|
|
||||||
|
public SessionManager(Context context, MediaWikiApi mediaWikiApi, SharedPreferences sharedPreferences) {
|
||||||
|
this.context = context;
|
||||||
|
this.mediaWikiApi = mediaWikiApi;
|
||||||
|
this.currentAccount = null;
|
||||||
|
this.sharedPreferences = sharedPreferences;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Account|null
|
||||||
|
*/
|
||||||
|
public Account getCurrentAccount() {
|
||||||
|
if (currentAccount == null) {
|
||||||
|
AccountManager accountManager = AccountManager.get(context);
|
||||||
|
Account[] allAccounts = accountManager.getAccountsByType(ACCOUNT_TYPE);
|
||||||
|
if (allAccounts.length != 0) {
|
||||||
|
currentAccount = allAccounts[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return currentAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean revalidateAuthToken() {
|
||||||
|
AccountManager accountManager = AccountManager.get(context);
|
||||||
|
Account curAccount = getCurrentAccount();
|
||||||
|
|
||||||
|
if (curAccount == null) {
|
||||||
|
return false; // This should never happen
|
||||||
|
}
|
||||||
|
|
||||||
|
accountManager.invalidateAuthToken(ACCOUNT_TYPE, mediaWikiApi.getAuthCookie());
|
||||||
|
String authCookie = getAuthCookie();
|
||||||
|
|
||||||
|
if (authCookie == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
mediaWikiApi.setAuthCookie(authCookie);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAuthCookie() {
|
||||||
|
boolean isLoggedIn = sharedPreferences.getBoolean("isUserLoggedIn", false);
|
||||||
|
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
Timber.e("User is not logged in");
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
String authCookie = sharedPreferences.getString("getAuthCookie", null);
|
||||||
|
if (authCookie == null) {
|
||||||
|
Timber.e("Auth cookie is null even after login");
|
||||||
|
}
|
||||||
|
return authCookie;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Completable clearAllAccounts() {
|
||||||
|
AccountManager accountManager = AccountManager.get(context);
|
||||||
|
Account[] allAccounts = accountManager.getAccountsByType(ACCOUNT_TYPE);
|
||||||
|
return Completable.fromObservable(Observable.fromArray(allAccounts)
|
||||||
|
.map(a -> accountManager.removeAccount(a, null, null).getResult()))
|
||||||
|
.doOnComplete(() -> currentAccount = null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,6 @@ import android.webkit.WebViewClient;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
import fr.free.nrw.commons.BuildConfig;
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
import fr.free.nrw.commons.theme.BaseActivity;
|
import fr.free.nrw.commons.theme.BaseActivity;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
|
@ -39,11 +38,8 @@ public class SignupActivity extends BaseActivity {
|
||||||
//Signup success, so clear cookies, notify user, and load LoginActivity again
|
//Signup success, so clear cookies, notify user, and load LoginActivity again
|
||||||
Timber.d("Overriding URL %s", url);
|
Timber.d("Overriding URL %s", url);
|
||||||
|
|
||||||
Toast toast = Toast.makeText(
|
Toast toast = Toast.makeText(SignupActivity.this,
|
||||||
CommonsApplication.getInstance(),
|
"Account created!", Toast.LENGTH_LONG);
|
||||||
"Account created!",
|
|
||||||
Toast.LENGTH_LONG
|
|
||||||
);
|
|
||||||
toast.show();
|
toast.show();
|
||||||
// terminate on task completion.
|
// terminate on task completion.
|
||||||
finish();
|
finish();
|
||||||
|
|
|
||||||
|
|
@ -5,51 +5,37 @@ import android.accounts.Account;
|
||||||
import android.accounts.AccountAuthenticatorResponse;
|
import android.accounts.AccountAuthenticatorResponse;
|
||||||
import android.accounts.AccountManager;
|
import android.accounts.AccountManager;
|
||||||
import android.accounts.NetworkErrorException;
|
import android.accounts.NetworkErrorException;
|
||||||
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
import java.io.IOException;
|
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
|
||||||
|
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE;
|
||||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE;
|
||||||
|
|
||||||
import static android.accounts.AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION;
|
|
||||||
import static android.accounts.AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE;
|
|
||||||
import static android.accounts.AccountManager.KEY_ACCOUNT_NAME;
|
|
||||||
import static android.accounts.AccountManager.KEY_ACCOUNT_TYPE;
|
|
||||||
import static android.accounts.AccountManager.KEY_AUTHTOKEN;
|
|
||||||
import static android.accounts.AccountManager.KEY_BOOLEAN_RESULT;
|
|
||||||
import static android.accounts.AccountManager.KEY_ERROR_CODE;
|
|
||||||
import static android.accounts.AccountManager.KEY_ERROR_MESSAGE;
|
|
||||||
import static android.accounts.AccountManager.KEY_INTENT;
|
|
||||||
import static fr.free.nrw.commons.auth.LoginActivity.PARAM_USERNAME;
|
|
||||||
|
|
||||||
public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
|
public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
|
||||||
|
private static final String[] SYNC_AUTHORITIES = {ContributionsContentProvider.CONTRIBUTION_AUTHORITY, ModificationsContentProvider.MODIFICATIONS_AUTHORITY};
|
||||||
|
|
||||||
private Context context;
|
@NonNull
|
||||||
|
private final Context context;
|
||||||
|
|
||||||
WikiAccountAuthenticator(Context context) {
|
public WikiAccountAuthenticator(@NonNull Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
this.context = context;
|
this.context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Bundle unsupportedOperation() {
|
@Override
|
||||||
|
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
|
||||||
Bundle bundle = new Bundle();
|
Bundle bundle = new Bundle();
|
||||||
bundle.putInt(KEY_ERROR_CODE, ERROR_CODE_UNSUPPORTED_OPERATION);
|
bundle.putString("test", "editProperties");
|
||||||
|
|
||||||
// HACK: the docs indicate that this is a required key bit it's not displayed to the user.
|
|
||||||
bundle.putString(KEY_ERROR_MESSAGE, "");
|
|
||||||
|
|
||||||
return bundle;
|
return bundle;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean supportedAccountType(@Nullable String type) {
|
|
||||||
return AccountUtil.accountType().equals(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Bundle addAccount(@NonNull AccountAuthenticatorResponse response,
|
public Bundle addAccount(@NonNull AccountAuthenticatorResponse response,
|
||||||
@NonNull String accountType, @Nullable String authTokenType,
|
@NonNull String accountType, @Nullable String authTokenType,
|
||||||
|
|
@ -57,87 +43,48 @@ public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
|
||||||
throws NetworkErrorException {
|
throws NetworkErrorException {
|
||||||
|
|
||||||
if (!supportedAccountType(accountType)) {
|
if (!supportedAccountType(accountType)) {
|
||||||
return unsupportedOperation();
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putString("test", "addAccount");
|
||||||
|
return bundle;
|
||||||
}
|
}
|
||||||
|
|
||||||
return addAccount(response);
|
return addAccount(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Bundle addAccount(AccountAuthenticatorResponse response) {
|
|
||||||
Intent Intent = new Intent(context, LoginActivity.class);
|
|
||||||
Intent.putExtra(KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
|
|
||||||
|
|
||||||
Bundle bundle = new Bundle();
|
|
||||||
bundle.putParcelable(KEY_INTENT, Intent);
|
|
||||||
|
|
||||||
return bundle;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Bundle confirmCredentials(@NonNull AccountAuthenticatorResponse response,
|
public Bundle confirmCredentials(@NonNull AccountAuthenticatorResponse response,
|
||||||
@NonNull Account account, @Nullable Bundle options)
|
@NonNull Account account, @Nullable Bundle options)
|
||||||
throws NetworkErrorException {
|
throws NetworkErrorException {
|
||||||
return unsupportedOperation();
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putString("test", "confirmCredentials");
|
||||||
|
return bundle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
|
public Bundle getAuthToken(@NonNull AccountAuthenticatorResponse response,
|
||||||
return unsupportedOperation();
|
@NonNull Account account, @NonNull String authTokenType,
|
||||||
}
|
@Nullable Bundle options)
|
||||||
|
throws NetworkErrorException {
|
||||||
private String getAuthCookie(String username, String password) throws IOException {
|
Bundle bundle = new Bundle();
|
||||||
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
|
bundle.putString("test", "getAuthToken");
|
||||||
//TODO add 2fa support here
|
|
||||||
String result = api.login(username, password);
|
|
||||||
if (result.equals("PASS")) {
|
|
||||||
return api.getAuthCookie();
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account,
|
|
||||||
String authTokenType, Bundle options) throws NetworkErrorException {
|
|
||||||
// Extract the username and password from the Account Manager, and ask
|
|
||||||
// the server for an appropriate AuthToken.
|
|
||||||
final AccountManager am = AccountManager.get(context);
|
|
||||||
final String password = am.getPassword(account);
|
|
||||||
if (password != null) {
|
|
||||||
String authCookie;
|
|
||||||
try {
|
|
||||||
authCookie = getAuthCookie(account.name, password);
|
|
||||||
} catch (IOException e) {
|
|
||||||
// Network error!
|
|
||||||
e.printStackTrace();
|
|
||||||
throw new NetworkErrorException(e);
|
|
||||||
}
|
|
||||||
if (authCookie != null) {
|
|
||||||
final Bundle result = new Bundle();
|
|
||||||
result.putString(KEY_ACCOUNT_NAME, account.name);
|
|
||||||
result.putString(KEY_ACCOUNT_TYPE, AccountUtil.accountType());
|
|
||||||
result.putString(KEY_AUTHTOKEN, authCookie);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we get here, then we couldn't access the user's password - so we
|
|
||||||
// need to re-prompt them for their credentials. We do that by creating
|
|
||||||
// an intent to display our AuthenticatorActivity panel.
|
|
||||||
final Intent intent = new Intent(context, LoginActivity.class);
|
|
||||||
intent.putExtra(PARAM_USERNAME, account.name);
|
|
||||||
intent.putExtra(KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
|
|
||||||
final Bundle bundle = new Bundle();
|
|
||||||
bundle.putParcelable(KEY_INTENT, intent);
|
|
||||||
return bundle;
|
return bundle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public String getAuthTokenLabel(@NonNull String authTokenType) {
|
public String getAuthTokenLabel(@NonNull String authTokenType) {
|
||||||
//Note: the wikipedia app actually returns a string here....
|
return supportedAccountType(authTokenType) ? AUTH_TOKEN_TYPE : null;
|
||||||
//return supportedAccountType(authTokenType) ? context.getString(R.string.wikimedia) : null;
|
}
|
||||||
return null;
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public Bundle updateCredentials(@NonNull AccountAuthenticatorResponse response,
|
||||||
|
@NonNull Account account, @Nullable String authTokenType,
|
||||||
|
@Nullable Bundle options)
|
||||||
|
throws NetworkErrorException {
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putString("test", "updateCredentials");
|
||||||
|
return bundle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
|
@ -146,16 +93,50 @@ public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
|
||||||
@NonNull Account account, @NonNull String[] features)
|
@NonNull Account account, @NonNull String[] features)
|
||||||
throws NetworkErrorException {
|
throws NetworkErrorException {
|
||||||
Bundle bundle = new Bundle();
|
Bundle bundle = new Bundle();
|
||||||
bundle.putBoolean(KEY_BOOLEAN_RESULT, false);
|
bundle.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
|
||||||
return bundle;
|
return bundle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
private boolean supportedAccountType(@Nullable String type) {
|
||||||
@Override
|
return ACCOUNT_TYPE.equals(type);
|
||||||
public Bundle updateCredentials(@NonNull AccountAuthenticatorResponse response,
|
|
||||||
@NonNull Account account, @Nullable String authTokenType,
|
|
||||||
@Nullable Bundle options) throws NetworkErrorException {
|
|
||||||
return unsupportedOperation();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Bundle addAccount(AccountAuthenticatorResponse response) {
|
||||||
|
Intent intent = new Intent(context, LoginActivity.class);
|
||||||
|
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
|
||||||
|
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
|
||||||
|
|
||||||
|
return bundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Bundle unsupportedOperation() {
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putInt(AccountManager.KEY_ERROR_CODE, AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION);
|
||||||
|
|
||||||
|
// HACK: the docs indicate that this is a required key bit it's not displayed to the user.
|
||||||
|
bundle.putString(AccountManager.KEY_ERROR_MESSAGE, "");
|
||||||
|
|
||||||
|
return bundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Bundle getAccountRemovalAllowed(AccountAuthenticatorResponse response,
|
||||||
|
Account account) throws NetworkErrorException {
|
||||||
|
Bundle result = super.getAccountRemovalAllowed(response, account);
|
||||||
|
|
||||||
|
if (result.containsKey(AccountManager.KEY_BOOLEAN_RESULT)
|
||||||
|
&& !result.containsKey(AccountManager.KEY_INTENT)) {
|
||||||
|
boolean allowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT);
|
||||||
|
|
||||||
|
if (allowed) {
|
||||||
|
for (String auth : SYNC_AUTHORITIES) {
|
||||||
|
ContentResolver.cancelSync(account, auth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,26 @@
|
||||||
package fr.free.nrw.commons.auth;
|
package fr.free.nrw.commons.auth;
|
||||||
|
|
||||||
import android.accounts.AccountManager;
|
import android.accounts.AbstractAccountAuthenticator;
|
||||||
import android.app.Service;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
public class WikiAccountAuthenticatorService extends Service {
|
import fr.free.nrw.commons.di.CommonsDaggerService;
|
||||||
|
|
||||||
private static WikiAccountAuthenticator wikiAccountAuthenticator = null;
|
public class WikiAccountAuthenticatorService extends CommonsDaggerService {
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private AbstractAccountAuthenticator authenticator;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
authenticator = new WikiAccountAuthenticator(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public IBinder onBind(Intent intent) {
|
public IBinder onBind(Intent intent) {
|
||||||
if (!intent.getAction().equals(AccountManager.ACTION_AUTHENTICATOR_INTENT)) {
|
return authenticator == null ? null : authenticator.getIBinder();
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wikiAccountAuthenticator == null) {
|
|
||||||
wikiAccountAuthenticator = new WikiAccountAuthenticator(this);
|
|
||||||
}
|
|
||||||
return wikiAccountAuthenticator.getIBinder();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
package fr.free.nrw.commons.category;
|
package fr.free.nrw.commons.category;
|
||||||
|
|
||||||
import android.content.ContentProviderClient;
|
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.support.v4.app.Fragment;
|
|
||||||
import android.support.v7.app.AlertDialog;
|
import android.support.v7.app.AlertDialog;
|
||||||
import android.support.v7.widget.LinearLayoutManager;
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
|
@ -31,11 +28,14 @@ import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
|
||||||
import butterknife.BindView;
|
import butterknife.BindView;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.data.Category;
|
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||||
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
import fr.free.nrw.commons.upload.MwVolleyApi;
|
import fr.free.nrw.commons.upload.MwVolleyApi;
|
||||||
import fr.free.nrw.commons.utils.StringSortingUtils;
|
import fr.free.nrw.commons.utils.StringSortingUtils;
|
||||||
import io.reactivex.Observable;
|
import io.reactivex.Observable;
|
||||||
|
|
@ -45,12 +45,11 @@ import timber.log.Timber;
|
||||||
|
|
||||||
import static android.view.KeyEvent.ACTION_UP;
|
import static android.view.KeyEvent.ACTION_UP;
|
||||||
import static android.view.KeyEvent.KEYCODE_BACK;
|
import static android.view.KeyEvent.KEYCODE_BACK;
|
||||||
import static fr.free.nrw.commons.category.CategoryContentProvider.AUTHORITY;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays the category suggestion and selection screen. Category search is initiated here.
|
* Displays the category suggestion and selection screen. Category search is initiated here.
|
||||||
*/
|
*/
|
||||||
public class CategorizationFragment extends Fragment {
|
public class CategorizationFragment extends CommonsDaggerSupportFragment {
|
||||||
|
|
||||||
public static final int SEARCH_CATS_LIMIT = 25;
|
public static final int SEARCH_CATS_LIMIT = 25;
|
||||||
|
|
||||||
|
|
@ -65,16 +64,19 @@ public class CategorizationFragment extends Fragment {
|
||||||
@BindView(R.id.categoriesExplanation)
|
@BindView(R.id.categoriesExplanation)
|
||||||
TextView categoriesSkip;
|
TextView categoriesSkip;
|
||||||
|
|
||||||
|
@Inject MediaWikiApi mwApi;
|
||||||
|
@Inject @Named("default_preferences") SharedPreferences prefs;
|
||||||
|
@Inject CategoryDao categoryDao;
|
||||||
|
|
||||||
private RVRendererAdapter<CategoryItem> categoriesAdapter;
|
private RVRendererAdapter<CategoryItem> categoriesAdapter;
|
||||||
private OnCategoriesSaveHandler onCategoriesSaveHandler;
|
private OnCategoriesSaveHandler onCategoriesSaveHandler;
|
||||||
private HashMap<String, ArrayList<String>> categoriesCache;
|
private HashMap<String, ArrayList<String>> categoriesCache;
|
||||||
private List<CategoryItem> selectedCategories = new ArrayList<>();
|
private List<CategoryItem> selectedCategories = new ArrayList<>();
|
||||||
private ContentProviderClient databaseClient;
|
|
||||||
|
|
||||||
private final CategoriesAdapterFactory adapterFactory = new CategoriesAdapterFactory(item -> {
|
private final CategoriesAdapterFactory adapterFactory = new CategoriesAdapterFactory(item -> {
|
||||||
if (item.isSelected()) {
|
if (item.isSelected()) {
|
||||||
selectedCategories.add(item);
|
selectedCategories.add(item);
|
||||||
updateCategoryCount(item, databaseClient);
|
updateCategoryCount(item);
|
||||||
} else {
|
} else {
|
||||||
selectedCategories.remove(item);
|
selectedCategories.remove(item);
|
||||||
}
|
}
|
||||||
|
|
@ -88,13 +90,6 @@ public class CategorizationFragment extends Fragment {
|
||||||
|
|
||||||
categoriesList.setLayoutManager(new LinearLayoutManager(getContext()));
|
categoriesList.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||||
|
|
||||||
RxView.clicks(categoriesSkip)
|
|
||||||
.takeUntil(RxView.detaches(categoriesSkip))
|
|
||||||
.subscribe(o -> {
|
|
||||||
getActivity().onBackPressed();
|
|
||||||
getActivity().finish();
|
|
||||||
});
|
|
||||||
|
|
||||||
ArrayList<CategoryItem> items = new ArrayList<>();
|
ArrayList<CategoryItem> items = new ArrayList<>();
|
||||||
categoriesCache = new HashMap<>();
|
categoriesCache = new HashMap<>();
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
|
|
@ -139,12 +134,6 @@ public class CategorizationFragment extends Fragment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
databaseClient.release();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(Bundle outState) {
|
public void onSaveInstanceState(Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
|
|
@ -180,7 +169,6 @@ public class CategorizationFragment extends Fragment {
|
||||||
setHasOptionsMenu(true);
|
setHasOptionsMenu(true);
|
||||||
onCategoriesSaveHandler = (OnCategoriesSaveHandler) getActivity();
|
onCategoriesSaveHandler = (OnCategoriesSaveHandler) getActivity();
|
||||||
getActivity().setTitle(R.string.categories_activity_title);
|
getActivity().setTitle(R.string.categories_activity_title);
|
||||||
databaseClient = getActivity().getContentResolver().acquireContentProviderClient(AUTHORITY);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateCategoryList(String filter) {
|
private void updateCategoryList(String filter) {
|
||||||
|
|
@ -205,7 +193,9 @@ public class CategorizationFragment extends Fragment {
|
||||||
.sorted(sortBySimilarity(filter))
|
.sorted(sortBySimilarity(filter))
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
s -> categoriesAdapter.add(s), Timber::e, () -> {
|
s -> categoriesAdapter.add(s),
|
||||||
|
Timber::e,
|
||||||
|
() -> {
|
||||||
categoriesAdapter.notifyDataSetChanged();
|
categoriesAdapter.notifyDataSetChanged();
|
||||||
categoriesSearchInProgress.setVisibility(View.GONE);
|
categoriesSearchInProgress.setVisibility(View.GONE);
|
||||||
|
|
||||||
|
|
@ -253,16 +243,15 @@ public class CategorizationFragment extends Fragment {
|
||||||
|
|
||||||
private Observable<CategoryItem> titleCategories() {
|
private Observable<CategoryItem> titleCategories() {
|
||||||
//Retrieve the title that was saved when user tapped submit icon
|
//Retrieve the title that was saved when user tapped submit icon
|
||||||
SharedPreferences titleDesc = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
String title = prefs.getString("Title", "");
|
||||||
String title = titleDesc.getString("Title", "");
|
|
||||||
|
|
||||||
return CommonsApplication.getInstance().getMWApi()
|
return mwApi
|
||||||
.searchTitles(title, SEARCH_CATS_LIMIT)
|
.searchTitles(title, SEARCH_CATS_LIMIT)
|
||||||
.map(name -> new CategoryItem(name, false));
|
.map(name -> new CategoryItem(name, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Observable<CategoryItem> recentCategories() {
|
private Observable<CategoryItem> recentCategories() {
|
||||||
return Observable.fromIterable(Category.recentCategories(databaseClient, SEARCH_CATS_LIMIT))
|
return Observable.fromIterable(categoryDao.recentCategories(SEARCH_CATS_LIMIT))
|
||||||
.map(s -> new CategoryItem(s, false));
|
.map(s -> new CategoryItem(s, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -279,7 +268,7 @@ public class CategorizationFragment extends Fragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
//otherwise, search API for matching categories
|
//otherwise, search API for matching categories
|
||||||
return CommonsApplication.getInstance().getMWApi()
|
return mwApi
|
||||||
.allCategories(term, SEARCH_CATS_LIMIT)
|
.allCategories(term, SEARCH_CATS_LIMIT)
|
||||||
.map(name -> new CategoryItem(name, false));
|
.map(name -> new CategoryItem(name, false));
|
||||||
}
|
}
|
||||||
|
|
@ -290,7 +279,7 @@ public class CategorizationFragment extends Fragment {
|
||||||
return Observable.empty();
|
return Observable.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
return CommonsApplication.getInstance().getMWApi()
|
return mwApi
|
||||||
.searchCategories(term, SEARCH_CATS_LIMIT)
|
.searchCategories(term, SEARCH_CATS_LIMIT)
|
||||||
.map(s -> new CategoryItem(s, false));
|
.map(s -> new CategoryItem(s, false));
|
||||||
}
|
}
|
||||||
|
|
@ -308,28 +297,22 @@ public class CategorizationFragment extends Fragment {
|
||||||
//Check if item contains a 4-digit word anywhere within the string (.* is wildcard)
|
//Check if item contains a 4-digit word anywhere within the string (.* is wildcard)
|
||||||
//And that item does not equal the current year or previous year
|
//And that item does not equal the current year or previous year
|
||||||
//And if it is an irrelevant category such as Media_needing_categories_as_of_16_June_2017(Issue #750)
|
//And if it is an irrelevant category such as Media_needing_categories_as_of_16_June_2017(Issue #750)
|
||||||
|
//Check if the year in the form of XX(X)0s is relevant, i.e. in the 2000s or 2010s as stated in Issue #1029
|
||||||
return ((item.matches(".*(19|20)\\d{2}.*") && !item.contains(yearInString) && !item.contains(prevYearInString))
|
return ((item.matches(".*(19|20)\\d{2}.*") && !item.contains(yearInString) && !item.contains(prevYearInString))
|
||||||
|| item.matches("(.*)needing(.*)") || item.matches("(.*)taken on(.*)"));
|
|| item.matches("(.*)needing(.*)") || item.matches("(.*)taken on(.*)")
|
||||||
|
|| (item.matches(".*0s.*") && !item.matches(".*(200|201)0s.*")));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateCategoryCount(CategoryItem item, ContentProviderClient client) {
|
private void updateCategoryCount(CategoryItem item) {
|
||||||
Category cat = lookupCategory(item.getName());
|
Category category = categoryDao.find(item.getName());
|
||||||
cat.incTimesUsed();
|
|
||||||
cat.save(client);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Category lookupCategory(String name) {
|
|
||||||
Category cat = Category.find(databaseClient, name);
|
|
||||||
|
|
||||||
if (cat == null) {
|
|
||||||
// Newly used category...
|
// Newly used category...
|
||||||
cat = new Category();
|
if (category == null) {
|
||||||
cat.setName(name);
|
category = new Category(null, item.getName(), new Date(), 0);
|
||||||
cat.setLastUsed(new Date());
|
|
||||||
cat.setTimesUsed(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return cat;
|
category.incTimesUsed();
|
||||||
|
categoryDao.save(category);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getCurrentSelectedCount() {
|
public int getCurrentSelectedCount() {
|
||||||
|
|
|
||||||
96
app/src/main/java/fr/free/nrw/commons/category/Category.java
Normal file
96
app/src/main/java/fr/free/nrw/commons/category/Category.java
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
package fr.free.nrw.commons.category;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a category
|
||||||
|
*/
|
||||||
|
public class Category {
|
||||||
|
private Uri contentUri;
|
||||||
|
private String name;
|
||||||
|
private Date lastUsed;
|
||||||
|
private int timesUsed;
|
||||||
|
|
||||||
|
public Category() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public Category(Uri contentUri, String name, Date lastUsed, int timesUsed) {
|
||||||
|
this.contentUri = contentUri;
|
||||||
|
this.name = name;
|
||||||
|
this.lastUsed = lastUsed;
|
||||||
|
this.timesUsed = timesUsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets name
|
||||||
|
*
|
||||||
|
* @return name
|
||||||
|
*/
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifies name
|
||||||
|
*
|
||||||
|
* @param name Category name
|
||||||
|
*/
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets last used date
|
||||||
|
*
|
||||||
|
* @return Last used date
|
||||||
|
*/
|
||||||
|
public Date getLastUsed() {
|
||||||
|
// warning: Date objects are mutable.
|
||||||
|
return (Date)lastUsed.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates new last used date
|
||||||
|
*/
|
||||||
|
private void touch() {
|
||||||
|
lastUsed = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets no. of times the category is used
|
||||||
|
*
|
||||||
|
* @return no. of times used
|
||||||
|
*/
|
||||||
|
public int getTimesUsed() {
|
||||||
|
return timesUsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments timesUsed by 1 and sets last used date as now.
|
||||||
|
*/
|
||||||
|
public void incTimesUsed() {
|
||||||
|
timesUsed++;
|
||||||
|
touch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the content URI for this category
|
||||||
|
*
|
||||||
|
* @return content URI
|
||||||
|
*/
|
||||||
|
public Uri getContentUri() {
|
||||||
|
return contentUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifies the content URI - marking this category as already saved in the database
|
||||||
|
*
|
||||||
|
* @param contentUri the content URI
|
||||||
|
*/
|
||||||
|
public void setContentUri(Uri contentUri) {
|
||||||
|
this.contentUri = contentUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package fr.free.nrw.commons.category;
|
package fr.free.nrw.commons.category;
|
||||||
|
|
||||||
import android.content.ContentProvider;
|
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.content.UriMatcher;
|
import android.content.UriMatcher;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
|
|
@ -10,17 +9,18 @@ import android.net.Uri;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
import javax.inject.Inject;
|
||||||
import fr.free.nrw.commons.data.Category;
|
|
||||||
import fr.free.nrw.commons.data.DBOpenHelper;
|
import fr.free.nrw.commons.data.DBOpenHelper;
|
||||||
|
import fr.free.nrw.commons.di.CommonsDaggerContentProvider;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
import static android.content.UriMatcher.NO_MATCH;
|
import static android.content.UriMatcher.NO_MATCH;
|
||||||
import static fr.free.nrw.commons.data.Category.Table.ALL_FIELDS;
|
import static fr.free.nrw.commons.category.CategoryDao.Table.ALL_FIELDS;
|
||||||
import static fr.free.nrw.commons.data.Category.Table.COLUMN_ID;
|
import static fr.free.nrw.commons.category.CategoryDao.Table.COLUMN_ID;
|
||||||
import static fr.free.nrw.commons.data.Category.Table.TABLE_NAME;
|
import static fr.free.nrw.commons.category.CategoryDao.Table.TABLE_NAME;
|
||||||
|
|
||||||
public class CategoryContentProvider extends ContentProvider {
|
public class CategoryContentProvider extends CommonsDaggerContentProvider {
|
||||||
|
|
||||||
public static final String AUTHORITY = "fr.free.nrw.commons.categories.contentprovider";
|
public static final String AUTHORITY = "fr.free.nrw.commons.categories.contentprovider";
|
||||||
// For URI matcher
|
// For URI matcher
|
||||||
|
|
@ -37,19 +37,11 @@ public class CategoryContentProvider extends ContentProvider {
|
||||||
uriMatcher.addURI(AUTHORITY, BASE_PATH + "/#", CATEGORIES_ID);
|
uriMatcher.addURI(AUTHORITY, BASE_PATH + "/#", CATEGORIES_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
private DBOpenHelper dbOpenHelper;
|
|
||||||
|
|
||||||
public static Uri uriForId(int id) {
|
public static Uri uriForId(int id) {
|
||||||
return Uri.parse(BASE_URI.toString() + "/" + id);
|
return Uri.parse(BASE_URI.toString() + "/" + id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
@Inject DBOpenHelper dbOpenHelper;
|
||||||
@Override
|
|
||||||
public boolean onCreate() {
|
|
||||||
CommonsApplication app = ((CommonsApplication) getContext().getApplicationContext());
|
|
||||||
dbOpenHelper = app.getDBOpenHelper();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
@SuppressWarnings("ConstantConditions")
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -1,115 +1,64 @@
|
||||||
package fr.free.nrw.commons.data;
|
package fr.free.nrw.commons.category;
|
||||||
|
|
||||||
import android.content.ContentProviderClient;
|
import android.content.ContentProviderClient;
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import fr.free.nrw.commons.category.CategoryContentProvider;
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
import javax.inject.Provider;
|
||||||
|
|
||||||
public class Category {
|
public class CategoryDao {
|
||||||
private Uri contentUri;
|
|
||||||
|
|
||||||
private String name;
|
private final Provider<ContentProviderClient> clientProvider;
|
||||||
private Date lastUsed;
|
|
||||||
private int timesUsed;
|
|
||||||
|
|
||||||
// Getters/setters
|
@Inject
|
||||||
public String getName() {
|
public CategoryDao(@Named("category") Provider<ContentProviderClient> clientProvider) {
|
||||||
return name;
|
this.clientProvider = clientProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setName(String name) {
|
public void save(Category category) {
|
||||||
this.name = name;
|
ContentProviderClient db = clientProvider.get();
|
||||||
}
|
|
||||||
|
|
||||||
private Date getLastUsed() {
|
|
||||||
// warning: Date objects are mutable.
|
|
||||||
return (Date)lastUsed.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setLastUsed(Date lastUsed) {
|
|
||||||
// warning: Date objects are mutable.
|
|
||||||
this.lastUsed = (Date)lastUsed.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void touch() {
|
|
||||||
lastUsed = new Date();
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getTimesUsed() {
|
|
||||||
return timesUsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTimesUsed(int timesUsed) {
|
|
||||||
this.timesUsed = timesUsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void incTimesUsed() {
|
|
||||||
timesUsed++;
|
|
||||||
touch();
|
|
||||||
}
|
|
||||||
|
|
||||||
//region Database/content-provider stuff
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Persist category.
|
|
||||||
* @param client ContentProviderClient to handle DB connection
|
|
||||||
*/
|
|
||||||
public void save(ContentProviderClient client) {
|
|
||||||
try {
|
try {
|
||||||
if (contentUri == null) {
|
if (category.getContentUri() == null) {
|
||||||
contentUri = client.insert(CategoryContentProvider.BASE_URI, this.toContentValues());
|
category.setContentUri(db.insert(CategoryContentProvider.BASE_URI, toContentValues(category)));
|
||||||
} else {
|
} else {
|
||||||
client.update(contentUri, toContentValues(), null, null);
|
db.update(category.getContentUri(), toContentValues(category), null, null);
|
||||||
}
|
}
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
|
} finally {
|
||||||
|
db.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ContentValues toContentValues() {
|
|
||||||
ContentValues cv = new ContentValues();
|
|
||||||
cv.put(Table.COLUMN_NAME, getName());
|
|
||||||
cv.put(Table.COLUMN_LAST_USED, getLastUsed().getTime());
|
|
||||||
cv.put(Table.COLUMN_TIMES_USED, getTimesUsed());
|
|
||||||
return cv;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Category fromCursor(Cursor cursor) {
|
|
||||||
// Hardcoding column positions!
|
|
||||||
Category c = new Category();
|
|
||||||
c.contentUri = CategoryContentProvider.uriForId(cursor.getInt(0));
|
|
||||||
c.name = cursor.getString(1);
|
|
||||||
c.lastUsed = new Date(cursor.getLong(2));
|
|
||||||
c.timesUsed = cursor.getInt(3);
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find persisted category in database, based on its name.
|
* Find persisted category in database, based on its name.
|
||||||
* @param client ContentProviderClient to handle DB connection
|
*
|
||||||
* @param name Category's name
|
* @param name Category's name
|
||||||
* @return category from database, or null if not found
|
* @return category from database, or null if not found
|
||||||
*/
|
*/
|
||||||
public static @Nullable Category find(ContentProviderClient client, String name) {
|
@Nullable
|
||||||
|
Category find(String name) {
|
||||||
Cursor cursor = null;
|
Cursor cursor = null;
|
||||||
|
ContentProviderClient db = clientProvider.get();
|
||||||
try {
|
try {
|
||||||
cursor = client.query(
|
cursor = db.query(
|
||||||
CategoryContentProvider.BASE_URI,
|
CategoryContentProvider.BASE_URI,
|
||||||
Category.Table.ALL_FIELDS,
|
Table.ALL_FIELDS,
|
||||||
Category.Table.COLUMN_NAME + "=?",
|
Table.COLUMN_NAME + "=?",
|
||||||
new String[]{name},
|
new String[]{name},
|
||||||
null);
|
null);
|
||||||
if (cursor != null && cursor.moveToFirst()) {
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
return Category.fromCursor(cursor);
|
return fromCursor(cursor);
|
||||||
}
|
}
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
// This feels lazy, but to hell with checked exceptions. :)
|
// This feels lazy, but to hell with checked exceptions. :)
|
||||||
|
|
@ -118,29 +67,32 @@ public class Category {
|
||||||
if (cursor != null) {
|
if (cursor != null) {
|
||||||
cursor.close();
|
cursor.close();
|
||||||
}
|
}
|
||||||
|
db.release();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve recently-used categories, ordered by descending date.
|
* Retrieve recently-used categories, ordered by descending date.
|
||||||
|
*
|
||||||
* @return a list containing recent categories
|
* @return a list containing recent categories
|
||||||
*/
|
*/
|
||||||
public static @NonNull ArrayList<String> recentCategories(ContentProviderClient client, int limit) {
|
@NonNull
|
||||||
ArrayList<String> items = new ArrayList<>();
|
List<String> recentCategories(int limit) {
|
||||||
|
List<String> items = new ArrayList<>();
|
||||||
Cursor cursor = null;
|
Cursor cursor = null;
|
||||||
|
ContentProviderClient db = clientProvider.get();
|
||||||
try {
|
try {
|
||||||
cursor = client.query(
|
cursor = db.query(
|
||||||
CategoryContentProvider.BASE_URI,
|
CategoryContentProvider.BASE_URI,
|
||||||
Category.Table.ALL_FIELDS,
|
Table.ALL_FIELDS,
|
||||||
null,
|
null,
|
||||||
new String[]{},
|
new String[]{},
|
||||||
Category.Table.COLUMN_LAST_USED + " DESC");
|
Table.COLUMN_LAST_USED + " DESC");
|
||||||
// fixme add a limit on the original query instead of falling out of the loop?
|
// fixme add a limit on the original query instead of falling out of the loop?
|
||||||
while (cursor != null && cursor.moveToNext()
|
while (cursor != null && cursor.moveToNext()
|
||||||
&& cursor.getPosition() < limit) {
|
&& cursor.getPosition() < limit) {
|
||||||
Category cat = Category.fromCursor(cursor);
|
items.add(fromCursor(cursor).getName());
|
||||||
items.add(cat.getName());
|
|
||||||
}
|
}
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
|
|
@ -148,17 +100,36 @@ public class Category {
|
||||||
if (cursor != null) {
|
if (cursor != null) {
|
||||||
cursor.close();
|
cursor.close();
|
||||||
}
|
}
|
||||||
|
db.release();
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Category fromCursor(Cursor cursor) {
|
||||||
|
// Hardcoding column positions!
|
||||||
|
return new Category(
|
||||||
|
CategoryContentProvider.uriForId(cursor.getInt(0)),
|
||||||
|
cursor.getString(1),
|
||||||
|
new Date(cursor.getLong(2)),
|
||||||
|
cursor.getInt(3)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ContentValues toContentValues(Category category) {
|
||||||
|
ContentValues cv = new ContentValues();
|
||||||
|
cv.put(CategoryDao.Table.COLUMN_NAME, category.getName());
|
||||||
|
cv.put(CategoryDao.Table.COLUMN_LAST_USED, category.getLastUsed().getTime());
|
||||||
|
cv.put(CategoryDao.Table.COLUMN_TIMES_USED, category.getTimesUsed());
|
||||||
|
return cv;
|
||||||
|
}
|
||||||
|
|
||||||
public static class Table {
|
public static class Table {
|
||||||
public static final String TABLE_NAME = "categories";
|
public static final String TABLE_NAME = "categories";
|
||||||
|
|
||||||
public static final String COLUMN_ID = "_id";
|
public static final String COLUMN_ID = "_id";
|
||||||
public static final String COLUMN_NAME = "name";
|
static final String COLUMN_NAME = "name";
|
||||||
public static final String COLUMN_LAST_USED = "last_used";
|
static final String COLUMN_LAST_USED = "last_used";
|
||||||
public static final String COLUMN_TIMES_USED = "times_used";
|
static final String COLUMN_TIMES_USED = "times_used";
|
||||||
|
|
||||||
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
|
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
|
||||||
public static final String[] ALL_FIELDS = {
|
public static final String[] ALL_FIELDS = {
|
||||||
|
|
@ -168,7 +139,9 @@ public class Category {
|
||||||
COLUMN_TIMES_USED
|
COLUMN_TIMES_USED
|
||||||
};
|
};
|
||||||
|
|
||||||
private static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
|
static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME;
|
||||||
|
|
||||||
|
static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
|
||||||
+ COLUMN_ID + " INTEGER PRIMARY KEY,"
|
+ COLUMN_ID + " INTEGER PRIMARY KEY,"
|
||||||
+ COLUMN_NAME + " STRING,"
|
+ COLUMN_NAME + " STRING,"
|
||||||
+ COLUMN_LAST_USED + " INTEGER,"
|
+ COLUMN_LAST_USED + " INTEGER,"
|
||||||
|
|
@ -180,7 +153,7 @@ public class Category {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void onDelete(SQLiteDatabase db) {
|
public static void onDelete(SQLiteDatabase db) {
|
||||||
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
|
db.execSQL(DROP_TABLE_STATEMENT);
|
||||||
onCreate(db);
|
onCreate(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -208,5 +181,4 @@ public class Category {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//endregion
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,13 +1,8 @@
|
||||||
package fr.free.nrw.commons.contributions;
|
package fr.free.nrw.commons.contributions;
|
||||||
|
|
||||||
import android.content.ContentProviderClient;
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Parcel;
|
import android.os.Parcel;
|
||||||
import android.os.RemoteException;
|
import android.support.annotation.NonNull;
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
@ -16,7 +11,6 @@ import java.util.Locale;
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
import fr.free.nrw.commons.BuildConfig;
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
import fr.free.nrw.commons.CommonsApplication;
|
||||||
import fr.free.nrw.commons.Media;
|
import fr.free.nrw.commons.Media;
|
||||||
import fr.free.nrw.commons.Utils;
|
|
||||||
import fr.free.nrw.commons.settings.Prefs;
|
import fr.free.nrw.commons.settings.Prefs;
|
||||||
|
|
||||||
public class Contribution extends Media {
|
public class Contribution extends Media {
|
||||||
|
|
@ -43,7 +37,6 @@ public class Contribution extends Media {
|
||||||
public static final String SOURCE_GALLERY = "gallery";
|
public static final String SOURCE_GALLERY = "gallery";
|
||||||
public static final String SOURCE_EXTERNAL = "external";
|
public static final String SOURCE_EXTERNAL = "external";
|
||||||
|
|
||||||
private ContentProviderClient client;
|
|
||||||
private Uri contentUri;
|
private Uri contentUri;
|
||||||
private String source;
|
private String source;
|
||||||
private String editSummary;
|
private String editSummary;
|
||||||
|
|
@ -51,24 +44,42 @@ public class Contribution extends Media {
|
||||||
private int state;
|
private int state;
|
||||||
private long transferred;
|
private long transferred;
|
||||||
private String decimalCoords;
|
private String decimalCoords;
|
||||||
|
|
||||||
private boolean isMultiple;
|
private boolean isMultiple;
|
||||||
|
|
||||||
public boolean getMultiple() {
|
public Contribution(Uri contentUri, String filename, Uri localUri, String imageUrl, Date timestamp,
|
||||||
return isMultiple;
|
int state, long dataLength, Date dateUploaded, long transferred,
|
||||||
|
String source, String description, String creator, boolean isMultiple,
|
||||||
|
int width, int height, String license) {
|
||||||
|
super(localUri, imageUrl, filename, description, dataLength, timestamp, dateUploaded, creator);
|
||||||
|
this.contentUri = contentUri;
|
||||||
|
this.state = state;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.transferred = transferred;
|
||||||
|
this.source = source;
|
||||||
|
this.isMultiple = isMultiple;
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
this.license = license;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setMultiple(boolean multiple) {
|
public Contribution(Uri localUri, String imageUrl, String filename, String description, long dataLength,
|
||||||
isMultiple = multiple;
|
Date dateCreated, Date dateUploaded, String creator, String editSummary, String decimalCoords) {
|
||||||
}
|
super(localUri, imageUrl, filename, description, dataLength, dateCreated, dateUploaded, creator);
|
||||||
|
|
||||||
public Contribution(Uri localUri, String remoteUri, String filename, String description, long dataLength, Date dateCreated, Date dateUploaded, String creator, String editSummary, String decimalCoords) {
|
|
||||||
super(localUri, remoteUri, filename, description, dataLength, dateCreated, dateUploaded, creator);
|
|
||||||
this.decimalCoords = decimalCoords;
|
this.decimalCoords = decimalCoords;
|
||||||
this.editSummary = editSummary;
|
this.editSummary = editSummary;
|
||||||
timestamp = new Date(System.currentTimeMillis());
|
timestamp = new Date(System.currentTimeMillis());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Contribution(Parcel in) {
|
||||||
|
super(in);
|
||||||
|
contentUri = in.readParcelable(Uri.class.getClassLoader());
|
||||||
|
source = in.readString();
|
||||||
|
timestamp = (Date) in.readSerializable();
|
||||||
|
state = in.readInt();
|
||||||
|
transferred = in.readLong();
|
||||||
|
isMultiple = in.readInt() == 1;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void writeToParcel(Parcel parcel, int flags) {
|
public void writeToParcel(Parcel parcel, int flags) {
|
||||||
super.writeToParcel(parcel, flags);
|
super.writeToParcel(parcel, flags);
|
||||||
|
|
@ -80,14 +91,12 @@ public class Contribution extends Media {
|
||||||
parcel.writeInt(isMultiple ? 1 : 0);
|
parcel.writeInt(isMultiple ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Contribution(Parcel in) {
|
public boolean getMultiple() {
|
||||||
super(in);
|
return isMultiple;
|
||||||
contentUri = in.readParcelable(Uri.class.getClassLoader());
|
}
|
||||||
source = in.readString();
|
|
||||||
timestamp = (Date) in.readSerializable();
|
public void setMultiple(boolean multiple) {
|
||||||
state = in.readInt();
|
isMultiple = multiple;
|
||||||
transferred = in.readLong();
|
|
||||||
isMultiple = in.readInt() == 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getTransferred() {
|
public long getTransferred() {
|
||||||
|
|
@ -106,10 +115,18 @@ public class Contribution extends Media {
|
||||||
return contentUri;
|
return contentUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setContentUri(Uri contentUri) {
|
||||||
|
this.contentUri = contentUri;
|
||||||
|
}
|
||||||
|
|
||||||
public Date getTimestamp() {
|
public Date getTimestamp() {
|
||||||
return timestamp;
|
return timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setTimestamp(Date timestamp) {
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
public int getState() {
|
public int getState() {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
@ -149,68 +166,12 @@ public class Contribution extends Media {
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer.append("== {{int:license-header}} ==\n")
|
buffer.append("== {{int:license-header}} ==\n")
|
||||||
.append(Utils.licenseTemplateFor(getLicense())).append("\n\n")
|
.append(licenseTemplateFor(getLicense())).append("\n\n")
|
||||||
.append("{{Uploaded from Mobile|platform=Android|version=").append(BuildConfig.VERSION_NAME).append("}}\n")
|
.append("{{Uploaded from Mobile|platform=Android|version=").append(BuildConfig.VERSION_NAME).append("}}\n")
|
||||||
.append(getTrackingTemplates());
|
.append(getTrackingTemplates());
|
||||||
return buffer.toString();
|
return buffer.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setContentProviderClient(ContentProviderClient client) {
|
|
||||||
this.client = client;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void save() {
|
|
||||||
try {
|
|
||||||
if (contentUri == null) {
|
|
||||||
contentUri = client.insert(ContributionsContentProvider.BASE_URI, this.toContentValues());
|
|
||||||
} else {
|
|
||||||
client.update(contentUri, toContentValues(), null, null);
|
|
||||||
}
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void delete() {
|
|
||||||
try {
|
|
||||||
if (contentUri == null) {
|
|
||||||
// noooo
|
|
||||||
throw new RuntimeException("tried to delete item with no content URI");
|
|
||||||
} else {
|
|
||||||
client.delete(contentUri, null, null);
|
|
||||||
}
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public ContentValues toContentValues() {
|
|
||||||
ContentValues cv = new ContentValues();
|
|
||||||
cv.put(Table.COLUMN_FILENAME, getFilename());
|
|
||||||
if (getLocalUri() != null) {
|
|
||||||
cv.put(Table.COLUMN_LOCAL_URI, getLocalUri().toString());
|
|
||||||
}
|
|
||||||
if (getImageUrl() != null) {
|
|
||||||
cv.put(Table.COLUMN_IMAGE_URL, getImageUrl());
|
|
||||||
}
|
|
||||||
if (getDateUploaded() != null) {
|
|
||||||
cv.put(Table.COLUMN_UPLOADED, getDateUploaded().getTime());
|
|
||||||
}
|
|
||||||
cv.put(Table.COLUMN_LENGTH, getDataLength());
|
|
||||||
cv.put(Table.COLUMN_TIMESTAMP, getTimestamp().getTime());
|
|
||||||
cv.put(Table.COLUMN_STATE, getState());
|
|
||||||
cv.put(Table.COLUMN_TRANSFERRED, transferred);
|
|
||||||
cv.put(Table.COLUMN_SOURCE, source);
|
|
||||||
cv.put(Table.COLUMN_DESCRIPTION, description);
|
|
||||||
cv.put(Table.COLUMN_CREATOR, creator);
|
|
||||||
cv.put(Table.COLUMN_MULTIPLE, isMultiple ? 1 : 0);
|
|
||||||
cv.put(Table.COLUMN_WIDTH, width);
|
|
||||||
cv.put(Table.COLUMN_HEIGHT, height);
|
|
||||||
cv.put(Table.COLUMN_LICENSE, license);
|
|
||||||
return cv;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setFilename(String filename) {
|
public void setFilename(String filename) {
|
||||||
this.filename = filename;
|
this.filename = filename;
|
||||||
|
|
@ -224,33 +185,6 @@ public class Contribution extends Media {
|
||||||
timestamp = new Date(System.currentTimeMillis());
|
timestamp = new Date(System.currentTimeMillis());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Contribution fromCursor(Cursor cursor) {
|
|
||||||
// Hardcoding column positions!
|
|
||||||
Contribution c = new Contribution();
|
|
||||||
|
|
||||||
//Check that cursor has a value to avoid CursorIndexOutOfBoundsException
|
|
||||||
if (cursor.getCount() > 0) {
|
|
||||||
c.contentUri = ContributionsContentProvider.uriForId(cursor.getInt(0));
|
|
||||||
c.filename = cursor.getString(1);
|
|
||||||
c.localUri = TextUtils.isEmpty(cursor.getString(2)) ? null : Uri.parse(cursor.getString(2));
|
|
||||||
c.imageUrl = cursor.getString(3);
|
|
||||||
c.timestamp = cursor.getLong(4) == 0 ? null : new Date(cursor.getLong(4));
|
|
||||||
c.state = cursor.getInt(5);
|
|
||||||
c.dataLength = cursor.getLong(6);
|
|
||||||
c.dateUploaded = cursor.getLong(7) == 0 ? null : new Date(cursor.getLong(7));
|
|
||||||
c.transferred = cursor.getLong(8);
|
|
||||||
c.source = cursor.getString(9);
|
|
||||||
c.description = cursor.getString(10);
|
|
||||||
c.creator = cursor.getString(11);
|
|
||||||
c.isMultiple = cursor.getInt(12) == 1;
|
|
||||||
c.width = cursor.getInt(13);
|
|
||||||
c.height = cursor.getInt(14);
|
|
||||||
c.license = cursor.getString(15);
|
|
||||||
}
|
|
||||||
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSource() {
|
public String getSource() {
|
||||||
return source;
|
return source;
|
||||||
}
|
}
|
||||||
|
|
@ -267,118 +201,25 @@ public class Contribution extends Media {
|
||||||
this.decimalCoords = decimalCoords;
|
this.decimalCoords = decimalCoords;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Table {
|
@NonNull
|
||||||
public static final String TABLE_NAME = "contributions";
|
private String licenseTemplateFor(String license) {
|
||||||
|
switch (license) {
|
||||||
public static final String COLUMN_ID = "_id";
|
case Prefs.Licenses.CC_BY_3:
|
||||||
public static final String COLUMN_FILENAME = "filename";
|
return "{{self|cc-by-3.0}}";
|
||||||
public static final String COLUMN_LOCAL_URI = "local_uri";
|
case Prefs.Licenses.CC_BY_4:
|
||||||
public static final String COLUMN_IMAGE_URL = "image_url";
|
return "{{self|cc-by-4.0}}";
|
||||||
public static final String COLUMN_TIMESTAMP = "timestamp";
|
case Prefs.Licenses.CC_BY_SA_3:
|
||||||
public static final String COLUMN_STATE = "state";
|
return "{{self|cc-by-sa-3.0}}";
|
||||||
public static final String COLUMN_LENGTH = "length";
|
case Prefs.Licenses.CC_BY_SA_4:
|
||||||
public static final String COLUMN_UPLOADED = "uploaded";
|
return "{{self|cc-by-sa-4.0}}";
|
||||||
public static final String COLUMN_TRANSFERRED = "transferred"; // Currently transferred number of bytes
|
case Prefs.Licenses.CC0:
|
||||||
public static final String COLUMN_SOURCE = "source";
|
return "{{self|cc-zero}}";
|
||||||
public static final String COLUMN_DESCRIPTION = "description";
|
case Prefs.Licenses.CC_BY:
|
||||||
public static final String COLUMN_CREATOR = "creator"; // Initial uploader
|
return "{{self|cc-by-3.0}}";
|
||||||
public static final String COLUMN_MULTIPLE = "multiple";
|
case Prefs.Licenses.CC_BY_SA:
|
||||||
public static final String COLUMN_WIDTH = "width";
|
return "{{self|cc-by-sa-3.0}}";
|
||||||
public static final String COLUMN_HEIGHT = "height";
|
|
||||||
public static final String COLUMN_LICENSE = "license";
|
|
||||||
|
|
||||||
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
|
|
||||||
public static final String[] ALL_FIELDS = {
|
|
||||||
COLUMN_ID,
|
|
||||||
COLUMN_FILENAME,
|
|
||||||
COLUMN_LOCAL_URI,
|
|
||||||
COLUMN_IMAGE_URL,
|
|
||||||
COLUMN_TIMESTAMP,
|
|
||||||
COLUMN_STATE,
|
|
||||||
COLUMN_LENGTH,
|
|
||||||
COLUMN_UPLOADED,
|
|
||||||
COLUMN_TRANSFERRED,
|
|
||||||
COLUMN_SOURCE,
|
|
||||||
COLUMN_DESCRIPTION,
|
|
||||||
COLUMN_CREATOR,
|
|
||||||
COLUMN_MULTIPLE,
|
|
||||||
COLUMN_WIDTH,
|
|
||||||
COLUMN_HEIGHT,
|
|
||||||
COLUMN_LICENSE
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
private static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
|
|
||||||
+ "_id INTEGER PRIMARY KEY,"
|
|
||||||
+ "filename STRING,"
|
|
||||||
+ "local_uri STRING,"
|
|
||||||
+ "image_url STRING,"
|
|
||||||
+ "uploaded INTEGER,"
|
|
||||||
+ "timestamp INTEGER,"
|
|
||||||
+ "state INTEGER,"
|
|
||||||
+ "length INTEGER,"
|
|
||||||
+ "transferred INTEGER,"
|
|
||||||
+ "source STRING,"
|
|
||||||
+ "description STRING,"
|
|
||||||
+ "creator STRING,"
|
|
||||||
+ "multiple INTEGER,"
|
|
||||||
+ "width INTEGER,"
|
|
||||||
+ "height INTEGER,"
|
|
||||||
+ "LICENSE STRING"
|
|
||||||
+ ");";
|
|
||||||
|
|
||||||
|
|
||||||
public static void onCreate(SQLiteDatabase db) {
|
|
||||||
db.execSQL(CREATE_TABLE_STATEMENT);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void onDelete(SQLiteDatabase db) {
|
throw new RuntimeException("Unrecognized license value: " + license);
|
||||||
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
|
|
||||||
onCreate(db);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void onUpdate(SQLiteDatabase db, int from, int to) {
|
|
||||||
if (from == to) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (from == 1) {
|
|
||||||
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN description STRING;");
|
|
||||||
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN creator STRING;");
|
|
||||||
from++;
|
|
||||||
onUpdate(db, from, to);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (from == 2) {
|
|
||||||
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN multiple INTEGER;");
|
|
||||||
db.execSQL("UPDATE " + TABLE_NAME + " SET multiple = 0");
|
|
||||||
from++;
|
|
||||||
onUpdate(db, from, to);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (from == 3) {
|
|
||||||
// Do nothing
|
|
||||||
from++;
|
|
||||||
onUpdate(db, from, to);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (from == 4) {
|
|
||||||
// Do nothing -- added Category
|
|
||||||
from++;
|
|
||||||
onUpdate(db, from, to);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (from == 5) {
|
|
||||||
// Added width and height fields
|
|
||||||
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN width INTEGER;");
|
|
||||||
db.execSQL("UPDATE " + TABLE_NAME + " SET width = 0");
|
|
||||||
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN height INTEGER;");
|
|
||||||
db.execSQL("UPDATE " + TABLE_NAME + " SET height = 0");
|
|
||||||
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN license STRING;");
|
|
||||||
db.execSQL("UPDATE " + TABLE_NAME + " SET license='" + Prefs.Licenses.CC_BY_SA_3 + "';");
|
|
||||||
from++;
|
|
||||||
onUpdate(db, from, to);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,10 +93,15 @@ class ContributionController {
|
||||||
shareIntent.putExtra(EXTRA_SOURCE, SOURCE_GALLERY);
|
shareIntent.putExtra(EXTRA_SOURCE, SOURCE_GALLERY);
|
||||||
break;
|
break;
|
||||||
case SELECT_FROM_CAMERA:
|
case SELECT_FROM_CAMERA:
|
||||||
shareIntent.setType("image/jpeg"); //FIXME: Find out appropriate mime type
|
//FIXME: Find out appropriate mime type
|
||||||
|
// AFAIK this is the right type for a JPEG image
|
||||||
|
// https://developer.android.com/training/sharing/send.html#send-binary-content
|
||||||
|
shareIntent.setType("image/jpeg");
|
||||||
shareIntent.putExtra(EXTRA_STREAM, lastGeneratedCaptureUri);
|
shareIntent.putExtra(EXTRA_STREAM, lastGeneratedCaptureUri);
|
||||||
shareIntent.putExtra(EXTRA_SOURCE, SOURCE_CAMERA);
|
shareIntent.putExtra(EXTRA_SOURCE, SOURCE_CAMERA);
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
Timber.i("Image selected");
|
Timber.i("Image selected");
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,281 @@
|
||||||
|
package fr.free.nrw.commons.contributions;
|
||||||
|
|
||||||
|
import android.content.ContentProviderClient;
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
import javax.inject.Provider;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.settings.Prefs;
|
||||||
|
|
||||||
|
import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS;
|
||||||
|
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI;
|
||||||
|
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.uriForId;
|
||||||
|
|
||||||
|
public class ContributionDao {
|
||||||
|
/*
|
||||||
|
This sorts in the following order:
|
||||||
|
Currently Uploading
|
||||||
|
Failed (Sorted in ascending order of time added - FIFO)
|
||||||
|
Queued to Upload (Sorted in ascending order of time added - FIFO)
|
||||||
|
Completed (Sorted in descending order of time added)
|
||||||
|
|
||||||
|
This is why Contribution.STATE_COMPLETED is -1.
|
||||||
|
*/
|
||||||
|
static final String CONTRIBUTION_SORT = Table.COLUMN_STATE + " DESC, "
|
||||||
|
+ Table.COLUMN_UPLOADED + " DESC , ("
|
||||||
|
+ Table.COLUMN_TIMESTAMP + " * "
|
||||||
|
+ Table.COLUMN_STATE + ")";
|
||||||
|
|
||||||
|
private final Provider<ContentProviderClient> clientProvider;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public ContributionDao(@Named("contribution") Provider<ContentProviderClient> clientProvider) {
|
||||||
|
this.clientProvider = clientProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
Cursor loadAllContributions() {
|
||||||
|
ContentProviderClient db = clientProvider.get();
|
||||||
|
try {
|
||||||
|
return db.query(BASE_URI, ALL_FIELDS, "", null, CONTRIBUTION_SORT);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
db.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void save(Contribution contribution) {
|
||||||
|
ContentProviderClient db = clientProvider.get();
|
||||||
|
try {
|
||||||
|
if (contribution.getContentUri() == null) {
|
||||||
|
contribution.setContentUri(db.insert(BASE_URI, toContentValues(contribution)));
|
||||||
|
} else {
|
||||||
|
db.update(contribution.getContentUri(), toContentValues(contribution), null, null);
|
||||||
|
}
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
} finally {
|
||||||
|
db.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(Contribution contribution) {
|
||||||
|
ContentProviderClient db = clientProvider.get();
|
||||||
|
try {
|
||||||
|
if (contribution.getContentUri() == null) {
|
||||||
|
// noooo
|
||||||
|
throw new RuntimeException("tried to delete item with no content URI");
|
||||||
|
} else {
|
||||||
|
db.delete(contribution.getContentUri(), null, null);
|
||||||
|
}
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
} finally {
|
||||||
|
db.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentValues toContentValues(Contribution contribution) {
|
||||||
|
ContentValues cv = new ContentValues();
|
||||||
|
cv.put(Table.COLUMN_FILENAME, contribution.getFilename());
|
||||||
|
if (contribution.getLocalUri() != null) {
|
||||||
|
cv.put(Table.COLUMN_LOCAL_URI, contribution.getLocalUri().toString());
|
||||||
|
}
|
||||||
|
if (contribution.getImageUrl() != null) {
|
||||||
|
cv.put(Table.COLUMN_IMAGE_URL, contribution.getImageUrl());
|
||||||
|
}
|
||||||
|
if (contribution.getDateUploaded() != null) {
|
||||||
|
cv.put(Table.COLUMN_UPLOADED, contribution.getDateUploaded().getTime());
|
||||||
|
}
|
||||||
|
cv.put(Table.COLUMN_LENGTH, contribution.getDataLength());
|
||||||
|
cv.put(Table.COLUMN_TIMESTAMP, contribution.getTimestamp().getTime());
|
||||||
|
cv.put(Table.COLUMN_STATE, contribution.getState());
|
||||||
|
cv.put(Table.COLUMN_TRANSFERRED, contribution.getTransferred());
|
||||||
|
cv.put(Table.COLUMN_SOURCE, contribution.getSource());
|
||||||
|
cv.put(Table.COLUMN_DESCRIPTION, contribution.getDescription());
|
||||||
|
cv.put(Table.COLUMN_CREATOR, contribution.getCreator());
|
||||||
|
cv.put(Table.COLUMN_MULTIPLE, contribution.getMultiple() ? 1 : 0);
|
||||||
|
cv.put(Table.COLUMN_WIDTH, contribution.getWidth());
|
||||||
|
cv.put(Table.COLUMN_HEIGHT, contribution.getHeight());
|
||||||
|
cv.put(Table.COLUMN_LICENSE, contribution.getLicense());
|
||||||
|
return cv;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Contribution fromCursor(Cursor cursor) {
|
||||||
|
// Hardcoding column positions!
|
||||||
|
//Check that cursor has a value to avoid CursorIndexOutOfBoundsException
|
||||||
|
if (cursor.getCount() > 0) {
|
||||||
|
return new Contribution(
|
||||||
|
uriForId(cursor.getInt(0)),
|
||||||
|
cursor.getString(1),
|
||||||
|
parseUri(cursor.getString(2)),
|
||||||
|
cursor.getString(3),
|
||||||
|
parseTimestamp(cursor.getLong(4)),
|
||||||
|
cursor.getInt(5),
|
||||||
|
cursor.getLong(6),
|
||||||
|
parseTimestamp(cursor.getLong(7)),
|
||||||
|
cursor.getLong(8),
|
||||||
|
cursor.getString(9),
|
||||||
|
cursor.getString(10),
|
||||||
|
cursor.getString(11),
|
||||||
|
cursor.getInt(12) == 1,
|
||||||
|
cursor.getInt(13),
|
||||||
|
cursor.getInt(14),
|
||||||
|
cursor.getString(15));
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static Date parseTimestamp(long timestamp) {
|
||||||
|
return timestamp == 0 ? null : new Date(timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static Uri parseUri(String uriString) {
|
||||||
|
return TextUtils.isEmpty(uriString) ? null : Uri.parse(uriString);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Table {
|
||||||
|
public static final String TABLE_NAME = "contributions";
|
||||||
|
|
||||||
|
public static final String COLUMN_ID = "_id";
|
||||||
|
public static final String COLUMN_FILENAME = "filename";
|
||||||
|
public static final String COLUMN_LOCAL_URI = "local_uri";
|
||||||
|
public static final String COLUMN_IMAGE_URL = "image_url";
|
||||||
|
public static final String COLUMN_TIMESTAMP = "timestamp";
|
||||||
|
public static final String COLUMN_STATE = "state";
|
||||||
|
public static final String COLUMN_LENGTH = "length";
|
||||||
|
public static final String COLUMN_UPLOADED = "uploaded";
|
||||||
|
public static final String COLUMN_TRANSFERRED = "transferred"; // Currently transferred number of bytes
|
||||||
|
public static final String COLUMN_SOURCE = "source";
|
||||||
|
public static final String COLUMN_DESCRIPTION = "description";
|
||||||
|
public static final String COLUMN_CREATOR = "creator"; // Initial uploader
|
||||||
|
public static final String COLUMN_MULTIPLE = "multiple";
|
||||||
|
public static final String COLUMN_WIDTH = "width";
|
||||||
|
public static final String COLUMN_HEIGHT = "height";
|
||||||
|
public static final String COLUMN_LICENSE = "license";
|
||||||
|
|
||||||
|
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
|
||||||
|
public static final String[] ALL_FIELDS = {
|
||||||
|
COLUMN_ID,
|
||||||
|
COLUMN_FILENAME,
|
||||||
|
COLUMN_LOCAL_URI,
|
||||||
|
COLUMN_IMAGE_URL,
|
||||||
|
COLUMN_TIMESTAMP,
|
||||||
|
COLUMN_STATE,
|
||||||
|
COLUMN_LENGTH,
|
||||||
|
COLUMN_UPLOADED,
|
||||||
|
COLUMN_TRANSFERRED,
|
||||||
|
COLUMN_SOURCE,
|
||||||
|
COLUMN_DESCRIPTION,
|
||||||
|
COLUMN_CREATOR,
|
||||||
|
COLUMN_MULTIPLE,
|
||||||
|
COLUMN_WIDTH,
|
||||||
|
COLUMN_HEIGHT,
|
||||||
|
COLUMN_LICENSE
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME;
|
||||||
|
|
||||||
|
public static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
|
||||||
|
+ "_id INTEGER PRIMARY KEY,"
|
||||||
|
+ "filename STRING,"
|
||||||
|
+ "local_uri STRING,"
|
||||||
|
+ "image_url STRING,"
|
||||||
|
+ "uploaded INTEGER,"
|
||||||
|
+ "timestamp INTEGER,"
|
||||||
|
+ "state INTEGER,"
|
||||||
|
+ "length INTEGER,"
|
||||||
|
+ "transferred INTEGER,"
|
||||||
|
+ "source STRING,"
|
||||||
|
+ "description STRING,"
|
||||||
|
+ "creator STRING,"
|
||||||
|
+ "multiple INTEGER,"
|
||||||
|
+ "width INTEGER,"
|
||||||
|
+ "height INTEGER,"
|
||||||
|
+ "LICENSE STRING"
|
||||||
|
+ ");";
|
||||||
|
|
||||||
|
// Upgrade from version 1 ->
|
||||||
|
static final String ADD_CREATOR_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN creator STRING;";
|
||||||
|
static final String ADD_DESCRIPTION_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN description STRING;";
|
||||||
|
|
||||||
|
// Upgrade from version 2 ->
|
||||||
|
static final String ADD_MULTIPLE_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN multiple INTEGER;";
|
||||||
|
static final String SET_DEFAULT_MULTIPLE = "UPDATE " + TABLE_NAME + " SET multiple = 0";
|
||||||
|
|
||||||
|
// Upgrade from version 5 ->
|
||||||
|
static final String ADD_WIDTH_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN width INTEGER;";
|
||||||
|
static final String SET_DEFAULT_WIDTH = "UPDATE " + TABLE_NAME + " SET width = 0";
|
||||||
|
static final String ADD_HEIGHT_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN height INTEGER;";
|
||||||
|
static final String SET_DEFAULT_HEIGHT = "UPDATE " + TABLE_NAME + " SET height = 0";
|
||||||
|
static final String ADD_LICENSE_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN license STRING;";
|
||||||
|
static final String SET_DEFAULT_LICENSE = "UPDATE " + TABLE_NAME + " SET license='" + Prefs.Licenses.CC_BY_SA_3 + "';";
|
||||||
|
|
||||||
|
|
||||||
|
public static void onCreate(SQLiteDatabase db) {
|
||||||
|
db.execSQL(CREATE_TABLE_STATEMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void onDelete(SQLiteDatabase db) {
|
||||||
|
db.execSQL(DROP_TABLE_STATEMENT);
|
||||||
|
onCreate(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void onUpdate(SQLiteDatabase db, int from, int to) {
|
||||||
|
if (from == to) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (from == 1) {
|
||||||
|
db.execSQL(ADD_DESCRIPTION_FIELD);
|
||||||
|
db.execSQL(ADD_CREATOR_FIELD);
|
||||||
|
from++;
|
||||||
|
onUpdate(db, from, to);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (from == 2) {
|
||||||
|
db.execSQL(ADD_MULTIPLE_FIELD);
|
||||||
|
db.execSQL(SET_DEFAULT_MULTIPLE);
|
||||||
|
from++;
|
||||||
|
onUpdate(db, from, to);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (from == 3) {
|
||||||
|
// Do nothing
|
||||||
|
from++;
|
||||||
|
onUpdate(db, from, to);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (from == 4) {
|
||||||
|
// Do nothing -- added Category
|
||||||
|
from++;
|
||||||
|
onUpdate(db, from, to);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (from == 5) {
|
||||||
|
// Added width and height fields
|
||||||
|
db.execSQL(ADD_WIDTH_FIELD);
|
||||||
|
db.execSQL(SET_DEFAULT_WIDTH);
|
||||||
|
db.execSQL(ADD_HEIGHT_FIELD);
|
||||||
|
db.execSQL(SET_DEFAULT_HEIGHT);
|
||||||
|
db.execSQL(ADD_LICENSE_FIELD);
|
||||||
|
db.execSQL(SET_DEFAULT_LICENSE);
|
||||||
|
from++;
|
||||||
|
onUpdate(db, from, to);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,7 +9,6 @@ import android.database.Cursor;
|
||||||
import android.database.DataSetObserver;
|
import android.database.DataSetObserver;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.support.v4.app.FragmentManager;
|
import android.support.v4.app.FragmentManager;
|
||||||
import android.support.v4.app.LoaderManager;
|
import android.support.v4.app.LoaderManager;
|
||||||
import android.support.v4.content.CursorLoader;
|
import android.support.v4.content.CursorLoader;
|
||||||
|
|
@ -23,13 +22,17 @@ import android.widget.AdapterView;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
import fr.free.nrw.commons.HandlerService;
|
import fr.free.nrw.commons.HandlerService;
|
||||||
import fr.free.nrw.commons.Media;
|
import fr.free.nrw.commons.Media;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.auth.AuthenticatedActivity;
|
import fr.free.nrw.commons.auth.AuthenticatedActivity;
|
||||||
|
import fr.free.nrw.commons.auth.SessionManager;
|
||||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
||||||
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
import fr.free.nrw.commons.settings.Prefs;
|
import fr.free.nrw.commons.settings.Prefs;
|
||||||
import fr.free.nrw.commons.upload.UploadService;
|
import fr.free.nrw.commons.upload.UploadService;
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
|
@ -39,37 +42,29 @@ import timber.log.Timber;
|
||||||
|
|
||||||
import static android.content.ContentResolver.requestSync;
|
import static android.content.ContentResolver.requestSync;
|
||||||
import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED;
|
import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED;
|
||||||
import static fr.free.nrw.commons.contributions.Contribution.Table.ALL_FIELDS;
|
import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS;
|
||||||
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.AUTHORITY;
|
|
||||||
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI;
|
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI;
|
||||||
import static fr.free.nrw.commons.settings.Prefs.UPLOADS_SHOWING;
|
import static fr.free.nrw.commons.settings.Prefs.UPLOADS_SHOWING;
|
||||||
|
|
||||||
public class ContributionsActivity extends AuthenticatedActivity
|
public class ContributionsActivity
|
||||||
implements LoaderManager.LoaderCallbacks<Cursor>, AdapterView.OnItemClickListener,
|
extends AuthenticatedActivity
|
||||||
MediaDetailPagerFragment.MediaDetailProvider, FragmentManager.OnBackStackChangedListener,
|
implements LoaderManager.LoaderCallbacks<Cursor>,
|
||||||
|
AdapterView.OnItemClickListener,
|
||||||
|
MediaDetailPagerFragment.MediaDetailProvider,
|
||||||
|
FragmentManager.OnBackStackChangedListener,
|
||||||
ContributionsListFragment.SourceRefresher {
|
ContributionsListFragment.SourceRefresher {
|
||||||
|
|
||||||
|
@Inject MediaWikiApi mediaWikiApi;
|
||||||
|
@Inject SessionManager sessionManager;
|
||||||
|
@Inject @Named("default_preferences") SharedPreferences prefs;
|
||||||
|
@Inject ContributionDao contributionDao;
|
||||||
|
|
||||||
private Cursor allContributions;
|
private Cursor allContributions;
|
||||||
private ContributionsListFragment contributionsList;
|
private ContributionsListFragment contributionsList;
|
||||||
private MediaDetailPagerFragment mediaDetails;
|
private MediaDetailPagerFragment mediaDetails;
|
||||||
private UploadService uploadService;
|
private UploadService uploadService;
|
||||||
private boolean isUploadServiceConnected;
|
private boolean isUploadServiceConnected;
|
||||||
private ArrayList<DataSetObserver> observersWaitingForLoad = new ArrayList<>();
|
private ArrayList<DataSetObserver> observersWaitingForLoad = new ArrayList<>();
|
||||||
private String CONTRIBUTION_SELECTION = "";
|
|
||||||
|
|
||||||
/*
|
|
||||||
This sorts in the following order:
|
|
||||||
Currently Uploading
|
|
||||||
Failed (Sorted in ascending order of time added - FIFO)
|
|
||||||
Queued to Upload (Sorted in ascending order of time added - FIFO)
|
|
||||||
Completed (Sorted in descending order of time added)
|
|
||||||
|
|
||||||
This is why Contribution.STATE_COMPLETED is -1.
|
|
||||||
*/
|
|
||||||
private String CONTRIBUTION_SORT = Contribution.Table.COLUMN_STATE + " DESC, "
|
|
||||||
+ Contribution.Table.COLUMN_UPLOADED + " DESC , ("
|
|
||||||
+ Contribution.Table.COLUMN_TIMESTAMP + " * "
|
|
||||||
+ Contribution.Table.COLUMN_STATE + ")";
|
|
||||||
|
|
||||||
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||||
|
|
||||||
|
|
@ -84,7 +79,7 @@ public class ContributionsActivity extends AuthenticatedActivity
|
||||||
@Override
|
@Override
|
||||||
public void onServiceDisconnected(ComponentName componentName) {
|
public void onServiceDisconnected(ComponentName componentName) {
|
||||||
// this should never happen
|
// this should never happen
|
||||||
throw new RuntimeException("UploadService died but the rest of the process did not!");
|
Timber.e(new RuntimeException("UploadService died but the rest of the process did not!"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -101,12 +96,8 @@ public class ContributionsActivity extends AuthenticatedActivity
|
||||||
@Override
|
@Override
|
||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
|
boolean isSettingsChanged = prefs.getBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false);
|
||||||
boolean isSettingsChanged =
|
prefs.edit().putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false).apply();
|
||||||
sharedPreferences.getBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false);
|
|
||||||
SharedPreferences.Editor editor = sharedPreferences.edit();
|
|
||||||
editor.putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false);
|
|
||||||
editor.apply();
|
|
||||||
if (isSettingsChanged) {
|
if (isSettingsChanged) {
|
||||||
refreshSource();
|
refreshSource();
|
||||||
}
|
}
|
||||||
|
|
@ -114,16 +105,14 @@ public class ContributionsActivity extends AuthenticatedActivity
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onAuthCookieAcquired(String authCookie) {
|
protected void onAuthCookieAcquired(String authCookie) {
|
||||||
// Do a sync every time we get here!
|
// Do a sync everytime we get here!
|
||||||
CommonsApplication app = ((CommonsApplication) getApplication());
|
requestSync(sessionManager.getCurrentAccount(), ContributionsContentProvider.CONTRIBUTION_AUTHORITY, new Bundle());
|
||||||
requestSync(app.getCurrentAccount(), AUTHORITY, new Bundle());
|
|
||||||
Intent uploadServiceIntent = new Intent(this, UploadService.class);
|
Intent uploadServiceIntent = new Intent(this, UploadService.class);
|
||||||
uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE);
|
uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE);
|
||||||
startService(uploadServiceIntent);
|
startService(uploadServiceIntent);
|
||||||
bindService(uploadServiceIntent, uploadServiceConnection, Context.BIND_AUTO_CREATE);
|
bindService(uploadServiceIntent, uploadServiceConnection, Context.BIND_AUTO_CREATE);
|
||||||
|
|
||||||
allContributions = getContentResolver().query(BASE_URI, ALL_FIELDS,
|
allContributions = contributionDao.loadAllContributions();
|
||||||
CONTRIBUTION_SELECTION, null, CONTRIBUTION_SORT);
|
|
||||||
|
|
||||||
getSupportLoaderManager().initLoader(0, null, this);
|
getSupportLoaderManager().initLoader(0, null, this);
|
||||||
}
|
}
|
||||||
|
|
@ -137,17 +126,20 @@ public class ContributionsActivity extends AuthenticatedActivity
|
||||||
// Activity can call methods in the fragment by acquiring a
|
// Activity can call methods in the fragment by acquiring a
|
||||||
// reference to the Fragment from FragmentManager, using findFragmentById()
|
// reference to the Fragment from FragmentManager, using findFragmentById()
|
||||||
FragmentManager supportFragmentManager = getSupportFragmentManager();
|
FragmentManager supportFragmentManager = getSupportFragmentManager();
|
||||||
contributionsList = (ContributionsListFragment) supportFragmentManager
|
contributionsList = (ContributionsListFragment)supportFragmentManager
|
||||||
.findFragmentById(R.id.contributionsListFragment);
|
.findFragmentById(R.id.contributionsListFragment);
|
||||||
|
|
||||||
supportFragmentManager.addOnBackStackChangedListener(this);
|
supportFragmentManager.addOnBackStackChangedListener(this);
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
mediaDetails = (MediaDetailPagerFragment) supportFragmentManager
|
mediaDetails = (MediaDetailPagerFragment)supportFragmentManager
|
||||||
.findFragmentById(R.id.contributionsFragmentContainer);
|
.findFragmentById(R.id.contributionsFragmentContainer);
|
||||||
|
|
||||||
|
getSupportLoaderManager().initLoader(0, null, this);
|
||||||
}
|
}
|
||||||
requestAuthToken();
|
requestAuthToken();
|
||||||
initDrawer();
|
initDrawer();
|
||||||
setTitle(getString(R.string.title_activity_contributions));
|
setTitle(getString(R.string.title_activity_contributions));
|
||||||
|
setUploadCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -178,24 +170,23 @@ public class ContributionsActivity extends AuthenticatedActivity
|
||||||
|
|
||||||
public void retryUpload(int i) {
|
public void retryUpload(int i) {
|
||||||
allContributions.moveToPosition(i);
|
allContributions.moveToPosition(i);
|
||||||
Contribution c = Contribution.fromCursor(allContributions);
|
Contribution c = contributionDao.fromCursor(allContributions);
|
||||||
if (c.getState() == STATE_FAILED) {
|
if (c.getState() == STATE_FAILED) {
|
||||||
uploadService.queue(UploadService.ACTION_UPLOAD_FILE, c);
|
uploadService.queue(UploadService.ACTION_UPLOAD_FILE, c);
|
||||||
Timber.d("Restarting for %s", c.toContentValues());
|
Timber.d("Restarting for %s", c.toString());
|
||||||
} else {
|
} else {
|
||||||
Timber.d("Skipping re-upload for non-failed %s", c.toContentValues());
|
Timber.d("Skipping re-upload for non-failed %s", c.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteUpload(int i) {
|
public void deleteUpload(int i) {
|
||||||
allContributions.moveToPosition(i);
|
allContributions.moveToPosition(i);
|
||||||
Contribution c = Contribution.fromCursor(allContributions);
|
Contribution c = contributionDao.fromCursor(allContributions);
|
||||||
if (c.getState() == STATE_FAILED) {
|
if (c.getState() == STATE_FAILED) {
|
||||||
Timber.d("Deleting failed contrib %s", c.toContentValues());
|
Timber.d("Deleting failed contrib %s", c.toString());
|
||||||
c.setContentProviderClient(getContentResolver().acquireContentProviderClient(AUTHORITY));
|
contributionDao.delete(c);
|
||||||
c.delete();
|
|
||||||
} else {
|
} else {
|
||||||
Timber.d("Skipping deletion for non-failed contrib %s", c.toContentValues());
|
Timber.d("Skipping deletion for non-failed contrib %s", c.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -229,24 +220,23 @@ public class ContributionsActivity extends AuthenticatedActivity
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
|
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
|
||||||
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
|
int uploads = prefs.getInt(UPLOADS_SHOWING, 100);
|
||||||
int uploads = sharedPref.getInt(UPLOADS_SHOWING, 100);
|
|
||||||
return new CursorLoader(this, BASE_URI,
|
return new CursorLoader(this, BASE_URI,
|
||||||
ALL_FIELDS, CONTRIBUTION_SELECTION, null,
|
ALL_FIELDS, "", null,
|
||||||
CONTRIBUTION_SORT + "LIMIT " + uploads);
|
ContributionDao.CONTRIBUTION_SORT + "LIMIT " + uploads);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
|
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
|
||||||
|
contributionsList.changeProgressBarVisibility(false);
|
||||||
|
|
||||||
if (contributionsList.getAdapter() == null) {
|
if (contributionsList.getAdapter() == null) {
|
||||||
contributionsList.setAdapter(new ContributionsListAdapter(getApplicationContext(),
|
contributionsList.setAdapter(new ContributionsListAdapter(getApplicationContext(),
|
||||||
cursor, 0));
|
cursor, 0, contributionDao));
|
||||||
} else {
|
} else {
|
||||||
((CursorAdapter) contributionsList.getAdapter()).swapCursor(cursor);
|
((CursorAdapter) contributionsList.getAdapter()).swapCursor(cursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
setUploadCount();
|
|
||||||
|
|
||||||
contributionsList.clearSyncMessage();
|
contributionsList.clearSyncMessage();
|
||||||
notifyAndMigrateDataSetObservers();
|
notifyAndMigrateDataSetObservers();
|
||||||
}
|
}
|
||||||
|
|
@ -263,7 +253,7 @@ public class ContributionsActivity extends AuthenticatedActivity
|
||||||
// not yet ready to return data
|
// not yet ready to return data
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
return Contribution.fromCursor((Cursor) contributionsList.getAdapter().getItem(i));
|
return contributionDao.fromCursor((Cursor) contributionsList.getAdapter().getItem(i));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -277,10 +267,8 @@ public class ContributionsActivity extends AuthenticatedActivity
|
||||||
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
@SuppressWarnings("ConstantConditions")
|
||||||
private void setUploadCount() {
|
private void setUploadCount() {
|
||||||
CommonsApplication app = ((CommonsApplication) getApplication());
|
compositeDisposable.add(mediaWikiApi
|
||||||
compositeDisposable.add(
|
.getUploadCount(sessionManager.getCurrentAccount().name)
|
||||||
app.getMWApi()
|
|
||||||
.getUploadCount(app.getCurrentAccount().name)
|
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
|
|
@ -288,8 +276,7 @@ public class ContributionsActivity extends AuthenticatedActivity
|
||||||
.getQuantityString(R.plurals.contributions_subtitle,
|
.getQuantityString(R.plurals.contributions_subtitle,
|
||||||
uploadCount, uploadCount)),
|
uploadCount, uploadCount)),
|
||||||
t -> Timber.e(t, "Fetching upload count failed")
|
t -> Timber.e(t, "Fetching upload count failed")
|
||||||
)
|
));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -341,9 +328,4 @@ public class ContributionsActivity extends AuthenticatedActivity
|
||||||
public void refreshSource() {
|
public void refreshSource() {
|
||||||
getSupportLoaderManager().restartLoader(0, null, this);
|
getSupportLoaderManager().restartLoader(0, null, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void startYourself(Context context) {
|
|
||||||
context.startActivity(new Intent(context, ContributionsActivity.class));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package fr.free.nrw.commons.contributions;
|
package fr.free.nrw.commons.contributions;
|
||||||
|
|
||||||
import android.content.ContentProvider;
|
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.content.UriMatcher;
|
import android.content.UriMatcher;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
|
|
@ -10,36 +9,36 @@ import android.net.Uri;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.data.DBOpenHelper;
|
||||||
|
import fr.free.nrw.commons.di.CommonsDaggerContentProvider;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
import static android.content.UriMatcher.NO_MATCH;
|
import static android.content.UriMatcher.NO_MATCH;
|
||||||
import static fr.free.nrw.commons.contributions.Contribution.Table.ALL_FIELDS;
|
import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS;
|
||||||
import static fr.free.nrw.commons.contributions.Contribution.Table.TABLE_NAME;
|
import static fr.free.nrw.commons.contributions.ContributionDao.Table.TABLE_NAME;
|
||||||
|
|
||||||
public class ContributionsContentProvider extends ContentProvider {
|
public class ContributionsContentProvider extends CommonsDaggerContentProvider {
|
||||||
|
|
||||||
private static final int CONTRIBUTIONS = 1;
|
private static final int CONTRIBUTIONS = 1;
|
||||||
private static final int CONTRIBUTIONS_ID = 2;
|
private static final int CONTRIBUTIONS_ID = 2;
|
||||||
private static final String BASE_PATH = "contributions";
|
private static final String BASE_PATH = "contributions";
|
||||||
private static final UriMatcher uriMatcher = new UriMatcher(NO_MATCH);
|
private static final UriMatcher uriMatcher = new UriMatcher(NO_MATCH);
|
||||||
public static final String AUTHORITY = "fr.free.nrw.commons.contributions.contentprovider";
|
public static final String CONTRIBUTION_AUTHORITY = "fr.free.nrw.commons.contributions.contentprovider";
|
||||||
|
|
||||||
public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY + "/" + BASE_PATH);
|
public static final Uri BASE_URI = Uri.parse("content://" + CONTRIBUTION_AUTHORITY + "/" + BASE_PATH);
|
||||||
|
|
||||||
static {
|
static {
|
||||||
uriMatcher.addURI(AUTHORITY, BASE_PATH, CONTRIBUTIONS);
|
uriMatcher.addURI(CONTRIBUTION_AUTHORITY, BASE_PATH, CONTRIBUTIONS);
|
||||||
uriMatcher.addURI(AUTHORITY, BASE_PATH + "/#", CONTRIBUTIONS_ID);
|
uriMatcher.addURI(CONTRIBUTION_AUTHORITY, BASE_PATH + "/#", CONTRIBUTIONS_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Uri uriForId(int id) {
|
public static Uri uriForId(int id) {
|
||||||
return Uri.parse(BASE_URI.toString() + "/" + id);
|
return Uri.parse(BASE_URI.toString() + "/" + id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Inject DBOpenHelper dbOpenHelper;
|
||||||
public boolean onCreate() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
@SuppressWarnings("ConstantConditions")
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -50,8 +49,7 @@ public class ContributionsContentProvider extends ContentProvider {
|
||||||
|
|
||||||
int uriType = uriMatcher.match(uri);
|
int uriType = uriMatcher.match(uri);
|
||||||
|
|
||||||
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext();
|
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
|
||||||
SQLiteDatabase db = app.getDBOpenHelper().getReadableDatabase();
|
|
||||||
Cursor cursor;
|
Cursor cursor;
|
||||||
|
|
||||||
switch (uriType) {
|
switch (uriType) {
|
||||||
|
|
@ -87,8 +85,7 @@ public class ContributionsContentProvider extends ContentProvider {
|
||||||
@Override
|
@Override
|
||||||
public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
|
public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
|
||||||
int uriType = uriMatcher.match(uri);
|
int uriType = uriMatcher.match(uri);
|
||||||
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext();
|
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||||
SQLiteDatabase sqlDB = app.getDBOpenHelper().getWritableDatabase();
|
|
||||||
long id;
|
long id;
|
||||||
switch (uriType) {
|
switch (uriType) {
|
||||||
case CONTRIBUTIONS:
|
case CONTRIBUTIONS:
|
||||||
|
|
@ -107,13 +104,12 @@ public class ContributionsContentProvider extends ContentProvider {
|
||||||
int rows;
|
int rows;
|
||||||
int uriType = uriMatcher.match(uri);
|
int uriType = uriMatcher.match(uri);
|
||||||
|
|
||||||
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext();
|
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
|
||||||
SQLiteDatabase sqlDB = app.getDBOpenHelper().getWritableDatabase();
|
|
||||||
|
|
||||||
switch (uriType) {
|
switch (uriType) {
|
||||||
case CONTRIBUTIONS_ID:
|
case CONTRIBUTIONS_ID:
|
||||||
Timber.d("Deleting contribution id %s", uri.getLastPathSegment());
|
Timber.d("Deleting contribution id %s", uri.getLastPathSegment());
|
||||||
rows = sqlDB.delete(TABLE_NAME,
|
rows = db.delete(TABLE_NAME,
|
||||||
"_id = ?",
|
"_id = ?",
|
||||||
new String[]{uri.getLastPathSegment()}
|
new String[]{uri.getLastPathSegment()}
|
||||||
);
|
);
|
||||||
|
|
@ -130,8 +126,7 @@ public class ContributionsContentProvider extends ContentProvider {
|
||||||
public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
|
public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
|
||||||
Timber.d("Hello, bulk insert! (ContributionsContentProvider)");
|
Timber.d("Hello, bulk insert! (ContributionsContentProvider)");
|
||||||
int uriType = uriMatcher.match(uri);
|
int uriType = uriMatcher.match(uri);
|
||||||
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext();
|
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||||
SQLiteDatabase sqlDB = app.getDBOpenHelper().getWritableDatabase();
|
|
||||||
sqlDB.beginTransaction();
|
sqlDB.beginTransaction();
|
||||||
switch (uriType) {
|
switch (uriType) {
|
||||||
case CONTRIBUTIONS:
|
case CONTRIBUTIONS:
|
||||||
|
|
@ -162,8 +157,7 @@ public class ContributionsContentProvider extends ContentProvider {
|
||||||
error out otherwise.
|
error out otherwise.
|
||||||
*/
|
*/
|
||||||
int uriType = uriMatcher.match(uri);
|
int uriType = uriMatcher.match(uri);
|
||||||
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext();
|
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||||
SQLiteDatabase sqlDB = app.getDBOpenHelper().getWritableDatabase();
|
|
||||||
int rowsUpdated;
|
int rowsUpdated;
|
||||||
switch (uriType) {
|
switch (uriType) {
|
||||||
case CONTRIBUTIONS:
|
case CONTRIBUTIONS:
|
||||||
|
|
@ -175,7 +169,7 @@ public class ContributionsContentProvider extends ContentProvider {
|
||||||
if (TextUtils.isEmpty(selection)) {
|
if (TextUtils.isEmpty(selection)) {
|
||||||
rowsUpdated = sqlDB.update(TABLE_NAME,
|
rowsUpdated = sqlDB.update(TABLE_NAME,
|
||||||
contentValues,
|
contentValues,
|
||||||
Contribution.Table.COLUMN_ID + " = ?",
|
ContributionDao.Table.COLUMN_ID + " = ?",
|
||||||
new String[]{String.valueOf(id)});
|
new String[]{String.valueOf(id)});
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,11 @@ import fr.free.nrw.commons.R;
|
||||||
|
|
||||||
class ContributionsListAdapter extends CursorAdapter {
|
class ContributionsListAdapter extends CursorAdapter {
|
||||||
|
|
||||||
public ContributionsListAdapter(Context context, Cursor c, int flags) {
|
private final ContributionDao contributionDao;
|
||||||
|
|
||||||
|
public ContributionsListAdapter(Context context, Cursor c, int flags, ContributionDao contributionDao) {
|
||||||
super(context, c, flags);
|
super(context, c, flags);
|
||||||
|
this.contributionDao = contributionDao;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -26,7 +29,7 @@ class ContributionsListAdapter extends CursorAdapter {
|
||||||
@Override
|
@Override
|
||||||
public void bindView(View view, Context context, Cursor cursor) {
|
public void bindView(View view, Context context, Cursor cursor) {
|
||||||
final ContributionViewHolder views = (ContributionViewHolder)view.getTag();
|
final ContributionViewHolder views = (ContributionViewHolder)view.getTag();
|
||||||
final Contribution contribution = Contribution.fromCursor(cursor);
|
final Contribution contribution = contributionDao.fromCursor(cursor);
|
||||||
|
|
||||||
views.imageView.setMedia(contribution);
|
views.imageView.setMedia(contribution);
|
||||||
views.titleView.setText(contribution.getDisplayTitle());
|
views.titleView.setText(contribution.getDisplayTitle());
|
||||||
|
|
@ -34,7 +37,7 @@ class ContributionsListAdapter extends CursorAdapter {
|
||||||
views.seqNumView.setText(String.valueOf(cursor.getPosition() + 1));
|
views.seqNumView.setText(String.valueOf(cursor.getPosition() + 1));
|
||||||
views.seqNumView.setVisibility(View.VISIBLE);
|
views.seqNumView.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
switch(contribution.getState()) {
|
switch (contribution.getState()) {
|
||||||
case Contribution.STATE_COMPLETED:
|
case Contribution.STATE_COMPLETED:
|
||||||
views.stateView.setVisibility(View.GONE);
|
views.stateView.setVisibility(View.GONE);
|
||||||
views.progressView.setVisibility(View.GONE);
|
views.progressView.setVisibility(View.GONE);
|
||||||
|
|
@ -50,7 +53,7 @@ class ContributionsListAdapter extends CursorAdapter {
|
||||||
views.progressView.setVisibility(View.VISIBLE);
|
views.progressView.setVisibility(View.VISIBLE);
|
||||||
long total = contribution.getDataLength();
|
long total = contribution.getDataLength();
|
||||||
long transferred = contribution.getTransferred();
|
long transferred = contribution.getTransferred();
|
||||||
if(transferred == 0 || transferred >= total) {
|
if (transferred == 0 || transferred >= total) {
|
||||||
views.progressView.setIndeterminate(true);
|
views.progressView.setIndeterminate(true);
|
||||||
} else {
|
} else {
|
||||||
views.progressView.setProgress((int)(((double)transferred / (double)total) * 100));
|
views.progressView.setProgress((int)(((double)transferred / (double)total) * 100));
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,7 @@ import android.content.SharedPreferences;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.v4.app.Fragment;
|
|
||||||
import android.support.v4.content.ContextCompat;
|
import android.support.v4.content.ContextCompat;
|
||||||
import android.support.v7.app.AlertDialog;
|
import android.support.v7.app.AlertDialog;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
|
|
@ -19,30 +17,43 @@ import android.view.ViewGroup;
|
||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
import android.widget.GridView;
|
import android.widget.GridView;
|
||||||
import android.widget.ListAdapter;
|
import android.widget.ListAdapter;
|
||||||
|
import android.widget.ProgressBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
|
||||||
import butterknife.BindView;
|
import butterknife.BindView;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
|
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||||
import fr.free.nrw.commons.nearby.NearbyActivity;
|
import fr.free.nrw.commons.nearby.NearbyActivity;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
|
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
|
||||||
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
|
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
|
||||||
import static android.app.Activity.RESULT_OK;
|
import static android.app.Activity.RESULT_OK;
|
||||||
import static android.content.Context.MODE_PRIVATE;
|
|
||||||
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
|
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
|
||||||
import static android.view.View.GONE;
|
import static android.view.View.GONE;
|
||||||
|
|
||||||
public class ContributionsListFragment extends Fragment {
|
public class ContributionsListFragment extends CommonsDaggerSupportFragment {
|
||||||
|
|
||||||
@BindView(R.id.contributionsList)
|
@BindView(R.id.contributionsList)
|
||||||
GridView contributionsList;
|
GridView contributionsList;
|
||||||
@BindView(R.id.waitingMessage)
|
@BindView(R.id.waitingMessage)
|
||||||
TextView waitingMessage;
|
TextView waitingMessage;
|
||||||
@BindView(R.id.emptyMessage)
|
@BindView(R.id.loadingContributionsProgressBar)
|
||||||
TextView emptyMessage;
|
ProgressBar progressBar;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@Named("prefs")
|
||||||
|
SharedPreferences prefs;
|
||||||
|
@Inject
|
||||||
|
@Named("default_preferences")
|
||||||
|
SharedPreferences defaultPrefs;
|
||||||
|
|
||||||
private ContributionController controller;
|
private ContributionController controller;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -57,7 +68,6 @@ public class ContributionsListFragment extends Fragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: Should this be in onResume?
|
//TODO: Should this be in onResume?
|
||||||
SharedPreferences prefs = getActivity().getSharedPreferences("prefs", MODE_PRIVATE);
|
|
||||||
String lastModified = prefs.getString("lastSyncTimestamp", "");
|
String lastModified = prefs.getString("lastSyncTimestamp", "");
|
||||||
Timber.d("Last Sync Timestamp: %s", lastModified);
|
Timber.d("Last Sync Timestamp: %s", lastModified);
|
||||||
|
|
||||||
|
|
@ -67,6 +77,7 @@ public class ContributionsListFragment extends Fragment {
|
||||||
waitingMessage.setVisibility(GONE);
|
waitingMessage.setVisibility(GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
changeProgressBarVisibility(true);
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,6 +89,10 @@ public class ContributionsListFragment extends Fragment {
|
||||||
this.contributionsList.setAdapter(adapter);
|
this.contributionsList.setAdapter(adapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void changeProgressBarVisibility(boolean isVisible) {
|
||||||
|
this.progressBar.setVisibility(isVisible ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(Bundle outState) {
|
public void onSaveInstanceState(Bundle outState) {
|
||||||
if (outState == null) {
|
if (outState == null) {
|
||||||
|
|
@ -155,9 +170,7 @@ public class ContributionsListFragment extends Fragment {
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
case R.id.menu_from_camera:
|
case R.id.menu_from_camera:
|
||||||
SharedPreferences sharedPref = PreferenceManager
|
boolean useExtStorage = defaultPrefs.getBoolean("useExternalStorage", true);
|
||||||
.getDefaultSharedPreferences(CommonsApplication.getInstance());
|
|
||||||
boolean useExtStorage = sharedPref.getBoolean("useExternalStorage", true);
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && useExtStorage) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && useExtStorage) {
|
||||||
// Here, thisActivity is the current activity
|
// Here, thisActivity is the current activity
|
||||||
if (ContextCompat.checkSelfPermission(getActivity(), WRITE_EXTERNAL_STORAGE)
|
if (ContextCompat.checkSelfPermission(getActivity(), WRITE_EXTERNAL_STORAGE)
|
||||||
|
|
@ -201,7 +214,7 @@ public class ContributionsListFragment extends Fragment {
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
|
||||||
@NonNull int[] grantResults) {
|
@NonNull int[] grantResults) {
|
||||||
Timber.d("onRequestPermissionsResult: req code = " + " perm = "
|
Timber.d("onRequestPermissionsResult: req code = " + " perm = "
|
||||||
+ permissions + " grant =" + grantResults);
|
+ Arrays.toString(permissions) + " grant =" + Arrays.toString(grantResults));
|
||||||
|
|
||||||
switch (requestCode) {
|
switch (requestCode) {
|
||||||
// 1 = Storage allowed when gallery selected
|
// 1 = Storage allowed when gallery selected
|
||||||
|
|
@ -235,12 +248,17 @@ public class ContributionsListFragment extends Fragment {
|
||||||
menu.clear(); // See http://stackoverflow.com/a/8495697/17865
|
menu.clear(); // See http://stackoverflow.com/a/8495697/17865
|
||||||
inflater.inflate(R.menu.fragment_contributions_list, menu);
|
inflater.inflate(R.menu.fragment_contributions_list, menu);
|
||||||
|
|
||||||
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext();
|
if (!deviceHasCamera()) {
|
||||||
if (!app.deviceHasCamera()) {
|
|
||||||
menu.findItem(R.id.menu_from_camera).setEnabled(false);
|
menu.findItem(R.id.menu_from_camera).setEnabled(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean deviceHasCamera() {
|
||||||
|
PackageManager pm = getContext().getPackageManager();
|
||||||
|
return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA) ||
|
||||||
|
pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
|
||||||
|
|
@ -13,21 +13,27 @@ import android.os.RemoteException;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
import fr.free.nrw.commons.Utils;
|
import fr.free.nrw.commons.Utils;
|
||||||
|
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||||
import fr.free.nrw.commons.mwapi.LogEventResult;
|
import fr.free.nrw.commons.mwapi.LogEventResult;
|
||||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
import static android.content.Context.MODE_PRIVATE;
|
|
||||||
import static fr.free.nrw.commons.contributions.Contribution.STATE_COMPLETED;
|
import static fr.free.nrw.commons.contributions.Contribution.STATE_COMPLETED;
|
||||||
import static fr.free.nrw.commons.contributions.Contribution.Table.COLUMN_FILENAME;
|
import static fr.free.nrw.commons.contributions.ContributionDao.Table.COLUMN_FILENAME;
|
||||||
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI;
|
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI;
|
||||||
|
|
||||||
|
@SuppressWarnings("WeakerAccess")
|
||||||
public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
|
public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||||
|
|
||||||
private static final String[] existsQuery = {COLUMN_FILENAME};
|
private static final String[] existsQuery = {COLUMN_FILENAME};
|
||||||
|
|
@ -35,6 +41,10 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||||
private static final ContentValues[] EMPTY = {};
|
private static final ContentValues[] EMPTY = {};
|
||||||
private static int COMMIT_THRESHOLD = 10;
|
private static int COMMIT_THRESHOLD = 10;
|
||||||
|
|
||||||
|
@SuppressWarnings("WeakerAccess")
|
||||||
|
@Inject MediaWikiApi mwApi;
|
||||||
|
@Inject @Named("prefs") SharedPreferences prefs;
|
||||||
|
|
||||||
public ContributionsSyncAdapter(Context context, boolean autoInitialize) {
|
public ContributionsSyncAdapter(Context context, boolean autoInitialize) {
|
||||||
super(context, autoInitialize);
|
super(context, autoInitialize);
|
||||||
}
|
}
|
||||||
|
|
@ -47,6 +57,9 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean fileExists(ContentProviderClient client, String filename) {
|
private boolean fileExists(ContentProviderClient client, String filename) {
|
||||||
|
if (filename == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
Cursor cursor = null;
|
Cursor cursor = null;
|
||||||
try {
|
try {
|
||||||
cursor = client.query(BASE_URI,
|
cursor = client.query(BASE_URI,
|
||||||
|
|
@ -68,19 +81,23 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||||
@Override
|
@Override
|
||||||
public void onPerformSync(Account account, Bundle bundle, String authority,
|
public void onPerformSync(Account account, Bundle bundle, String authority,
|
||||||
ContentProviderClient contentProviderClient, SyncResult syncResult) {
|
ContentProviderClient contentProviderClient, SyncResult syncResult) {
|
||||||
|
ApplicationlessInjection
|
||||||
|
.getInstance(getContext()
|
||||||
|
.getApplicationContext())
|
||||||
|
.getCommonsApplicationComponent()
|
||||||
|
.inject(this);
|
||||||
// This code is fraught with possibilities of race conditions, but lalalalala I can't hear you!
|
// This code is fraught with possibilities of race conditions, but lalalalala I can't hear you!
|
||||||
String user = account.name;
|
String user = account.name;
|
||||||
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
|
|
||||||
SharedPreferences prefs = getContext().getSharedPreferences("prefs", MODE_PRIVATE);
|
|
||||||
String lastModified = prefs.getString("lastSyncTimestamp", "");
|
String lastModified = prefs.getString("lastSyncTimestamp", "");
|
||||||
Date curTime = new Date();
|
Date curTime = new Date();
|
||||||
LogEventResult result;
|
LogEventResult result;
|
||||||
Boolean done = false;
|
Boolean done = false;
|
||||||
String queryContinue = null;
|
String queryContinue = null;
|
||||||
|
ContributionDao contributionDao = new ContributionDao(() -> contentProviderClient);
|
||||||
while (!done) {
|
while (!done) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
result = api.logEvents(user, lastModified, queryContinue, getLimit());
|
result = mwApi.logEvents(user, lastModified, queryContinue, getLimit());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// There isn't really much we can do, eh?
|
// There isn't really much we can do, eh?
|
||||||
// FIXME: Perhaps add EventLogging?
|
// FIXME: Perhaps add EventLogging?
|
||||||
|
|
@ -109,7 +126,7 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||||
"", -1, dateUpdated, dateUpdated, user,
|
"", -1, dateUpdated, dateUpdated, user,
|
||||||
"", "");
|
"", "");
|
||||||
contrib.setState(STATE_COMPLETED);
|
contrib.setState(STATE_COMPLETED);
|
||||||
imageValues.add(contrib.toContentValues());
|
imageValues.add(contributionDao.toContentValues(contrib));
|
||||||
|
|
||||||
if (imageValues.size() % COMMIT_THRESHOLD == 0) {
|
if (imageValues.size() % COMMIT_THRESHOLD == 0) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -134,8 +151,13 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||||
done = true;
|
done = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
prefs.edit().putString("lastSyncTimestamp", Utils.toMWDate(curTime)).apply();
|
prefs.edit().putString("lastSyncTimestamp", toMWDate(curTime)).apply();
|
||||||
Timber.d("Oh hai, everyone! Look, a kitty!");
|
Timber.d("Oh hai, everyone! Look, a kitty!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String toMWDate(Date date) {
|
||||||
|
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH); // Assuming MW always gives me UTC
|
||||||
|
isoFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||||
|
return isoFormat.format(date);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,18 @@ import android.content.Context;
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
import android.database.sqlite.SQLiteOpenHelper;
|
import android.database.sqlite.SQLiteOpenHelper;
|
||||||
|
|
||||||
import fr.free.nrw.commons.contributions.Contribution;
|
import fr.free.nrw.commons.category.CategoryDao;
|
||||||
import fr.free.nrw.commons.modifications.ModifierSequence;
|
import fr.free.nrw.commons.contributions.ContributionDao;
|
||||||
|
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
|
||||||
|
|
||||||
public class DBOpenHelper extends SQLiteOpenHelper{
|
public class DBOpenHelper extends SQLiteOpenHelper {
|
||||||
|
|
||||||
private static final String DATABASE_NAME = "commons.db";
|
private static final String DATABASE_NAME = "commons.db";
|
||||||
private static final int DATABASE_VERSION = 6;
|
private static final int DATABASE_VERSION = 6;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Do not use, please call CommonsApplication.getDBOpenHelper()
|
* Do not use directly - @Inject an instance where it's needed and let
|
||||||
|
* dependency injection take care of managing this as a singleton.
|
||||||
*/
|
*/
|
||||||
public DBOpenHelper(Context context) {
|
public DBOpenHelper(Context context) {
|
||||||
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
||||||
|
|
@ -21,15 +23,15 @@ public class DBOpenHelper extends SQLiteOpenHelper{
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(SQLiteDatabase sqLiteDatabase) {
|
public void onCreate(SQLiteDatabase sqLiteDatabase) {
|
||||||
Contribution.Table.onCreate(sqLiteDatabase);
|
ContributionDao.Table.onCreate(sqLiteDatabase);
|
||||||
ModifierSequence.Table.onCreate(sqLiteDatabase);
|
ModifierSequenceDao.Table.onCreate(sqLiteDatabase);
|
||||||
Category.Table.onCreate(sqLiteDatabase);
|
CategoryDao.Table.onCreate(sqLiteDatabase);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int from, int to) {
|
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int from, int to) {
|
||||||
Contribution.Table.onUpdate(sqLiteDatabase, from, to);
|
ContributionDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||||
ModifierSequence.Table.onUpdate(sqLiteDatabase, from, to);
|
ModifierSequenceDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||||
Category.Table.onUpdate(sqLiteDatabase, from, to);
|
CategoryDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
package fr.free.nrw.commons.di;
|
||||||
|
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.android.ContributesAndroidInjector;
|
||||||
|
import fr.free.nrw.commons.AboutActivity;
|
||||||
|
import fr.free.nrw.commons.WelcomeActivity;
|
||||||
|
import fr.free.nrw.commons.auth.LoginActivity;
|
||||||
|
import fr.free.nrw.commons.auth.SignupActivity;
|
||||||
|
import fr.free.nrw.commons.contributions.ContributionsActivity;
|
||||||
|
import fr.free.nrw.commons.nearby.NearbyActivity;
|
||||||
|
import fr.free.nrw.commons.notification.NotificationActivity;
|
||||||
|
import fr.free.nrw.commons.settings.SettingsActivity;
|
||||||
|
import fr.free.nrw.commons.upload.MultipleShareActivity;
|
||||||
|
import fr.free.nrw.commons.upload.ShareActivity;
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||||
|
public abstract class ActivityBuilderModule {
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract LoginActivity bindLoginActivity();
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract WelcomeActivity bindWelcomeActivity();
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract ShareActivity bindShareActivity();
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract MultipleShareActivity bindMultipleShareActivity();
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract ContributionsActivity bindContributionsActivity();
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract SettingsActivity bindSettingsActivity();
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract AboutActivity bindAboutActivity();
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract SignupActivity bindSignupActivity();
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract NearbyActivity bindNearbyActivity();
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract NotificationActivity bindNotificationActivity();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
package fr.free.nrw.commons.di;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.ContentProvider;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.support.v4.app.Fragment;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import dagger.android.AndroidInjector;
|
||||||
|
import dagger.android.DispatchingAndroidInjector;
|
||||||
|
import dagger.android.HasActivityInjector;
|
||||||
|
import dagger.android.HasBroadcastReceiverInjector;
|
||||||
|
import dagger.android.HasContentProviderInjector;
|
||||||
|
import dagger.android.HasFragmentInjector;
|
||||||
|
import dagger.android.HasServiceInjector;
|
||||||
|
import dagger.android.support.HasSupportFragmentInjector;
|
||||||
|
|
||||||
|
public class ApplicationlessInjection
|
||||||
|
implements
|
||||||
|
HasActivityInjector,
|
||||||
|
HasFragmentInjector,
|
||||||
|
HasSupportFragmentInjector,
|
||||||
|
HasServiceInjector,
|
||||||
|
HasBroadcastReceiverInjector,
|
||||||
|
HasContentProviderInjector {
|
||||||
|
|
||||||
|
private static ApplicationlessInjection instance = null;
|
||||||
|
|
||||||
|
@Inject DispatchingAndroidInjector<Activity> activityInjector;
|
||||||
|
@Inject DispatchingAndroidInjector<BroadcastReceiver> broadcastReceiverInjector;
|
||||||
|
@Inject DispatchingAndroidInjector<android.app.Fragment> fragmentInjector;
|
||||||
|
@Inject DispatchingAndroidInjector<Fragment> supportFragmentInjector;
|
||||||
|
@Inject DispatchingAndroidInjector<Service> serviceInjector;
|
||||||
|
@Inject DispatchingAndroidInjector<ContentProvider> contentProviderInjector;
|
||||||
|
|
||||||
|
private CommonsApplicationComponent commonsApplicationComponent;
|
||||||
|
|
||||||
|
public ApplicationlessInjection(Context applicationContext) {
|
||||||
|
commonsApplicationComponent = DaggerCommonsApplicationComponent.builder()
|
||||||
|
.appModule(new CommonsApplicationModule(applicationContext)).build();
|
||||||
|
commonsApplicationComponent.inject(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DispatchingAndroidInjector<Activity> activityInjector() {
|
||||||
|
return activityInjector;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DispatchingAndroidInjector<android.app.Fragment> fragmentInjector() {
|
||||||
|
return fragmentInjector;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DispatchingAndroidInjector<Fragment> supportFragmentInjector() {
|
||||||
|
return supportFragmentInjector;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DispatchingAndroidInjector<BroadcastReceiver> broadcastReceiverInjector() {
|
||||||
|
return broadcastReceiverInjector;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DispatchingAndroidInjector<Service> serviceInjector() {
|
||||||
|
return serviceInjector;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AndroidInjector<ContentProvider> contentProviderInjector() {
|
||||||
|
return contentProviderInjector;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommonsApplicationComponent getCommonsApplicationComponent() {
|
||||||
|
return commonsApplicationComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ApplicationlessInjection getInstance(Context applicationContext) {
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized (ApplicationlessInjection.class) {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new ApplicationlessInjection(applicationContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
package fr.free.nrw.commons.di;
|
||||||
|
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import dagger.Component;
|
||||||
|
import dagger.android.AndroidInjectionModule;
|
||||||
|
import dagger.android.AndroidInjector;
|
||||||
|
import dagger.android.support.AndroidSupportInjectionModule;
|
||||||
|
import fr.free.nrw.commons.CommonsApplication;
|
||||||
|
import fr.free.nrw.commons.MediaWikiImageView;
|
||||||
|
import fr.free.nrw.commons.auth.LoginActivity;
|
||||||
|
import fr.free.nrw.commons.contributions.ContributionsSyncAdapter;
|
||||||
|
import fr.free.nrw.commons.modifications.ModificationsSyncAdapter;
|
||||||
|
import fr.free.nrw.commons.settings.SettingsFragment;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Component(modules = {
|
||||||
|
CommonsApplicationModule.class,
|
||||||
|
AndroidInjectionModule.class,
|
||||||
|
AndroidSupportInjectionModule.class,
|
||||||
|
ActivityBuilderModule.class,
|
||||||
|
FragmentBuilderModule.class,
|
||||||
|
ServiceBuilderModule.class,
|
||||||
|
ContentProviderBuilderModule.class
|
||||||
|
})
|
||||||
|
public interface CommonsApplicationComponent extends AndroidInjector<ApplicationlessInjection> {
|
||||||
|
void inject(CommonsApplication application);
|
||||||
|
|
||||||
|
void inject(ContributionsSyncAdapter syncAdapter);
|
||||||
|
|
||||||
|
void inject(ModificationsSyncAdapter syncAdapter);
|
||||||
|
|
||||||
|
void inject(MediaWikiImageView mediaWikiImageView);
|
||||||
|
|
||||||
|
void inject(LoginActivity activity);
|
||||||
|
|
||||||
|
void inject(SettingsFragment fragment);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void inject(ApplicationlessInjection instance);
|
||||||
|
|
||||||
|
@Component.Builder
|
||||||
|
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||||
|
interface Builder {
|
||||||
|
Builder appModule(CommonsApplicationModule applicationModule);
|
||||||
|
|
||||||
|
CommonsApplicationComponent build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,139 @@
|
||||||
|
package fr.free.nrw.commons.di;
|
||||||
|
|
||||||
|
import android.content.ContentProviderClient;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.preference.PreferenceManager;
|
||||||
|
import android.support.v4.util.LruCache;
|
||||||
|
|
||||||
|
import javax.inject.Named;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.Provides;
|
||||||
|
import fr.free.nrw.commons.BuildConfig;
|
||||||
|
import fr.free.nrw.commons.CommonsApplication;
|
||||||
|
import fr.free.nrw.commons.auth.AccountUtil;
|
||||||
|
import fr.free.nrw.commons.auth.SessionManager;
|
||||||
|
import fr.free.nrw.commons.caching.CacheController;
|
||||||
|
import fr.free.nrw.commons.data.DBOpenHelper;
|
||||||
|
import fr.free.nrw.commons.location.LocationServiceManager;
|
||||||
|
import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi;
|
||||||
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
|
import fr.free.nrw.commons.nearby.NearbyPlaces;
|
||||||
|
import fr.free.nrw.commons.upload.UploadController;
|
||||||
|
|
||||||
|
import static android.content.Context.MODE_PRIVATE;
|
||||||
|
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.CONTRIBUTION_AUTHORITY;
|
||||||
|
import static fr.free.nrw.commons.modifications.ModificationsContentProvider.MODIFICATIONS_AUTHORITY;
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||||
|
public class CommonsApplicationModule {
|
||||||
|
public static final String CATEGORY_AUTHORITY = "fr.free.nrw.commons.categories.contentprovider";
|
||||||
|
public static final long OK_HTTP_CACHE_SIZE = 10 * 1024 * 1024;
|
||||||
|
|
||||||
|
private CommonsApplication application;
|
||||||
|
private Context applicationContext;
|
||||||
|
|
||||||
|
public CommonsApplicationModule(Context applicationContext) {
|
||||||
|
this.applicationContext = applicationContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
public Context providesApplicationContext() {
|
||||||
|
return this.applicationContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
public AccountUtil providesAccountUtil(Context context) {
|
||||||
|
return new AccountUtil(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Named("category")
|
||||||
|
public ContentProviderClient provideCategoryContentProviderClient(Context context) {
|
||||||
|
return context.getContentResolver().acquireContentProviderClient(CATEGORY_AUTHORITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Named("contribution")
|
||||||
|
public ContentProviderClient provideContributionContentProviderClient(Context context) {
|
||||||
|
return context.getContentResolver().acquireContentProviderClient(CONTRIBUTION_AUTHORITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Named("modification")
|
||||||
|
public ContentProviderClient provideModificationContentProviderClient(Context context) {
|
||||||
|
return context.getContentResolver().acquireContentProviderClient(MODIFICATIONS_AUTHORITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Named("application_preferences")
|
||||||
|
public SharedPreferences providesApplicationSharedPreferences(Context context) {
|
||||||
|
return context.getSharedPreferences("fr.free.nrw.commons", MODE_PRIVATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Named("default_preferences")
|
||||||
|
public SharedPreferences providesDefaultSharedPreferences(Context context) {
|
||||||
|
return PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Named("prefs")
|
||||||
|
public SharedPreferences providesOtherSharedPreferences(Context context) {
|
||||||
|
return context.getSharedPreferences("prefs", MODE_PRIVATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
public UploadController providesUploadController(Context context,
|
||||||
|
SessionManager sessionManager,
|
||||||
|
@Named("default_preferences") SharedPreferences sharedPreferences) {
|
||||||
|
return new UploadController(sessionManager, context, sharedPreferences);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
public SessionManager providesSessionManager(Context context,
|
||||||
|
MediaWikiApi mediaWikiApi,
|
||||||
|
@Named("default_preferences") SharedPreferences sharedPreferences) {
|
||||||
|
return new SessionManager(context, mediaWikiApi, sharedPreferences);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
public MediaWikiApi provideMediaWikiApi(Context context, @Named("default_preferences") SharedPreferences sharedPreferences) {
|
||||||
|
return new ApacheHttpClientMediaWikiApi(context, BuildConfig.WIKIMEDIA_API_HOST, sharedPreferences);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
public LocationServiceManager provideLocationServiceManager(Context context) {
|
||||||
|
return new LocationServiceManager(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
public CacheController provideCacheController() {
|
||||||
|
return new CacheController();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
public DBOpenHelper provideDBOpenHelper(Context context) {
|
||||||
|
return new DBOpenHelper(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
public NearbyPlaces provideNearbyPlaces() {
|
||||||
|
return new NearbyPlaces();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
public LruCache<String, String> provideLruCache() {
|
||||||
|
return new LruCache<>(1024);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
package fr.free.nrw.commons.di;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.v4.app.Fragment;
|
||||||
|
import android.support.v7.app.AppCompatActivity;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import dagger.android.AndroidInjector;
|
||||||
|
import dagger.android.DispatchingAndroidInjector;
|
||||||
|
import dagger.android.support.HasSupportFragmentInjector;
|
||||||
|
|
||||||
|
public abstract class CommonsDaggerAppCompatActivity extends AppCompatActivity implements HasSupportFragmentInjector {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
DispatchingAndroidInjector<Fragment> supportFragmentInjector;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
inject();
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AndroidInjector<Fragment> supportFragmentInjector() {
|
||||||
|
return supportFragmentInjector;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void inject() {
|
||||||
|
ApplicationlessInjection injection = ApplicationlessInjection.getInstance(getApplicationContext());
|
||||||
|
|
||||||
|
AndroidInjector<Activity> activityInjector = injection.activityInjector();
|
||||||
|
|
||||||
|
if (activityInjector == null) {
|
||||||
|
throw new NullPointerException("ApplicationlessInjection.activityInjector() returned null");
|
||||||
|
}
|
||||||
|
|
||||||
|
activityInjector.inject(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package fr.free.nrw.commons.di;
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
|
||||||
|
import dagger.android.AndroidInjector;
|
||||||
|
|
||||||
|
public abstract class CommonsDaggerBroadcastReceiver extends BroadcastReceiver {
|
||||||
|
|
||||||
|
public CommonsDaggerBroadcastReceiver() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
inject(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void inject(Context context) {
|
||||||
|
ApplicationlessInjection injection = ApplicationlessInjection.getInstance(context.getApplicationContext());
|
||||||
|
|
||||||
|
AndroidInjector<BroadcastReceiver> serviceInjector = injection.broadcastReceiverInjector();
|
||||||
|
|
||||||
|
if (serviceInjector == null) {
|
||||||
|
throw new NullPointerException("ApplicationlessInjection.broadcastReceiverInjector() returned null");
|
||||||
|
}
|
||||||
|
serviceInjector.inject(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
package fr.free.nrw.commons.di;
|
||||||
|
|
||||||
|
import android.content.ContentProvider;
|
||||||
|
|
||||||
|
import dagger.android.AndroidInjector;
|
||||||
|
|
||||||
|
|
||||||
|
public abstract class CommonsDaggerContentProvider extends ContentProvider {
|
||||||
|
|
||||||
|
public CommonsDaggerContentProvider() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreate() {
|
||||||
|
inject();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void inject() {
|
||||||
|
ApplicationlessInjection injection = ApplicationlessInjection.getInstance(getContext());
|
||||||
|
|
||||||
|
AndroidInjector<ContentProvider> serviceInjector = injection.contentProviderInjector();
|
||||||
|
|
||||||
|
if (serviceInjector == null) {
|
||||||
|
throw new NullPointerException("ApplicationlessInjection.contentProviderInjector() returned null");
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceInjector.inject(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
package fr.free.nrw.commons.di;
|
||||||
|
|
||||||
|
import android.app.IntentService;
|
||||||
|
import android.app.Service;
|
||||||
|
|
||||||
|
import dagger.android.AndroidInjector;
|
||||||
|
|
||||||
|
public abstract class CommonsDaggerIntentService extends IntentService {
|
||||||
|
|
||||||
|
public CommonsDaggerIntentService(String name) {
|
||||||
|
super(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
inject();
|
||||||
|
super.onCreate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void inject() {
|
||||||
|
ApplicationlessInjection injection = ApplicationlessInjection.getInstance(getApplicationContext());
|
||||||
|
|
||||||
|
AndroidInjector<Service> serviceInjector = injection.serviceInjector();
|
||||||
|
|
||||||
|
if (serviceInjector == null) {
|
||||||
|
throw new NullPointerException("ApplicationlessInjection.serviceInjector() returned null");
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceInjector.inject(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package fr.free.nrw.commons.di;
|
||||||
|
|
||||||
|
import android.app.Service;
|
||||||
|
|
||||||
|
import dagger.android.AndroidInjector;
|
||||||
|
|
||||||
|
public abstract class CommonsDaggerService extends Service {
|
||||||
|
|
||||||
|
public CommonsDaggerService() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
inject();
|
||||||
|
super.onCreate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void inject() {
|
||||||
|
ApplicationlessInjection injection = ApplicationlessInjection.getInstance(getApplicationContext());
|
||||||
|
|
||||||
|
AndroidInjector<Service> serviceInjector = injection.serviceInjector();
|
||||||
|
|
||||||
|
if (serviceInjector == null) {
|
||||||
|
throw new NullPointerException("ApplicationlessInjection.serviceInjector() returned null");
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceInjector.inject(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
package fr.free.nrw.commons.di;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.support.v4.app.Fragment;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import dagger.android.AndroidInjector;
|
||||||
|
import dagger.android.DispatchingAndroidInjector;
|
||||||
|
import dagger.android.support.HasSupportFragmentInjector;
|
||||||
|
|
||||||
|
public abstract class CommonsDaggerSupportFragment extends Fragment implements HasSupportFragmentInjector {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
DispatchingAndroidInjector<Fragment> childFragmentInjector;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttach(Context context) {
|
||||||
|
inject();
|
||||||
|
super.onAttach(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AndroidInjector<Fragment> supportFragmentInjector() {
|
||||||
|
return childFragmentInjector;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void inject() {
|
||||||
|
HasSupportFragmentInjector hasSupportFragmentInjector = findHasFragmentInjector();
|
||||||
|
|
||||||
|
AndroidInjector<Fragment> fragmentInjector = hasSupportFragmentInjector.supportFragmentInjector();
|
||||||
|
|
||||||
|
if (fragmentInjector == null) {
|
||||||
|
throw new NullPointerException(String.format("%s.supportFragmentInjector() returned null", hasSupportFragmentInjector.getClass().getCanonicalName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fragmentInjector.inject(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HasSupportFragmentInjector findHasFragmentInjector() {
|
||||||
|
Fragment parentFragment = this;
|
||||||
|
|
||||||
|
while ((parentFragment = parentFragment.getParentFragment()) != null) {
|
||||||
|
if (parentFragment instanceof HasSupportFragmentInjector) {
|
||||||
|
return (HasSupportFragmentInjector) parentFragment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Activity activity = getActivity();
|
||||||
|
|
||||||
|
if (activity instanceof HasSupportFragmentInjector) {
|
||||||
|
return (HasSupportFragmentInjector) activity;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplicationlessInjection injection = ApplicationlessInjection.getInstance(activity.getApplicationContext());
|
||||||
|
if (injection != null) {
|
||||||
|
return injection;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException(String.format("No injector was found for %s", getClass().getCanonicalName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package fr.free.nrw.commons.di;
|
||||||
|
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.android.ContributesAndroidInjector;
|
||||||
|
import fr.free.nrw.commons.category.CategoryContentProvider;
|
||||||
|
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
|
||||||
|
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||||
|
public abstract class ContentProviderBuilderModule {
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract ContributionsContentProvider bindContributionsContentProvider();
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract ModificationsContentProvider bindModificationsContentProvider();
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract CategoryContentProvider bindCategoryContentProvider();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
package fr.free.nrw.commons.di;
|
||||||
|
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.android.ContributesAndroidInjector;
|
||||||
|
import fr.free.nrw.commons.category.CategorizationFragment;
|
||||||
|
import fr.free.nrw.commons.contributions.ContributionsListFragment;
|
||||||
|
import fr.free.nrw.commons.media.MediaDetailFragment;
|
||||||
|
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
||||||
|
import fr.free.nrw.commons.nearby.NearbyListFragment;
|
||||||
|
import fr.free.nrw.commons.nearby.NoPermissionsFragment;
|
||||||
|
import fr.free.nrw.commons.settings.SettingsFragment;
|
||||||
|
import fr.free.nrw.commons.upload.MultipleUploadListFragment;
|
||||||
|
import fr.free.nrw.commons.upload.SingleUploadFragment;
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||||
|
public abstract class FragmentBuilderModule {
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract CategorizationFragment bindCategorizationFragment();
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract ContributionsListFragment bindContributionsListFragment();
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract MediaDetailFragment bindMediaDetailFragment();
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract MediaDetailPagerFragment bindMediaDetailPagerFragment();
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract NearbyListFragment bindNearbyListFragment();
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract NoPermissionsFragment bindNoPermissionsFragment();
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract SettingsFragment bindSettingsFragment();
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract MultipleUploadListFragment bindMultipleUploadListFragment();
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract SingleUploadFragment bindSingleUploadFragment();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package fr.free.nrw.commons.di;
|
||||||
|
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.android.ContributesAndroidInjector;
|
||||||
|
import fr.free.nrw.commons.auth.WikiAccountAuthenticatorService;
|
||||||
|
import fr.free.nrw.commons.upload.UploadService;
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||||
|
public abstract class ServiceBuilderModule {
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract UploadService bindUploadService();
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract WikiAccountAuthenticatorService bindWikiAccountAuthenticatorService();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,19 +1,32 @@
|
||||||
package fr.free.nrw.commons.location;
|
package fr.free.nrw.commons.location;
|
||||||
|
|
||||||
|
import android.location.Location;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a latitude and longitude point with accuracy information, often of a picture
|
||||||
|
*/
|
||||||
public class LatLng {
|
public class LatLng {
|
||||||
|
|
||||||
private final double latitude;
|
private final double latitude;
|
||||||
private final double longitude;
|
private final double longitude;
|
||||||
private final float accuracy;
|
private final float accuracy;
|
||||||
|
|
||||||
/** Accepts latitude and longitude.
|
/**
|
||||||
|
* Accepts latitude and longitude.
|
||||||
* North and South values are cut off at 90°
|
* North and South values are cut off at 90°
|
||||||
*
|
*
|
||||||
* @param latitude double value
|
* @param latitude the latitude
|
||||||
* @param longitude double value
|
* @param longitude the longitude
|
||||||
|
* @param accuracy the accuracy
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* the Statue of Liberty is located at 40.69° N, 74.04° W
|
||||||
|
* The Statue of Liberty could be constructed as LatLng(40.69, -74.04, 1.0)
|
||||||
|
* where positive signifies north, east and negative signifies south, west.
|
||||||
*/
|
*/
|
||||||
public LatLng(double latitude, double longitude, float accuracy) {
|
public LatLng(double latitude, double longitude, float accuracy) {
|
||||||
if(-180.0D <= longitude && longitude < 180.0D) {
|
if (-180.0D <= longitude && longitude < 180.0D) {
|
||||||
this.longitude = longitude;
|
this.longitude = longitude;
|
||||||
} else {
|
} else {
|
||||||
this.longitude = ((longitude - 180.0D) % 360.0D + 360.0D) % 360.0D - 180.0D;
|
this.longitude = ((longitude - 180.0D) % 360.0D + 360.0D) % 360.0D - 180.0D;
|
||||||
|
|
@ -22,20 +35,35 @@ public class LatLng {
|
||||||
this.accuracy = accuracy;
|
this.accuracy = accuracy;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int hashCode() {
|
/**
|
||||||
boolean var1 = true;
|
* gets the latitude and longitude of a given non-null location
|
||||||
byte var2 = 1;
|
* @param location the non-null location of the user
|
||||||
long var3 = Double.doubleToLongBits(this.latitude);
|
* @return LatLng the Latitude and Longitude of a given location
|
||||||
int var5 = 31 * var2 + (int)(var3 ^ var3 >>> 32);
|
*/
|
||||||
var3 = Double.doubleToLongBits(this.longitude);
|
public static LatLng from(@NonNull Location location) {
|
||||||
var5 = 31 * var5 + (int)(var3 ^ var3 >>> 32);
|
return new LatLng(location.getLatitude(), location.getLongitude(), location.getAccuracy());
|
||||||
return var5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* creates a hash code for the longitude and longitude
|
||||||
|
*/
|
||||||
|
public int hashCode() {
|
||||||
|
byte var1 = 1;
|
||||||
|
long var2 = Double.doubleToLongBits(this.latitude);
|
||||||
|
int var3 = 31 * var1 + (int)(var2 ^ var2 >>> 32);
|
||||||
|
var2 = Double.doubleToLongBits(this.longitude);
|
||||||
|
var3 = 31 * var3 + (int)(var2 ^ var2 >>> 32);
|
||||||
|
return var3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks for equality of two LatLng objects
|
||||||
|
* @param o the second LatLng object
|
||||||
|
*/
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if(this == o) {
|
if (this == o) {
|
||||||
return true;
|
return true;
|
||||||
} else if(!(o instanceof LatLng)) {
|
} else if (!(o instanceof LatLng)) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
LatLng var2 = (LatLng)o;
|
LatLng var2 = (LatLng)o;
|
||||||
|
|
@ -43,6 +71,9 @@ public class LatLng {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns a string representation of the latitude and longitude
|
||||||
|
*/
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "lat/lng: (" + this.latitude + "," + this.longitude + ")";
|
return "lat/lng: (" + this.latitude + "," + this.longitude + ")";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,62 +1,184 @@
|
||||||
package fr.free.nrw.commons.location;
|
package fr.free.nrw.commons.location;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.location.Criteria;
|
import android.content.pm.PackageManager;
|
||||||
import android.location.Location;
|
import android.location.Location;
|
||||||
import android.location.LocationListener;
|
import android.location.LocationListener;
|
||||||
import android.location.LocationManager;
|
import android.location.LocationManager;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.support.v4.app.ActivityCompat;
|
||||||
|
import android.support.v4.content.ContextCompat;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
public class LocationServiceManager implements LocationListener {
|
public class LocationServiceManager implements LocationListener {
|
||||||
|
public static final int LOCATION_REQUEST = 1;
|
||||||
|
|
||||||
private String provider;
|
private static final long MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS = 2 * 60 * 1000;
|
||||||
|
private static final long MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS = 10;
|
||||||
|
|
||||||
|
private Context context;
|
||||||
private LocationManager locationManager;
|
private LocationManager locationManager;
|
||||||
private LatLng latestLocation;
|
private Location lastLocation;
|
||||||
private Float latestLocationAccuracy;
|
private final List<LocationUpdateListener> locationListeners = new CopyOnWriteArrayList<>();
|
||||||
|
private boolean isLocationManagerRegistered = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of LocationServiceManager.
|
||||||
|
*
|
||||||
|
* @param context the context
|
||||||
|
*/
|
||||||
public LocationServiceManager(Context context) {
|
public LocationServiceManager(Context context) {
|
||||||
|
this.context = context;
|
||||||
this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
|
this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
|
||||||
provider = locationManager.getBestProvider(new Criteria(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public LatLng getLatestLocation() {
|
|
||||||
return latestLocation;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the accuracy of the location. The measurement is
|
* Returns the current status of the GPS provider.
|
||||||
* given as a radius in meter of 68 % confidence.
|
|
||||||
*
|
*
|
||||||
* @return Float
|
* @return true if the GPS provider is enabled
|
||||||
*/
|
*/
|
||||||
public Float getLatestLocationAccuracy() {
|
public boolean isProviderEnabled() {
|
||||||
return latestLocationAccuracy;
|
return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Registers a LocationManager to listen for current location.
|
/**
|
||||||
|
* Returns whether the location permission is granted.
|
||||||
|
*
|
||||||
|
* @return true if the location permission is granted
|
||||||
|
*/
|
||||||
|
public boolean isLocationPermissionGranted() {
|
||||||
|
return ContextCompat.checkSelfPermission(context,
|
||||||
|
Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests the location permission to be granted.
|
||||||
|
*
|
||||||
|
* @param activity the activity
|
||||||
|
*/
|
||||||
|
public void requestPermissions(Activity activity) {
|
||||||
|
if (activity.isFinishing()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ActivityCompat.requestPermissions(activity,
|
||||||
|
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
|
||||||
|
LOCATION_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPermissionExplanationRequired(Activity activity) {
|
||||||
|
return !activity.isFinishing() &&
|
||||||
|
ActivityCompat.shouldShowRequestPermissionRationale(activity,
|
||||||
|
Manifest.permission.ACCESS_FINE_LOCATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LatLng getLastLocation() {
|
||||||
|
if (lastLocation == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return LatLng.from(lastLocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a LocationManager to listen for current location.
|
||||||
*/
|
*/
|
||||||
public void registerLocationManager() {
|
public void registerLocationManager() {
|
||||||
try {
|
if (!isLocationManagerRegistered)
|
||||||
locationManager.requestLocationUpdates(provider, 400, 1, this);
|
isLocationManagerRegistered = requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER)
|
||||||
Location location = locationManager.getLastKnownLocation(provider);
|
&& requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER);
|
||||||
//Location works, just need to 'send' GPS coords
|
|
||||||
// via emulator extended controls if testing on emulator
|
|
||||||
Timber.d("Checking for location...");
|
|
||||||
if (location != null) {
|
|
||||||
this.onLocationChanged(location);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests location updates from the specified provider.
|
||||||
|
*
|
||||||
|
* @param locationProvider the location provider
|
||||||
|
* @return true if successful
|
||||||
|
*/
|
||||||
|
private boolean requestLocationUpdatesFromProvider(String locationProvider) {
|
||||||
|
try {
|
||||||
|
locationManager.requestLocationUpdates(locationProvider,
|
||||||
|
MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS,
|
||||||
|
MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS,
|
||||||
|
this);
|
||||||
|
return true;
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
Timber.e(e, "Illegal argument exception");
|
Timber.e(e, "Illegal argument exception");
|
||||||
|
return false;
|
||||||
} catch (SecurityException e) {
|
} catch (SecurityException e) {
|
||||||
Timber.e(e, "Security exception");
|
Timber.e(e, "Security exception");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Unregisters location manager.
|
/**
|
||||||
|
* Returns whether a given location is better than the current best location.
|
||||||
|
*
|
||||||
|
* @param location the location to be tested
|
||||||
|
* @param currentBestLocation the current best location
|
||||||
|
* @return true if the given location is better
|
||||||
|
*/
|
||||||
|
protected boolean isBetterLocation(Location location, Location currentBestLocation) {
|
||||||
|
if (currentBestLocation == null) {
|
||||||
|
// A new location is always better than no location
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whether the new location fix is newer or older
|
||||||
|
long timeDelta = location.getTime() - currentBestLocation.getTime();
|
||||||
|
boolean isSignificantlyNewer = timeDelta > MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS;
|
||||||
|
boolean isSignificantlyOlder = timeDelta < -MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS;
|
||||||
|
boolean isNewer = timeDelta > 0;
|
||||||
|
|
||||||
|
// If it's been more than two minutes since the current location, use the new location
|
||||||
|
// because the user has likely moved
|
||||||
|
if (isSignificantlyNewer) {
|
||||||
|
return true;
|
||||||
|
// If the new location is more than two minutes older, it must be worse
|
||||||
|
} else if (isSignificantlyOlder) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whether the new location fix is more or less accurate
|
||||||
|
int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy());
|
||||||
|
boolean isLessAccurate = accuracyDelta > 0;
|
||||||
|
boolean isMoreAccurate = accuracyDelta < 0;
|
||||||
|
boolean isSignificantlyLessAccurate = accuracyDelta > 200;
|
||||||
|
|
||||||
|
// Check if the old and new location are from the same provider
|
||||||
|
boolean isFromSameProvider = isSameProvider(location.getProvider(),
|
||||||
|
currentBestLocation.getProvider());
|
||||||
|
|
||||||
|
// Determine location quality using a combination of timeliness and accuracy
|
||||||
|
if (isMoreAccurate) {
|
||||||
|
return true;
|
||||||
|
} else if (isNewer && !isLessAccurate) {
|
||||||
|
return true;
|
||||||
|
} else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether two providers are the same
|
||||||
|
*/
|
||||||
|
private boolean isSameProvider(String provider1, String provider2) {
|
||||||
|
if (provider1 == null) {
|
||||||
|
return provider2 == null;
|
||||||
|
}
|
||||||
|
return provider1.equals(provider2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregisters location manager.
|
||||||
*/
|
*/
|
||||||
public void unregisterLocationManager() {
|
public void unregisterLocationManager() {
|
||||||
|
isLocationManagerRegistered = false;
|
||||||
try {
|
try {
|
||||||
locationManager.removeUpdates(this);
|
locationManager.removeUpdates(this);
|
||||||
} catch (SecurityException e) {
|
} catch (SecurityException e) {
|
||||||
|
|
@ -64,15 +186,34 @@ public class LocationServiceManager implements LocationListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new listener to the list of location listeners.
|
||||||
|
*
|
||||||
|
* @param listener the new listener
|
||||||
|
*/
|
||||||
|
public void addLocationListener(LocationUpdateListener listener) {
|
||||||
|
if (!locationListeners.contains(listener)) {
|
||||||
|
locationListeners.add(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a listener from the list of location listeners.
|
||||||
|
*
|
||||||
|
* @param listener the listener to be removed
|
||||||
|
*/
|
||||||
|
public void removeLocationListener(LocationUpdateListener listener) {
|
||||||
|
locationListeners.remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLocationChanged(Location location) {
|
public void onLocationChanged(Location location) {
|
||||||
double currentLatitude = location.getLatitude();
|
if (isBetterLocation(location, lastLocation)) {
|
||||||
double currentLongitude = location.getLongitude();
|
lastLocation = location;
|
||||||
latestLocationAccuracy = location.getAccuracy();
|
for (LocationUpdateListener listener : locationListeners) {
|
||||||
Timber.d("Latitude: %f Longitude: %f Accuracy %f",
|
listener.onLocationChanged(LatLng.from(lastLocation));
|
||||||
currentLatitude, currentLongitude, latestLocationAccuracy);
|
}
|
||||||
|
}
|
||||||
latestLocation = new LatLng(currentLatitude, currentLongitude, latestLocationAccuracy);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package fr.free.nrw.commons.location;
|
||||||
|
|
||||||
|
public interface LocationUpdateListener {
|
||||||
|
void onLocationChanged(LatLng latLng);
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,6 @@ import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v4.app.Fragment;
|
|
||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
@ -22,6 +21,9 @@ import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Provider;
|
||||||
|
|
||||||
import fr.free.nrw.commons.License;
|
import fr.free.nrw.commons.License;
|
||||||
import fr.free.nrw.commons.LicenseList;
|
import fr.free.nrw.commons.LicenseList;
|
||||||
import fr.free.nrw.commons.Media;
|
import fr.free.nrw.commons.Media;
|
||||||
|
|
@ -29,11 +31,12 @@ import fr.free.nrw.commons.MediaDataExtractor;
|
||||||
import fr.free.nrw.commons.MediaWikiImageView;
|
import fr.free.nrw.commons.MediaWikiImageView;
|
||||||
import fr.free.nrw.commons.PageTitle;
|
import fr.free.nrw.commons.PageTitle;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
|
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||||
import fr.free.nrw.commons.location.LatLng;
|
import fr.free.nrw.commons.location.LatLng;
|
||||||
import fr.free.nrw.commons.ui.widget.CompatTextView;
|
import fr.free.nrw.commons.ui.widget.CompatTextView;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
public class MediaDetailFragment extends Fragment {
|
public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
||||||
|
|
||||||
private boolean editable;
|
private boolean editable;
|
||||||
private MediaDetailPagerFragment.MediaDetailProvider detailProvider;
|
private MediaDetailPagerFragment.MediaDetailProvider detailProvider;
|
||||||
|
|
@ -53,6 +56,9 @@ public class MediaDetailFragment extends Fragment {
|
||||||
return mf;
|
return mf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
Provider<MediaDataExtractor> mediaDataExtractorProvider;
|
||||||
|
|
||||||
private MediaWikiImageView image;
|
private MediaWikiImageView image;
|
||||||
private MediaDetailSpacer spacer;
|
private MediaDetailSpacer spacer;
|
||||||
private int initialListTop = 0;
|
private int initialListTop = 0;
|
||||||
|
|
@ -69,8 +75,8 @@ public class MediaDetailFragment extends Fragment {
|
||||||
private boolean categoriesPresent = false;
|
private boolean categoriesPresent = false;
|
||||||
private ViewTreeObserver.OnGlobalLayoutListener layoutListener; // for layout stuff, only used once!
|
private ViewTreeObserver.OnGlobalLayoutListener layoutListener; // for layout stuff, only used once!
|
||||||
private ViewTreeObserver.OnScrollChangedListener scrollListener;
|
private ViewTreeObserver.OnScrollChangedListener scrollListener;
|
||||||
DataSetObserver dataObserver;
|
private DataSetObserver dataObserver;
|
||||||
private AsyncTask<Void,Void,Boolean> detailFetchTask;
|
private AsyncTask<Void, Void, Boolean> detailFetchTask;
|
||||||
private LicenseList licenseList;
|
private LicenseList licenseList;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -89,7 +95,7 @@ public class MediaDetailFragment extends Fragment {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
detailProvider = (MediaDetailPagerFragment.MediaDetailProvider)getActivity();
|
detailProvider = (MediaDetailPagerFragment.MediaDetailProvider) getActivity();
|
||||||
|
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
editable = savedInstanceState.getBoolean("editable");
|
editable = savedInstanceState.getBoolean("editable");
|
||||||
|
|
@ -150,7 +156,8 @@ public class MediaDetailFragment extends Fragment {
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public void onResume() {
|
@Override
|
||||||
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
Media media = detailProvider.getMediaAtPosition(index);
|
Media media = detailProvider.getMediaAtPosition(index);
|
||||||
if (media == null) {
|
if (media == null) {
|
||||||
|
|
@ -188,13 +195,13 @@ public class MediaDetailFragment extends Fragment {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPreExecute() {
|
protected void onPreExecute() {
|
||||||
extractor = new MediaDataExtractor(media.getFilename(), licenseList);
|
extractor = mediaDataExtractorProvider.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Boolean doInBackground(Void... voids) {
|
protected Boolean doInBackground(Void... voids) {
|
||||||
try {
|
try {
|
||||||
extractor.fetch();
|
extractor.fetch(media.getFilename(), licenseList);
|
||||||
return Boolean.TRUE;
|
return Boolean.TRUE;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Timber.d(e);
|
Timber.d(e);
|
||||||
|
|
@ -232,11 +239,11 @@ public class MediaDetailFragment extends Fragment {
|
||||||
detailFetchTask.cancel(true);
|
detailFetchTask.cancel(true);
|
||||||
detailFetchTask = null;
|
detailFetchTask = null;
|
||||||
}
|
}
|
||||||
if (layoutListener != null) {
|
if (layoutListener != null && getView() != null) {
|
||||||
getView().getViewTreeObserver().removeGlobalOnLayoutListener(layoutListener); // old Android was on crack. CRACK IS WHACK
|
getView().getViewTreeObserver().removeGlobalOnLayoutListener(layoutListener); // old Android was on crack. CRACK IS WHACK
|
||||||
layoutListener = null;
|
layoutListener = null;
|
||||||
}
|
}
|
||||||
if (scrollListener != null) {
|
if (scrollListener != null && getView() != null) {
|
||||||
getView().getViewTreeObserver().removeOnScrollChangedListener(scrollListener);
|
getView().getViewTreeObserver().removeOnScrollChangedListener(scrollListener);
|
||||||
scrollListener = null;
|
scrollListener = null;
|
||||||
}
|
}
|
||||||
|
|
@ -283,7 +290,7 @@ public class MediaDetailFragment extends Fragment {
|
||||||
|
|
||||||
private View buildCatLabel(final String catName, ViewGroup categoryContainer) {
|
private View buildCatLabel(final String catName, ViewGroup categoryContainer) {
|
||||||
final View item = LayoutInflater.from(getContext()).inflate(R.layout.detail_category_item, categoryContainer, false);
|
final View item = LayoutInflater.from(getContext()).inflate(R.layout.detail_category_item, categoryContainer, false);
|
||||||
final CompatTextView textView = (CompatTextView)item.findViewById(R.id.mediaDetailCategoryItemText);
|
final CompatTextView textView = (CompatTextView) item.findViewById(R.id.mediaDetailCategoryItemText);
|
||||||
|
|
||||||
textView.setText(catName);
|
textView.setText(catName);
|
||||||
if (categoriesLoaded && categoriesPresent) {
|
if (categoriesLoaded && categoriesPresent) {
|
||||||
|
|
@ -302,7 +309,7 @@ public class MediaDetailFragment extends Fragment {
|
||||||
// You must face the darkness alone
|
// You must face the darkness alone
|
||||||
int scrollY = scrollView.getScrollY();
|
int scrollY = scrollView.getScrollY();
|
||||||
int scrollMax = getView().getHeight();
|
int scrollMax = getView().getHeight();
|
||||||
float scrollPercentage = (float)scrollY / (float)scrollMax;
|
float scrollPercentage = (float) scrollY / (float) scrollMax;
|
||||||
final float transparencyMax = 0.75f;
|
final float transparencyMax = 0.75f;
|
||||||
if (scrollPercentage > transparencyMax) {
|
if (scrollPercentage > transparencyMax) {
|
||||||
scrollPercentage = transparencyMax;
|
scrollPercentage = transparencyMax;
|
||||||
|
|
@ -356,7 +363,8 @@ public class MediaDetailFragment extends Fragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private @Nullable String licenseLink(Media media) {
|
private @Nullable
|
||||||
|
String licenseLink(Media media) {
|
||||||
String licenseKey = media.getLicense();
|
String licenseKey = media.getLicense();
|
||||||
if (licenseKey == null || licenseKey.equals("")) {
|
if (licenseKey == null || licenseKey.equals("")) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -377,7 +385,7 @@ public class MediaDetailFragment extends Fragment {
|
||||||
private void openMap(LatLng coordinates) {
|
private void openMap(LatLng coordinates) {
|
||||||
//Open map app at given position
|
//Open map app at given position
|
||||||
Uri gmmIntentUri = Uri.parse(
|
Uri gmmIntentUri = Uri.parse(
|
||||||
"geo:0,0?q=" + coordinates.getLatitude() + "," + coordinates.getLatitude());
|
"geo:0,0?q=" + coordinates.getLatitude() + "," + coordinates.getLongitude());
|
||||||
Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri);
|
Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri);
|
||||||
|
|
||||||
if (mapIntent.resolveActivity(getActivity().getPackageManager()) != null) {
|
if (mapIntent.resolveActivity(getActivity().getPackageManager()) != null) {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package fr.free.nrw.commons.media;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.DownloadManager;
|
import android.app.DownloadManager;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
import android.database.DataSetObserver;
|
import android.database.DataSetObserver;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
|
@ -24,20 +25,31 @@ import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
|
||||||
import fr.free.nrw.commons.Media;
|
import fr.free.nrw.commons.Media;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
|
import fr.free.nrw.commons.auth.SessionManager;
|
||||||
import fr.free.nrw.commons.contributions.Contribution;
|
import fr.free.nrw.commons.contributions.Contribution;
|
||||||
import fr.free.nrw.commons.contributions.ContributionsActivity;
|
import fr.free.nrw.commons.contributions.ContributionsActivity;
|
||||||
import fr.free.nrw.commons.mwapi.EventLog;
|
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||||
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
|
|
||||||
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
|
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
|
||||||
import static android.content.Context.DOWNLOAD_SERVICE;
|
import static android.content.Context.DOWNLOAD_SERVICE;
|
||||||
import static android.content.Intent.ACTION_VIEW;
|
import static android.content.Intent.ACTION_VIEW;
|
||||||
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
|
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
|
||||||
import static fr.free.nrw.commons.CommonsApplication.EVENT_SHARE_ATTEMPT;
|
|
||||||
|
|
||||||
public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPageChangeListener {
|
public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment implements ViewPager.OnPageChangeListener {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
MediaWikiApi mwApi;
|
||||||
|
@Inject
|
||||||
|
SessionManager sessionManager;
|
||||||
|
@Inject
|
||||||
|
@Named("default_preferences")
|
||||||
|
SharedPreferences prefs;
|
||||||
|
|
||||||
private ViewPager pager;
|
private ViewPager pager;
|
||||||
private Boolean editable;
|
private Boolean editable;
|
||||||
|
|
@ -99,12 +111,7 @@ public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPa
|
||||||
Media m = provider.getMediaAtPosition(pager.getCurrentItem());
|
Media m = provider.getMediaAtPosition(pager.getCurrentItem());
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
case R.id.menu_share_current_image:
|
case R.id.menu_share_current_image:
|
||||||
// Share - this is just logs it, intent set in onCreateOptionsMenu, around line 252
|
// Share - intent set in onCreateOptionsMenu, around line 252
|
||||||
CommonsApplication app = (CommonsApplication) getActivity().getApplication();
|
|
||||||
EventLog.schema(EVENT_SHARE_ATTEMPT)
|
|
||||||
.param("username", app.getCurrentAccount().name)
|
|
||||||
.param("filename", m.getFilename())
|
|
||||||
.log();
|
|
||||||
return true;
|
return true;
|
||||||
case R.id.menu_browser_current_image:
|
case R.id.menu_browser_current_image:
|
||||||
// View in browser
|
// View in browser
|
||||||
|
|
@ -141,8 +148,14 @@ public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPa
|
||||||
private void downloadMedia(Media m) {
|
private void downloadMedia(Media m) {
|
||||||
String imageUrl = m.getImageUrl(),
|
String imageUrl = m.getImageUrl(),
|
||||||
fileName = m.getFilename();
|
fileName = m.getFilename();
|
||||||
|
|
||||||
|
if (imageUrl == null || fileName == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Strip 'File:' from beginning of filename, we really shouldn't store it
|
// Strip 'File:' from beginning of filename, we really shouldn't store it
|
||||||
fileName = fileName.replaceFirst("^File:", "");
|
fileName = fileName.replaceFirst("^File:", "");
|
||||||
|
|
||||||
Uri imageUri = Uri.parse(imageUrl);
|
Uri imageUri = Uri.parse(imageUrl);
|
||||||
|
|
||||||
DownloadManager.Request req = new DownloadManager.Request(imageUri);
|
DownloadManager.Request req = new DownloadManager.Request(imageUri);
|
||||||
|
|
@ -155,15 +168,19 @@ public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPa
|
||||||
req.allowScanningByMediaScanner();
|
req.allowScanningByMediaScanner();
|
||||||
req.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
|
req.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
|
||||||
&& !(ContextCompat.checkSelfPermission(getContext(),
|
ContextCompat.checkSelfPermission(getContext(), READ_EXTERNAL_STORAGE)
|
||||||
READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED)) {
|
!= PERMISSION_GRANTED
|
||||||
|
&& getView() != null) {
|
||||||
Snackbar.make(getView(), R.string.read_storage_permission_rationale,
|
Snackbar.make(getView(), R.string.read_storage_permission_rationale,
|
||||||
Snackbar.LENGTH_INDEFINITE).setAction(R.string.ok,
|
Snackbar.LENGTH_INDEFINITE).setAction(R.string.ok,
|
||||||
view -> ActivityCompat.requestPermissions(getActivity(),
|
view -> ActivityCompat.requestPermissions(getActivity(),
|
||||||
new String[]{READ_EXTERNAL_STORAGE}, 1)).show();
|
new String[]{READ_EXTERNAL_STORAGE}, 1)).show();
|
||||||
} else {
|
} else {
|
||||||
((DownloadManager) getActivity().getSystemService(DOWNLOAD_SERVICE)).enqueue(req);
|
DownloadManager systemService = (DownloadManager) getActivity().getSystemService(DOWNLOAD_SERVICE);
|
||||||
|
if (systemService != null) {
|
||||||
|
systemService.enqueue(req);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ public class CategoryModifier extends PageModifier {
|
||||||
public CategoryModifier(String... categories) {
|
public CategoryModifier(String... categories) {
|
||||||
super(MODIFIER_NAME);
|
super(MODIFIER_NAME);
|
||||||
JSONArray categoriesArray = new JSONArray();
|
JSONArray categoriesArray = new JSONArray();
|
||||||
for(String category: categories) {
|
for (String category: categories) {
|
||||||
categoriesArray.put(category);
|
categoriesArray.put(category);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|
@ -34,7 +34,7 @@ public class CategoryModifier extends PageModifier {
|
||||||
categories = params.optJSONArray(PARAM_CATEGORIES);
|
categories = params.optJSONArray(PARAM_CATEGORIES);
|
||||||
|
|
||||||
StringBuilder categoriesString = new StringBuilder();
|
StringBuilder categoriesString = new StringBuilder();
|
||||||
for(int i=0; i < categories.length(); i++) {
|
for (int i = 0; i < categories.length(); i++) {
|
||||||
String category = categories.optString(i);
|
String category = categories.optString(i);
|
||||||
categoriesString.append("\n[[Category:").append(category).append("]]");
|
categoriesString.append("\n[[Category:").append(category).append("]]");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package fr.free.nrw.commons.modifications;
|
package fr.free.nrw.commons.modifications;
|
||||||
|
|
||||||
import android.content.ContentProvider;
|
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.content.UriMatcher;
|
import android.content.UriMatcher;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
|
|
@ -10,50 +9,51 @@ import android.net.Uri;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.data.DBOpenHelper;
|
||||||
|
import fr.free.nrw.commons.di.CommonsDaggerContentProvider;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
public class ModificationsContentProvider extends ContentProvider{
|
import static fr.free.nrw.commons.modifications.ModifierSequenceDao.Table.TABLE_NAME;
|
||||||
|
|
||||||
|
public class ModificationsContentProvider extends CommonsDaggerContentProvider {
|
||||||
|
|
||||||
private static final int MODIFICATIONS = 1;
|
private static final int MODIFICATIONS = 1;
|
||||||
private static final int MODIFICATIONS_ID = 2;
|
private static final int MODIFICATIONS_ID = 2;
|
||||||
|
|
||||||
public static final String AUTHORITY = "fr.free.nrw.commons.modifications.contentprovider";
|
public static final String MODIFICATIONS_AUTHORITY = "fr.free.nrw.commons.modifications.contentprovider";
|
||||||
private static final String BASE_PATH = "modifications";
|
public static final String BASE_PATH = "modifications";
|
||||||
|
|
||||||
public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY + "/" + BASE_PATH);
|
public static final Uri BASE_URI = Uri.parse("content://" + MODIFICATIONS_AUTHORITY + "/" + BASE_PATH);
|
||||||
|
|
||||||
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||||
static {
|
static {
|
||||||
uriMatcher.addURI(AUTHORITY, BASE_PATH, MODIFICATIONS);
|
uriMatcher.addURI(MODIFICATIONS_AUTHORITY, BASE_PATH, MODIFICATIONS);
|
||||||
uriMatcher.addURI(AUTHORITY, BASE_PATH + "/#", MODIFICATIONS_ID);
|
uriMatcher.addURI(MODIFICATIONS_AUTHORITY, BASE_PATH + "/#", MODIFICATIONS_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Uri uriForId(int id) {
|
public static Uri uriForId(int id) {
|
||||||
return Uri.parse(BASE_URI.toString() + "/" + id);
|
return Uri.parse(BASE_URI.toString() + "/" + id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Inject DBOpenHelper dbOpenHelper;
|
||||||
@Override
|
|
||||||
public boolean onCreate() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
||||||
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
|
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
|
||||||
queryBuilder.setTables(ModifierSequence.Table.TABLE_NAME);
|
queryBuilder.setTables(TABLE_NAME);
|
||||||
|
|
||||||
int uriType = uriMatcher.match(uri);
|
int uriType = uriMatcher.match(uri);
|
||||||
|
|
||||||
switch(uriType) {
|
switch (uriType) {
|
||||||
case MODIFICATIONS:
|
case MODIFICATIONS:
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Unknown URI" + uri);
|
throw new IllegalArgumentException("Unknown URI" + uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
SQLiteDatabase db = CommonsApplication.getInstance().getDBOpenHelper().getReadableDatabase();
|
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
|
||||||
|
|
||||||
Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder);
|
Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder);
|
||||||
cursor.setNotificationUri(getContext().getContentResolver(), uri);
|
cursor.setNotificationUri(getContext().getContentResolver(), uri);
|
||||||
|
|
@ -69,11 +69,11 @@ public class ModificationsContentProvider extends ContentProvider{
|
||||||
@Override
|
@Override
|
||||||
public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
|
public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
|
||||||
int uriType = uriMatcher.match(uri);
|
int uriType = uriMatcher.match(uri);
|
||||||
SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase();
|
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||||
long id = 0;
|
long id;
|
||||||
switch (uriType) {
|
switch (uriType) {
|
||||||
case MODIFICATIONS:
|
case MODIFICATIONS:
|
||||||
id = sqlDB.insert(ModifierSequence.Table.TABLE_NAME, null, contentValues);
|
id = sqlDB.insert(TABLE_NAME, null, contentValues);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Unknown URI: " + uri);
|
throw new IllegalArgumentException("Unknown URI: " + uri);
|
||||||
|
|
@ -85,11 +85,11 @@ public class ModificationsContentProvider extends ContentProvider{
|
||||||
@Override
|
@Override
|
||||||
public int delete(@NonNull Uri uri, String s, String[] strings) {
|
public int delete(@NonNull Uri uri, String s, String[] strings) {
|
||||||
int uriType = uriMatcher.match(uri);
|
int uriType = uriMatcher.match(uri);
|
||||||
SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase();
|
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||||
switch (uriType) {
|
switch (uriType) {
|
||||||
case MODIFICATIONS_ID:
|
case MODIFICATIONS_ID:
|
||||||
String id = uri.getLastPathSegment();
|
String id = uri.getLastPathSegment();
|
||||||
sqlDB.delete(ModifierSequence.Table.TABLE_NAME,
|
sqlDB.delete(TABLE_NAME,
|
||||||
"_id = ?",
|
"_id = ?",
|
||||||
new String[] { id }
|
new String[] { id }
|
||||||
);
|
);
|
||||||
|
|
@ -103,13 +103,13 @@ public class ModificationsContentProvider extends ContentProvider{
|
||||||
public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
|
public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
|
||||||
Timber.d("Hello, bulk insert! (ModificationsContentProvider)");
|
Timber.d("Hello, bulk insert! (ModificationsContentProvider)");
|
||||||
int uriType = uriMatcher.match(uri);
|
int uriType = uriMatcher.match(uri);
|
||||||
SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase();
|
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||||
sqlDB.beginTransaction();
|
sqlDB.beginTransaction();
|
||||||
switch (uriType) {
|
switch (uriType) {
|
||||||
case MODIFICATIONS:
|
case MODIFICATIONS:
|
||||||
for(ContentValues value: values) {
|
for (ContentValues value: values) {
|
||||||
Timber.d("Inserting! %s", value);
|
Timber.d("Inserting! %s", value);
|
||||||
sqlDB.insert(ModifierSequence.Table.TABLE_NAME, null, value);
|
sqlDB.insert(TABLE_NAME, null, value);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|
@ -131,11 +131,11 @@ public class ModificationsContentProvider extends ContentProvider{
|
||||||
In here, the only concat created argument is for id. It is cast to an int, and will error out otherwise.
|
In here, the only concat created argument is for id. It is cast to an int, and will error out otherwise.
|
||||||
*/
|
*/
|
||||||
int uriType = uriMatcher.match(uri);
|
int uriType = uriMatcher.match(uri);
|
||||||
SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase();
|
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||||
int rowsUpdated = 0;
|
int rowsUpdated;
|
||||||
switch (uriType) {
|
switch (uriType) {
|
||||||
case MODIFICATIONS:
|
case MODIFICATIONS:
|
||||||
rowsUpdated = sqlDB.update(ModifierSequence.Table.TABLE_NAME,
|
rowsUpdated = sqlDB.update(TABLE_NAME,
|
||||||
contentValues,
|
contentValues,
|
||||||
selection,
|
selection,
|
||||||
selectionArgs);
|
selectionArgs);
|
||||||
|
|
@ -144,9 +144,9 @@ public class ModificationsContentProvider extends ContentProvider{
|
||||||
int id = Integer.valueOf(uri.getLastPathSegment());
|
int id = Integer.valueOf(uri.getLastPathSegment());
|
||||||
|
|
||||||
if (TextUtils.isEmpty(selection)) {
|
if (TextUtils.isEmpty(selection)) {
|
||||||
rowsUpdated = sqlDB.update(ModifierSequence.Table.TABLE_NAME,
|
rowsUpdated = sqlDB.update(TABLE_NAME,
|
||||||
contentValues,
|
contentValues,
|
||||||
ModifierSequence.Table.COLUMN_ID + " = ?",
|
ModifierSequenceDao.Table.COLUMN_ID + " = ?",
|
||||||
new String[] { String.valueOf(id) } );
|
new String[] { String.valueOf(id) } );
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException("Parameter `selection` should be empty when updating an ID");
|
throw new IllegalArgumentException("Parameter `selection` should be empty when updating an ID");
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
package fr.free.nrw.commons.modifications;
|
package fr.free.nrw.commons.modifications;
|
||||||
|
|
||||||
import android.accounts.Account;
|
import android.accounts.Account;
|
||||||
import android.accounts.AccountManager;
|
|
||||||
import android.accounts.AuthenticatorException;
|
|
||||||
import android.accounts.OperationCanceledException;
|
|
||||||
import android.content.AbstractThreadedSyncAdapter;
|
import android.content.AbstractThreadedSyncAdapter;
|
||||||
import android.content.ContentProviderClient;
|
import android.content.ContentProviderClient;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
@ -14,15 +11,24 @@ import android.os.RemoteException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
import javax.inject.Inject;
|
||||||
import fr.free.nrw.commons.Utils;
|
|
||||||
|
import fr.free.nrw.commons.auth.SessionManager;
|
||||||
import fr.free.nrw.commons.contributions.Contribution;
|
import fr.free.nrw.commons.contributions.Contribution;
|
||||||
|
import fr.free.nrw.commons.contributions.ContributionDao;
|
||||||
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
|
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
|
||||||
|
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
|
public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||||
|
|
||||||
|
@Inject MediaWikiApi mwApi;
|
||||||
|
@Inject ContributionDao contributionDao;
|
||||||
|
@Inject ModifierSequenceDao modifierSequenceDao;
|
||||||
|
@Inject
|
||||||
|
SessionManager sessionManager;
|
||||||
|
|
||||||
public ModificationsSyncAdapter(Context context, boolean autoInitialize) {
|
public ModificationsSyncAdapter(Context context, boolean autoInitialize) {
|
||||||
super(context, autoInitialize);
|
super(context, autoInitialize);
|
||||||
}
|
}
|
||||||
|
|
@ -30,6 +36,11 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||||
@Override
|
@Override
|
||||||
public void onPerformSync(Account account, Bundle bundle, String s, ContentProviderClient contentProviderClient, SyncResult syncResult) {
|
public void onPerformSync(Account account, Bundle bundle, String s, ContentProviderClient contentProviderClient, SyncResult syncResult) {
|
||||||
// This code is fraught with possibilities of race conditions, but lalalalala I can't hear you!
|
// This code is fraught with possibilities of race conditions, but lalalalala I can't hear you!
|
||||||
|
ApplicationlessInjection
|
||||||
|
.getInstance(getContext()
|
||||||
|
.getApplicationContext())
|
||||||
|
.getCommonsApplicationComponent()
|
||||||
|
.inject(this);
|
||||||
|
|
||||||
Cursor allModifications;
|
Cursor allModifications;
|
||||||
try {
|
try {
|
||||||
|
|
@ -44,27 +55,17 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String authCookie;
|
String authCookie = sessionManager.getAuthCookie();
|
||||||
try {
|
if (isNullOrWhiteSpace(authCookie)) {
|
||||||
authCookie = AccountManager.get(getContext()).blockingGetAuthToken(account, "", false);
|
|
||||||
} catch (OperationCanceledException | AuthenticatorException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
} catch (IOException e) {
|
|
||||||
Timber.d("Could not authenticate :(");
|
Timber.d("Could not authenticate :(");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Utils.isNullOrWhiteSpace(authCookie)) {
|
mwApi.setAuthCookie(authCookie);
|
||||||
Timber.d("Could not authenticate :(");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
|
|
||||||
api.setAuthCookie(authCookie);
|
|
||||||
String editToken;
|
String editToken;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
editToken = api.getEditToken();
|
editToken = mwApi.getEditToken();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Timber.d("Can not retreive edit token!");
|
Timber.d("Can not retreive edit token!");
|
||||||
return;
|
return;
|
||||||
|
|
@ -76,28 +77,36 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||||
|
|
||||||
ContentProviderClient contributionsClient = null;
|
ContentProviderClient contributionsClient = null;
|
||||||
try {
|
try {
|
||||||
contributionsClient = getContext().getContentResolver().acquireContentProviderClient(ContributionsContentProvider.AUTHORITY);
|
contributionsClient = getContext().getContentResolver().acquireContentProviderClient(ContributionsContentProvider.CONTRIBUTION_AUTHORITY);
|
||||||
|
|
||||||
while (!allModifications.isAfterLast()) {
|
while (!allModifications.isAfterLast()) {
|
||||||
ModifierSequence sequence = ModifierSequence.fromCursor(allModifications);
|
ModifierSequence sequence = modifierSequenceDao.fromCursor(allModifications);
|
||||||
sequence.setContentProviderClient(contentProviderClient);
|
|
||||||
Contribution contrib;
|
Contribution contrib;
|
||||||
|
|
||||||
Cursor contributionCursor;
|
Cursor contributionCursor;
|
||||||
|
|
||||||
|
if (contributionsClient == null) {
|
||||||
|
Timber.e("ContributionsClient is null. This should not happen!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
contributionCursor = contributionsClient.query(sequence.getMediaUri(), null, null, null, null);
|
contributionCursor = contributionsClient.query(sequence.getMediaUri(), null, null, null, null);
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
contributionCursor.moveToFirst();
|
|
||||||
contrib = Contribution.fromCursor(contributionCursor);
|
|
||||||
|
|
||||||
if (contrib.getState() == Contribution.STATE_COMPLETED) {
|
if (contributionCursor != null) {
|
||||||
|
contributionCursor.moveToFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
contrib = contributionDao.fromCursor(contributionCursor);
|
||||||
|
|
||||||
|
if (contrib != null && contrib.getState() == Contribution.STATE_COMPLETED) {
|
||||||
String pageContent;
|
String pageContent;
|
||||||
try {
|
try {
|
||||||
pageContent = api.revisionsByFilename(contrib.getFilename());
|
pageContent = mwApi.revisionsByFilename(contrib.getFilename());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Timber.d("Network fuckup on modifications sync!");
|
Timber.d("Network messed up on modifications sync!");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,19 +115,19 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||||
|
|
||||||
String editResult;
|
String editResult;
|
||||||
try {
|
try {
|
||||||
editResult = api.edit(editToken, processedPageContent, contrib.getFilename(), sequence.getEditSummary());
|
editResult = mwApi.edit(editToken, processedPageContent, contrib.getFilename(), sequence.getEditSummary());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Timber.d("Network fuckup on modifications sync!");
|
Timber.d("Network messed up on modifications sync!");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
Timber.d("Response is %s", editResult);
|
Timber.d("Response is %s", editResult);
|
||||||
|
|
||||||
if (!editResult.equals("Success")) {
|
if (!"Success".equals(editResult)) {
|
||||||
// FIXME: Log this somewhere else
|
// FIXME: Log this somewhere else
|
||||||
Timber.d("Non success result! %s", editResult);
|
Timber.d("Non success result! %s", editResult);
|
||||||
} else {
|
} else {
|
||||||
sequence.delete();
|
modifierSequenceDao.delete(sequence);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
allModifications.moveToNext();
|
allModifications.moveToNext();
|
||||||
|
|
@ -129,4 +138,8 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isNullOrWhiteSpace(String value) {
|
||||||
|
return value == null || value.trim().isEmpty();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,8 @@
|
||||||
package fr.free.nrw.commons.modifications;
|
package fr.free.nrw.commons.modifications;
|
||||||
|
|
||||||
import android.content.ContentProviderClient;
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.RemoteException;
|
|
||||||
|
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
@ -17,22 +11,21 @@ public class ModifierSequence {
|
||||||
private Uri mediaUri;
|
private Uri mediaUri;
|
||||||
private ArrayList<PageModifier> modifiers;
|
private ArrayList<PageModifier> modifiers;
|
||||||
private Uri contentUri;
|
private Uri contentUri;
|
||||||
private ContentProviderClient client;
|
|
||||||
|
|
||||||
public ModifierSequence(Uri mediaUri) {
|
public ModifierSequence(Uri mediaUri) {
|
||||||
this.mediaUri = mediaUri;
|
this.mediaUri = mediaUri;
|
||||||
modifiers = new ArrayList<>();
|
modifiers = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ModifierSequence(Uri mediaUri, JSONObject data) {
|
ModifierSequence(Uri mediaUri, JSONObject data) {
|
||||||
this(mediaUri);
|
this(mediaUri);
|
||||||
JSONArray modifiersJSON = data.optJSONArray("modifiers");
|
JSONArray modifiersJSON = data.optJSONArray("modifiers");
|
||||||
for (int i=0; i< modifiersJSON.length(); i++) {
|
for (int i = 0; i < modifiersJSON.length(); i++) {
|
||||||
modifiers.add(PageModifier.fromJSON(modifiersJSON.optJSONObject(i)));
|
modifiers.add(PageModifier.fromJSON(modifiersJSON.optJSONObject(i)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Uri getMediaUri() {
|
Uri getMediaUri() {
|
||||||
return mediaUri;
|
return mediaUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,113 +33,32 @@ public class ModifierSequence {
|
||||||
modifiers.add(modifier);
|
modifiers.add(modifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String executeModifications(String pageName, String pageContents) {
|
String executeModifications(String pageName, String pageContents) {
|
||||||
for (PageModifier modifier: modifiers) {
|
for (PageModifier modifier: modifiers) {
|
||||||
pageContents = modifier.doModification(pageName, pageContents);
|
pageContents = modifier.doModification(pageName, pageContents);
|
||||||
}
|
}
|
||||||
return pageContents;
|
return pageContents;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getEditSummary() {
|
String getEditSummary() {
|
||||||
StringBuilder editSummary = new StringBuilder();
|
StringBuilder editSummary = new StringBuilder();
|
||||||
for(PageModifier modifier: modifiers) {
|
for (PageModifier modifier: modifiers) {
|
||||||
editSummary.append(modifier.getEditSumary()).append(" ");
|
editSummary.append(modifier.getEditSumary()).append(" ");
|
||||||
}
|
}
|
||||||
editSummary.append("Via Commons Mobile App");
|
editSummary.append("Via Commons Mobile App");
|
||||||
return editSummary.toString();
|
return editSummary.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public JSONObject toJSON() {
|
ArrayList<PageModifier> getModifiers() {
|
||||||
JSONObject data = new JSONObject();
|
return modifiers;
|
||||||
try {
|
|
||||||
JSONArray modifiersJSON = new JSONArray();
|
|
||||||
for (PageModifier modifier: modifiers) {
|
|
||||||
modifiersJSON.put(modifier.toJSON());
|
|
||||||
}
|
|
||||||
data.put("modifiers", modifiersJSON);
|
|
||||||
return data;
|
|
||||||
} catch (JSONException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ContentValues toContentValues() {
|
Uri getContentUri() {
|
||||||
ContentValues cv = new ContentValues();
|
return contentUri;
|
||||||
cv.put(Table.COLUMN_MEDIA_URI, mediaUri.toString());
|
|
||||||
cv.put(Table.COLUMN_DATA, toJSON().toString());
|
|
||||||
return cv;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ModifierSequence fromCursor(Cursor cursor) {
|
void setContentUri(Uri contentUri) {
|
||||||
// Hardcoding column positions!
|
this.contentUri = contentUri;
|
||||||
ModifierSequence ms = null;
|
|
||||||
try {
|
|
||||||
ms = new ModifierSequence(Uri.parse(cursor.getString(1)),
|
|
||||||
new JSONObject(cursor.getString(2)));
|
|
||||||
} catch (JSONException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
ms.contentUri = ModificationsContentProvider.uriForId(cursor.getInt(0));
|
|
||||||
|
|
||||||
return ms;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void save() {
|
|
||||||
try {
|
|
||||||
if(contentUri == null) {
|
|
||||||
contentUri = client.insert(ModificationsContentProvider.BASE_URI, this.toContentValues());
|
|
||||||
} else {
|
|
||||||
client.update(contentUri, toContentValues(), null, null);
|
|
||||||
}
|
|
||||||
} catch(RemoteException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void delete() {
|
|
||||||
try {
|
|
||||||
client.delete(contentUri, null, null);
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setContentProviderClient(ContentProviderClient client) {
|
|
||||||
this.client = client;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Table {
|
|
||||||
public static final String TABLE_NAME = "modifications";
|
|
||||||
|
|
||||||
public static final String COLUMN_ID = "_id";
|
|
||||||
public static final String COLUMN_MEDIA_URI = "mediauri";
|
|
||||||
public static final String COLUMN_DATA = "data";
|
|
||||||
|
|
||||||
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
|
|
||||||
public static final String[] ALL_FIELDS = {
|
|
||||||
COLUMN_ID,
|
|
||||||
COLUMN_MEDIA_URI,
|
|
||||||
COLUMN_DATA
|
|
||||||
};
|
|
||||||
|
|
||||||
private static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
|
|
||||||
+ "_id INTEGER PRIMARY KEY,"
|
|
||||||
+ "mediauri STRING,"
|
|
||||||
+ "data STRING"
|
|
||||||
+ ");";
|
|
||||||
|
|
||||||
public static void onCreate(SQLiteDatabase db) {
|
|
||||||
db.execSQL(CREATE_TABLE_STATEMENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void onUpdate(SQLiteDatabase db, int from, int to) {
|
|
||||||
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
|
|
||||||
onCreate(db);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void onDelete(SQLiteDatabase db) {
|
|
||||||
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
|
|
||||||
onCreate(db);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
package fr.free.nrw.commons.modifications;
|
||||||
|
|
||||||
|
import android.content.ContentProviderClient;
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
import javax.inject.Provider;
|
||||||
|
|
||||||
|
public class ModifierSequenceDao {
|
||||||
|
|
||||||
|
private final Provider<ContentProviderClient> clientProvider;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public ModifierSequenceDao(@Named("modification") Provider<ContentProviderClient> clientProvider) {
|
||||||
|
this.clientProvider = clientProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void save(ModifierSequence sequence) {
|
||||||
|
ContentProviderClient db = clientProvider.get();
|
||||||
|
try {
|
||||||
|
if (sequence.getContentUri() == null) {
|
||||||
|
sequence.setContentUri(db.insert(ModificationsContentProvider.BASE_URI, toContentValues(sequence)));
|
||||||
|
} else {
|
||||||
|
db.update(sequence.getContentUri(), toContentValues(sequence), null, null);
|
||||||
|
}
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
} finally {
|
||||||
|
db.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(ModifierSequence sequence) {
|
||||||
|
ContentProviderClient db = clientProvider.get();
|
||||||
|
try {
|
||||||
|
db.delete(sequence.getContentUri(), null, null);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
} finally {
|
||||||
|
db.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ModifierSequence fromCursor(Cursor cursor) {
|
||||||
|
// Hardcoding column positions!
|
||||||
|
ModifierSequence ms;
|
||||||
|
try {
|
||||||
|
ms = new ModifierSequence(Uri.parse(cursor.getString(1)),
|
||||||
|
new JSONObject(cursor.getString(2)));
|
||||||
|
} catch (JSONException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
ms.setContentUri( ModificationsContentProvider.uriForId(cursor.getInt(0)));
|
||||||
|
|
||||||
|
return ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JSONObject toJSON(ModifierSequence sequence) {
|
||||||
|
JSONObject data = new JSONObject();
|
||||||
|
try {
|
||||||
|
JSONArray modifiersJSON = new JSONArray();
|
||||||
|
for (PageModifier modifier: sequence.getModifiers()) {
|
||||||
|
modifiersJSON.put(modifier.toJSON());
|
||||||
|
}
|
||||||
|
data.put("modifiers", modifiersJSON);
|
||||||
|
return data;
|
||||||
|
} catch (JSONException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ContentValues toContentValues(ModifierSequence sequence) {
|
||||||
|
ContentValues cv = new ContentValues();
|
||||||
|
cv.put(Table.COLUMN_MEDIA_URI, sequence.getMediaUri().toString());
|
||||||
|
cv.put(Table.COLUMN_DATA, toJSON(sequence).toString());
|
||||||
|
return cv;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Table {
|
||||||
|
static final String TABLE_NAME = "modifications";
|
||||||
|
|
||||||
|
static final String COLUMN_ID = "_id";
|
||||||
|
static final String COLUMN_MEDIA_URI = "mediauri";
|
||||||
|
static final String COLUMN_DATA = "data";
|
||||||
|
|
||||||
|
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
|
||||||
|
public static final String[] ALL_FIELDS = {
|
||||||
|
COLUMN_ID,
|
||||||
|
COLUMN_MEDIA_URI,
|
||||||
|
COLUMN_DATA
|
||||||
|
};
|
||||||
|
|
||||||
|
static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME;
|
||||||
|
|
||||||
|
static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
|
||||||
|
+ "_id INTEGER PRIMARY KEY,"
|
||||||
|
+ "mediauri STRING,"
|
||||||
|
+ "data STRING"
|
||||||
|
+ ");";
|
||||||
|
|
||||||
|
public static void onCreate(SQLiteDatabase db) {
|
||||||
|
db.execSQL(CREATE_TABLE_STATEMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void onUpdate(SQLiteDatabase db, int from, int to) {
|
||||||
|
db.execSQL(DROP_TABLE_STATEMENT);
|
||||||
|
onCreate(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void onDelete(SQLiteDatabase db) {
|
||||||
|
db.execSQL(DROP_TABLE_STATEMENT);
|
||||||
|
onCreate(db);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,9 +7,9 @@ public abstract class PageModifier {
|
||||||
|
|
||||||
public static PageModifier fromJSON(JSONObject data) {
|
public static PageModifier fromJSON(JSONObject data) {
|
||||||
String name = data.optString("name");
|
String name = data.optString("name");
|
||||||
if(name.equals(CategoryModifier.MODIFIER_NAME)) {
|
if (name.equals(CategoryModifier.MODIFIER_NAME)) {
|
||||||
return new CategoryModifier(data.optJSONObject("data"));
|
return new CategoryModifier(data.optJSONObject("data"));
|
||||||
} else if(name.equals(TemplateRemoveModifier.MODIFIER_NAME)) {
|
} else if (name.equals(TemplateRemoveModifier.MODIFIER_NAME)) {
|
||||||
return new TemplateRemoveModifier(data.optJSONObject("data"));
|
return new TemplateRemoveModifier(data.optJSONObject("data"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,18 +41,18 @@ public class TemplateRemoveModifier extends PageModifier {
|
||||||
Pattern templateStartPattern = Pattern.compile("\\{\\{" + templateNormalized, Pattern.CASE_INSENSITIVE);
|
Pattern templateStartPattern = Pattern.compile("\\{\\{" + templateNormalized, Pattern.CASE_INSENSITIVE);
|
||||||
Matcher matcher = templateStartPattern.matcher(pageContents);
|
Matcher matcher = templateStartPattern.matcher(pageContents);
|
||||||
|
|
||||||
while(matcher.find()) {
|
while (matcher.find()) {
|
||||||
int braceCount = 1;
|
int braceCount = 1;
|
||||||
int startIndex = matcher.start();
|
int startIndex = matcher.start();
|
||||||
int curIndex = matcher.end();
|
int curIndex = matcher.end();
|
||||||
Matcher openMatch = PATTERN_TEMPLATE_OPEN.matcher(pageContents);
|
Matcher openMatch = PATTERN_TEMPLATE_OPEN.matcher(pageContents);
|
||||||
Matcher closeMatch = PATTERN_TEMPLATE_CLOSE.matcher(pageContents);
|
Matcher closeMatch = PATTERN_TEMPLATE_CLOSE.matcher(pageContents);
|
||||||
|
|
||||||
while(curIndex < pageContents.length()) {
|
while (curIndex < pageContents.length()) {
|
||||||
boolean openFound = openMatch.find(curIndex);
|
boolean openFound = openMatch.find(curIndex);
|
||||||
boolean closeFound = closeMatch.find(curIndex);
|
boolean closeFound = closeMatch.find(curIndex);
|
||||||
|
|
||||||
if(openFound && (!closeFound || openMatch.start() < closeMatch.start())) {
|
if (openFound && (!closeFound || openMatch.start() < closeMatch.start())) {
|
||||||
braceCount++;
|
braceCount++;
|
||||||
curIndex = openMatch.end();
|
curIndex = openMatch.end();
|
||||||
} else if (closeFound) {
|
} else if (closeFound) {
|
||||||
|
|
@ -71,8 +71,8 @@ public class TemplateRemoveModifier extends PageModifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strip trailing whitespace
|
// Strip trailing whitespace
|
||||||
while(curIndex < pageContents.length()) {
|
while (curIndex < pageContents.length()) {
|
||||||
if(pageContents.charAt(curIndex) == ' ' || pageContents.charAt(curIndex) == '\n') {
|
if (pageContents.charAt(curIndex) == ' ' || pageContents.charAt(curIndex) == '\n') {
|
||||||
curIndex++;
|
curIndex++;
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package fr.free.nrw.commons.mwapi;
|
package fr.free.nrw.commons.mwapi;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
|
@ -21,10 +23,14 @@ import org.apache.http.params.CoreProtocolPNames;
|
||||||
import org.apache.http.util.EntityUtils;
|
import org.apache.http.util.EntityUtils;
|
||||||
import org.mediawiki.api.ApiResult;
|
import org.mediawiki.api.ApiResult;
|
||||||
import org.mediawiki.api.MWApi;
|
import org.mediawiki.api.MWApi;
|
||||||
|
import org.w3c.dom.Node;
|
||||||
|
import org.w3c.dom.NodeList;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
@ -34,12 +40,17 @@ import java.util.concurrent.Callable;
|
||||||
|
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
import fr.free.nrw.commons.BuildConfig;
|
||||||
import fr.free.nrw.commons.PageTitle;
|
import fr.free.nrw.commons.PageTitle;
|
||||||
import fr.free.nrw.commons.Utils;
|
import fr.free.nrw.commons.notification.Notification;
|
||||||
import in.yuvi.http.fluent.Http;
|
import in.yuvi.http.fluent.Http;
|
||||||
import io.reactivex.Observable;
|
import io.reactivex.Observable;
|
||||||
import io.reactivex.Single;
|
import io.reactivex.Single;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
import static fr.free.nrw.commons.notification.NotificationType.UNKNOWN;
|
||||||
|
import static fr.free.nrw.commons.notification.NotificationUtils.getNotificationFromApiResult;
|
||||||
|
import static fr.free.nrw.commons.notification.NotificationUtils.getNotificationType;
|
||||||
|
import static fr.free.nrw.commons.notification.NotificationUtils.isCommonsNotification;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Addshore
|
* @author Addshore
|
||||||
*/
|
*/
|
||||||
|
|
@ -49,17 +60,27 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
||||||
private static final String THUMB_SIZE = "640";
|
private static final String THUMB_SIZE = "640";
|
||||||
private AbstractHttpClient httpClient;
|
private AbstractHttpClient httpClient;
|
||||||
private MWApi api;
|
private MWApi api;
|
||||||
|
private Context context;
|
||||||
|
private SharedPreferences sharedPreferences;
|
||||||
|
|
||||||
public ApacheHttpClientMediaWikiApi(String apiURL) {
|
public ApacheHttpClientMediaWikiApi(Context context, String apiURL, SharedPreferences sharedPreferences) {
|
||||||
|
this.context = context;
|
||||||
BasicHttpParams params = new BasicHttpParams();
|
BasicHttpParams params = new BasicHttpParams();
|
||||||
SchemeRegistry schemeRegistry = new SchemeRegistry();
|
SchemeRegistry schemeRegistry = new SchemeRegistry();
|
||||||
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
|
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
|
||||||
final SSLSocketFactory sslSocketFactory = SSLSocketFactory.getSocketFactory();
|
final SSLSocketFactory sslSocketFactory = SSLSocketFactory.getSocketFactory();
|
||||||
schemeRegistry.register(new Scheme("https", sslSocketFactory, 443));
|
schemeRegistry.register(new Scheme("https", sslSocketFactory, 443));
|
||||||
ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry);
|
ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry);
|
||||||
params.setParameter(CoreProtocolPNames.USER_AGENT, "Commons/" + BuildConfig.VERSION_NAME + " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE);
|
params.setParameter(CoreProtocolPNames.USER_AGENT, getUserAgent());
|
||||||
httpClient = new DefaultHttpClient(cm, params);
|
httpClient = new DefaultHttpClient(cm, params);
|
||||||
api = new MWApi(apiURL, httpClient);
|
api = new MWApi(apiURL, httpClient);
|
||||||
|
this.sharedPreferences = sharedPreferences;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NonNull
|
||||||
|
public String getUserAgent() {
|
||||||
|
return "Commons/" + BuildConfig.VERSION_NAME + " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
|
|
@ -74,11 +95,13 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
||||||
* @throws IOException On api request IO issue
|
* @throws IOException On api request IO issue
|
||||||
*/
|
*/
|
||||||
public String login(String username, String password) throws IOException {
|
public String login(String username, String password) throws IOException {
|
||||||
|
String loginToken = getLoginToken();
|
||||||
|
Timber.d("Login token is %s", loginToken);
|
||||||
return getErrorCodeToReturn(api.action("clientlogin")
|
return getErrorCodeToReturn(api.action("clientlogin")
|
||||||
.param("rememberMe", "1")
|
.param("rememberMe", "1")
|
||||||
.param("username", username)
|
.param("username", username)
|
||||||
.param("password", password)
|
.param("password", password)
|
||||||
.param("logintoken", getLoginToken())
|
.param("logintoken", loginToken)
|
||||||
.param("loginreturnurl", "https://commons.wikimedia.org")
|
.param("loginreturnurl", "https://commons.wikimedia.org")
|
||||||
.post());
|
.post());
|
||||||
}
|
}
|
||||||
|
|
@ -91,12 +114,14 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
||||||
* @throws IOException On api request IO issue
|
* @throws IOException On api request IO issue
|
||||||
*/
|
*/
|
||||||
public String login(String username, String password, String twoFactorCode) throws IOException {
|
public String login(String username, String password, String twoFactorCode) throws IOException {
|
||||||
|
String loginToken = getLoginToken();
|
||||||
|
Timber.d("Login token is %s", loginToken);
|
||||||
return getErrorCodeToReturn(api.action("clientlogin")
|
return getErrorCodeToReturn(api.action("clientlogin")
|
||||||
.param("rememberMe", "1")
|
.param("rememberMe", "true")
|
||||||
.param("username", username)
|
.param("username", username)
|
||||||
.param("password", password)
|
.param("password", password)
|
||||||
.param("logintoken", getLoginToken())
|
.param("logintoken", loginToken)
|
||||||
.param("logincontinue", "1")
|
.param("logincontinue", "true")
|
||||||
.param("OATHToken", twoFactorCode)
|
.param("OATHToken", twoFactorCode)
|
||||||
.post());
|
.post());
|
||||||
}
|
}
|
||||||
|
|
@ -121,14 +146,17 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
||||||
String status = loginApiResult.getString("/api/clientlogin/@status");
|
String status = loginApiResult.getString("/api/clientlogin/@status");
|
||||||
if (status.equals("PASS")) {
|
if (status.equals("PASS")) {
|
||||||
api.isLoggedIn = true;
|
api.isLoggedIn = true;
|
||||||
|
setAuthCookieOnLogin(true);
|
||||||
return status;
|
return status;
|
||||||
} else if (status.equals("FAIL")) {
|
} else if (status.equals("FAIL")) {
|
||||||
|
setAuthCookieOnLogin(false);
|
||||||
return loginApiResult.getString("/api/clientlogin/@messagecode");
|
return loginApiResult.getString("/api/clientlogin/@messagecode");
|
||||||
} else if (
|
} else if (
|
||||||
status.equals("UI")
|
status.equals("UI")
|
||||||
&& loginApiResult.getString("/api/clientlogin/requests/_v/@id").equals("TOTPAuthenticationRequest")
|
&& loginApiResult.getString("/api/clientlogin/requests/_v/@id").equals("TOTPAuthenticationRequest")
|
||||||
&& loginApiResult.getString("/api/clientlogin/requests/_v/@provider").equals("Two-factor authentication (OATH).")
|
&& loginApiResult.getString("/api/clientlogin/requests/_v/@provider").equals("Two-factor authentication (OATH).")
|
||||||
) {
|
) {
|
||||||
|
setAuthCookieOnLogin(false);
|
||||||
return "2FA";
|
return "2FA";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,6 +164,18 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
||||||
return "genericerror-" + status;
|
return "genericerror-" + status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setAuthCookieOnLogin(boolean isLoggedIn) {
|
||||||
|
SharedPreferences.Editor editor = sharedPreferences.edit();
|
||||||
|
if (isLoggedIn) {
|
||||||
|
editor.putBoolean("isUserLoggedIn", true);
|
||||||
|
editor.putString("getAuthCookie", api.getAuthCookie());
|
||||||
|
} else {
|
||||||
|
editor.putBoolean("isUserLoggedIn", false);
|
||||||
|
editor.remove("getAuthCookie");
|
||||||
|
}
|
||||||
|
editor.apply();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getAuthCookie() {
|
public String getAuthCookie() {
|
||||||
return api.getAuthCookie();
|
return api.getAuthCookie();
|
||||||
|
|
@ -335,7 +375,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
||||||
logEvents.add(new LogEventResult.LogEvent(
|
logEvents.add(new LogEventResult.LogEvent(
|
||||||
image.getString("@pageid"),
|
image.getString("@pageid"),
|
||||||
image.getString("@title"),
|
image.getString("@title"),
|
||||||
Utils.parseMWDate(image.getString("@timestamp")))
|
parseMWDate(image.getString("@timestamp")))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return logEvents;
|
return logEvents;
|
||||||
|
|
@ -352,6 +392,42 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
||||||
.getString("/api/query/pages/page/revisions/rev");
|
.getString("/api/query/pages/page/revisions/rev");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NonNull
|
||||||
|
public List<Notification> getNotifications() {
|
||||||
|
ApiResult notificationNode = null;
|
||||||
|
try {
|
||||||
|
notificationNode = api.action("query")
|
||||||
|
.param("notprop", "list")
|
||||||
|
.param("format", "xml")
|
||||||
|
.param("meta", "notifications")
|
||||||
|
.param("notfilter", "!read")
|
||||||
|
.get()
|
||||||
|
.getNode("/api/query/notifications/list");
|
||||||
|
} catch (IOException e) {
|
||||||
|
Timber.e("Failed to obtain searchCategories", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notificationNode == null) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Notification> notifications = new ArrayList<>();
|
||||||
|
|
||||||
|
NodeList childNodes = notificationNode.getDocument().getChildNodes();
|
||||||
|
|
||||||
|
for (int i = 0; i < childNodes.getLength(); i++) {
|
||||||
|
Node node = childNodes.item(i);
|
||||||
|
if (isCommonsNotification(node)
|
||||||
|
&& !getNotificationType(node).equals(UNKNOWN)) {
|
||||||
|
notifications.add(getNotificationFromApiResult(context, node));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return notifications;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean existingFile(String fileSha1) throws IOException {
|
public boolean existingFile(String fileSha1) throws IOException {
|
||||||
return api.action("query")
|
return api.action("query")
|
||||||
|
|
@ -387,17 +463,22 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@NonNull
|
@NonNull
|
||||||
public UploadResult uploadFile(String filename, InputStream file, long dataLength, String pageContents, String editSummary, final ProgressListener progressListener) throws IOException {
|
public UploadResult uploadFile(String filename,
|
||||||
|
@NonNull InputStream file,
|
||||||
|
long dataLength,
|
||||||
|
String pageContents,
|
||||||
|
String editSummary,
|
||||||
|
final ProgressListener progressListener) throws IOException {
|
||||||
ApiResult result = api.upload(filename, file, dataLength, pageContents, editSummary, progressListener::onProgress);
|
ApiResult result = api.upload(filename, file, dataLength, pageContents, editSummary, progressListener::onProgress);
|
||||||
|
|
||||||
Log.e("WTF", "Result: " +result.toString());
|
Log.e("WTF", "Result: " + result.toString());
|
||||||
|
|
||||||
String resultStatus = result.getString("/api/upload/@result");
|
String resultStatus = result.getString("/api/upload/@result");
|
||||||
if (!resultStatus.equals("Success")) {
|
if (!resultStatus.equals("Success")) {
|
||||||
String errorCode = result.getString("/api/error/@code");
|
String errorCode = result.getString("/api/error/@code");
|
||||||
return new UploadResult(resultStatus, errorCode);
|
return new UploadResult(resultStatus, errorCode);
|
||||||
} else {
|
} else {
|
||||||
Date dateUploaded = Utils.parseMWDate(result.getString("/api/upload/imageinfo/@timestamp"));
|
Date dateUploaded = parseMWDate(result.getString("/api/upload/imageinfo/@timestamp"));
|
||||||
String canonicalFilename = "File:" + result.getString("/api/upload/@filename").replace("_", " "); // Title vs Filename
|
String canonicalFilename = "File:" + result.getString("/api/upload/@filename").replace("_", " "); // Title vs Filename
|
||||||
String imageUrl = result.getString("/api/upload/imageinfo/@url");
|
String imageUrl = result.getString("/api/upload/imageinfo/@url");
|
||||||
return new UploadResult(resultStatus, dateUploaded, canonicalFilename, imageUrl);
|
return new UploadResult(resultStatus, dateUploaded, canonicalFilename, imageUrl);
|
||||||
|
|
@ -423,4 +504,13 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
||||||
return Integer.parseInt(uploadCount);
|
return Integer.parseInt(uploadCount);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Date parseMWDate(String mwDate) {
|
||||||
|
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH); // Assuming MW always gives me UTC
|
||||||
|
try {
|
||||||
|
return isoFormat.parse(mwDate);
|
||||||
|
} catch (ParseException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package fr.free.nrw.commons.mwapi;
|
package fr.free.nrw.commons.mwapi;
|
||||||
|
|
||||||
|
import android.content.SharedPreferences;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
|
||||||
import fr.free.nrw.commons.Utils;
|
import fr.free.nrw.commons.Utils;
|
||||||
|
|
@ -15,14 +16,14 @@ public class EventLog {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static LogBuilder schema(String schema, long revision) {
|
private static LogBuilder schema(String schema, long revision, MediaWikiApi mwApi, SharedPreferences prefs) {
|
||||||
return new LogBuilder(schema, revision);
|
return new LogBuilder(schema, revision, mwApi, prefs);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LogBuilder schema(Object[] scid) {
|
public static LogBuilder schema(Object[] scid, MediaWikiApi mwApi, SharedPreferences prefs) {
|
||||||
if (scid.length != 2) {
|
if (scid.length != 2) {
|
||||||
throw new IllegalArgumentException("Needs an object array with schema as first param and revision as second");
|
throw new IllegalArgumentException("Needs an object array with schema as first param and revision as second");
|
||||||
}
|
}
|
||||||
return schema((String) scid[0], (Long) scid[1]);
|
return schema((String) scid[0], (Long) scid[1], mwApi, prefs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package fr.free.nrw.commons.mwapi;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
|
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
@ -12,21 +11,39 @@ import java.net.MalformedURLException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
import fr.free.nrw.commons.BuildConfig;
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
import fr.free.nrw.commons.Utils;
|
import fr.free.nrw.commons.Utils;
|
||||||
import fr.free.nrw.commons.settings.Prefs;
|
import fr.free.nrw.commons.settings.Prefs;
|
||||||
|
|
||||||
|
@SuppressWarnings("WeakerAccess")
|
||||||
public class LogBuilder {
|
public class LogBuilder {
|
||||||
private JSONObject data;
|
private final MediaWikiApi mwApi;
|
||||||
private long rev;
|
private final JSONObject data;
|
||||||
private String schema;
|
private final long rev;
|
||||||
|
private final String schema;
|
||||||
|
private final SharedPreferences prefs;
|
||||||
|
|
||||||
LogBuilder(String schema, long revision) {
|
/**
|
||||||
data = new JSONObject();
|
* Main constructor of LogBuilder
|
||||||
|
*
|
||||||
|
* @param schema Log schema
|
||||||
|
* @param revision Log revision
|
||||||
|
* @param mwApi Wiki media API instance
|
||||||
|
* @param prefs Instance of SharedPreferences
|
||||||
|
*/
|
||||||
|
LogBuilder(String schema, long revision, MediaWikiApi mwApi, SharedPreferences prefs) {
|
||||||
|
this.prefs = prefs;
|
||||||
|
this.data = new JSONObject();
|
||||||
this.schema = schema;
|
this.schema = schema;
|
||||||
this.rev = revision;
|
this.rev = revision;
|
||||||
|
this.mwApi = mwApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds data to preferences
|
||||||
|
* @param key Log key
|
||||||
|
* @param value Log object value
|
||||||
|
* @return LogBuilder
|
||||||
|
*/
|
||||||
public LogBuilder param(String key, Object value) {
|
public LogBuilder param(String key, Object value) {
|
||||||
try {
|
try {
|
||||||
data.put(key, value);
|
data.put(key, value);
|
||||||
|
|
@ -36,6 +53,10 @@ public class LogBuilder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes JSON object to URL
|
||||||
|
* @return URL to JSON object
|
||||||
|
*/
|
||||||
URL toUrl() {
|
URL toUrl() {
|
||||||
JSONObject fullData = new JSONObject();
|
JSONObject fullData = new JSONObject();
|
||||||
try {
|
try {
|
||||||
|
|
@ -56,11 +77,10 @@ public class LogBuilder {
|
||||||
// Use *only* for tracking the user preference change for EventLogging
|
// Use *only* for tracking the user preference change for EventLogging
|
||||||
// Attempting to use anywhere else will cause kitten explosions
|
// Attempting to use anywhere else will cause kitten explosions
|
||||||
public void log(boolean force) {
|
public void log(boolean force) {
|
||||||
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(CommonsApplication.getInstance());
|
if (!prefs.getBoolean(Prefs.TRACKING_ENABLED, true) && !force) {
|
||||||
if (!settings.getBoolean(Prefs.TRACKING_ENABLED, true) && !force) {
|
|
||||||
return; // User has disabled tracking
|
return; // User has disabled tracking
|
||||||
}
|
}
|
||||||
LogTask logTask = new LogTask();
|
LogTask logTask = new LogTask(mwApi);
|
||||||
logTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, this);
|
logTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,26 @@ package fr.free.nrw.commons.mwapi;
|
||||||
|
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
|
|
||||||
class LogTask extends AsyncTask<LogBuilder, Void, Boolean> {
|
class LogTask extends AsyncTask<LogBuilder, Void, Boolean> {
|
||||||
|
|
||||||
|
private final MediaWikiApi mwApi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main constructor of LogTask
|
||||||
|
*
|
||||||
|
* @param mwApi Media wiki API instance
|
||||||
|
*/
|
||||||
|
public LogTask(MediaWikiApi mwApi) {
|
||||||
|
this.mwApi = mwApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs events in background
|
||||||
|
* @param logBuilders LogBuilder instance
|
||||||
|
* @return Background success state ( TRUE or FALSE )
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected Boolean doInBackground(LogBuilder... logBuilders) {
|
protected Boolean doInBackground(LogBuilder... logBuilders) {
|
||||||
return CommonsApplication.getInstance().getMWApi().logEvents(logBuilders);
|
return mwApi.logEvents(logBuilders);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,29 @@ public class MediaResult {
|
||||||
private final String wikiSource;
|
private final String wikiSource;
|
||||||
private final String parseTreeXmlSource;
|
private final String parseTreeXmlSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full-fledged constructor of MediaResult
|
||||||
|
*
|
||||||
|
* @param wikiSource Media wiki source
|
||||||
|
* @param parseTreeXmlSource Media tree parsed in XML
|
||||||
|
*/
|
||||||
MediaResult(String wikiSource, String parseTreeXmlSource) {
|
MediaResult(String wikiSource, String parseTreeXmlSource) {
|
||||||
this.wikiSource = wikiSource;
|
this.wikiSource = wikiSource;
|
||||||
this.parseTreeXmlSource = parseTreeXmlSource;
|
this.parseTreeXmlSource = parseTreeXmlSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets wiki source
|
||||||
|
* @return Wiki source
|
||||||
|
*/
|
||||||
public String getWikiSource() {
|
public String getWikiSource() {
|
||||||
return wikiSource;
|
return wikiSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets tree parsed in XML
|
||||||
|
* @return XML parsed tree
|
||||||
|
*/
|
||||||
public String getParseTreeXmlSource() {
|
public String getParseTreeXmlSource() {
|
||||||
return parseTreeXmlSource;
|
return parseTreeXmlSource;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,15 @@ import android.support.annotation.Nullable;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.notification.Notification;
|
||||||
import io.reactivex.Observable;
|
import io.reactivex.Observable;
|
||||||
import io.reactivex.Single;
|
import io.reactivex.Single;
|
||||||
|
|
||||||
public interface MediaWikiApi {
|
public interface MediaWikiApi {
|
||||||
|
String getUserAgent();
|
||||||
|
|
||||||
String getAuthCookie();
|
String getAuthCookie();
|
||||||
|
|
||||||
void setAuthCookie(String authCookie);
|
void setAuthCookie(String authCookie);
|
||||||
|
|
@ -43,6 +47,9 @@ public interface MediaWikiApi {
|
||||||
@NonNull
|
@NonNull
|
||||||
Observable<String> allCategories(String filter, int searchCatsLimit);
|
Observable<String> allCategories(String filter, int searchCatsLimit);
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
List<Notification> getNotifications() throws IOException;
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
Observable<String> searchTitles(String title, int searchCatsLimit);
|
Observable<String> searchTitles(String title, int searchCatsLimit);
|
||||||
|
|
||||||
|
|
@ -51,6 +58,8 @@ public interface MediaWikiApi {
|
||||||
|
|
||||||
boolean existingFile(String fileSha1) throws IOException;
|
boolean existingFile(String fileSha1) throws IOException;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
LogEventResult logEvents(String user, String lastModified, String queryContinue, int limit) throws IOException;
|
LogEventResult logEvents(String user, String lastModified, String queryContinue, int limit) throws IOException;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,24 @@ public class UploadResult {
|
||||||
private String imageUrl;
|
private String imageUrl;
|
||||||
private String canonicalFilename;
|
private String canonicalFilename;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal constructor
|
||||||
|
*
|
||||||
|
* @param resultStatus Upload result status
|
||||||
|
* @param errorCode Upload error code
|
||||||
|
*/
|
||||||
UploadResult(String resultStatus, String errorCode) {
|
UploadResult(String resultStatus, String errorCode) {
|
||||||
this.resultStatus = resultStatus;
|
this.resultStatus = resultStatus;
|
||||||
this.errorCode = errorCode;
|
this.errorCode = errorCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full-fledged constructor
|
||||||
|
* @param resultStatus Upload result status
|
||||||
|
* @param dateUploaded Uploaded date
|
||||||
|
* @param canonicalFilename Uploaded file name
|
||||||
|
* @param imageUrl Uploaded image file name
|
||||||
|
*/
|
||||||
UploadResult(String resultStatus, Date dateUploaded, String canonicalFilename, String imageUrl) {
|
UploadResult(String resultStatus, Date dateUploaded, String canonicalFilename, String imageUrl) {
|
||||||
this.resultStatus = resultStatus;
|
this.resultStatus = resultStatus;
|
||||||
this.dateUploaded = dateUploaded;
|
this.dateUploaded = dateUploaded;
|
||||||
|
|
@ -21,22 +34,42 @@ public class UploadResult {
|
||||||
this.imageUrl = imageUrl;
|
this.imageUrl = imageUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets uploaded date
|
||||||
|
* @return Upload date
|
||||||
|
*/
|
||||||
public Date getDateUploaded() {
|
public Date getDateUploaded() {
|
||||||
return dateUploaded;
|
return dateUploaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets image url
|
||||||
|
* @return Uploaded image url
|
||||||
|
*/
|
||||||
public String getImageUrl() {
|
public String getImageUrl() {
|
||||||
return imageUrl;
|
return imageUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets canonical file name
|
||||||
|
* @return Uploaded file name
|
||||||
|
*/
|
||||||
public String getCanonicalFilename() {
|
public String getCanonicalFilename() {
|
||||||
return canonicalFilename;
|
return canonicalFilename;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets upload error code
|
||||||
|
* @return Error code
|
||||||
|
*/
|
||||||
public String getErrorCode() {
|
public String getErrorCode() {
|
||||||
return errorCode;
|
return errorCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets upload result status
|
||||||
|
* @return Upload result status
|
||||||
|
*/
|
||||||
public String getResultStatus() {
|
public String getResultStatus() {
|
||||||
return resultStatus;
|
return resultStatus;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,15 @@
|
||||||
package fr.free.nrw.commons.nearby;
|
package fr.free.nrw.commons.nearby;
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.location.LocationManager;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.v4.app.ActivityCompat;
|
|
||||||
import android.support.v4.app.Fragment;
|
import android.support.v4.app.Fragment;
|
||||||
import android.support.v4.app.FragmentTransaction;
|
import android.support.v4.app.FragmentTransaction;
|
||||||
import android.support.v4.content.ContextCompat;
|
|
||||||
import android.support.v7.app.AlertDialog;
|
import android.support.v7.app.AlertDialog;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
|
|
@ -29,30 +23,43 @@ import com.google.gson.GsonBuilder;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import butterknife.BindView;
|
import butterknife.BindView;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.location.LatLng;
|
import fr.free.nrw.commons.location.LatLng;
|
||||||
import fr.free.nrw.commons.location.LocationServiceManager;
|
import fr.free.nrw.commons.location.LocationServiceManager;
|
||||||
|
import fr.free.nrw.commons.location.LocationUpdateListener;
|
||||||
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
||||||
import fr.free.nrw.commons.utils.UriSerializer;
|
import fr.free.nrw.commons.utils.UriSerializer;
|
||||||
|
import fr.free.nrw.commons.utils.ViewUtil;
|
||||||
|
import io.reactivex.Observable;
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.disposables.Disposable;
|
||||||
|
import io.reactivex.schedulers.Schedulers;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
|
||||||
public class NearbyActivity extends NavigationBaseActivity {
|
public class NearbyActivity extends NavigationBaseActivity implements LocationUpdateListener {
|
||||||
|
|
||||||
@BindView(R.id.progressBar)
|
|
||||||
ProgressBar progressBar;
|
|
||||||
private static final int LOCATION_REQUEST = 1;
|
private static final int LOCATION_REQUEST = 1;
|
||||||
private static final String MAP_LAST_USED_PREFERENCE = "mapLastUsed";
|
private static final String MAP_LAST_USED_PREFERENCE = "mapLastUsed";
|
||||||
|
|
||||||
private LocationServiceManager locationManager;
|
@BindView(R.id.progressBar)
|
||||||
|
ProgressBar progressBar;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
LocationServiceManager locationManager;
|
||||||
|
@Inject
|
||||||
|
NearbyController nearbyController;
|
||||||
|
|
||||||
private LatLng curLatLang;
|
private LatLng curLatLang;
|
||||||
private Bundle bundle;
|
private Bundle bundle;
|
||||||
private NearbyAsyncTask nearbyAsyncTask;
|
|
||||||
private SharedPreferences sharedPreferences;
|
private SharedPreferences sharedPreferences;
|
||||||
private NearbyActivityMode viewMode;
|
private NearbyActivityMode viewMode;
|
||||||
|
private Disposable placesDisposable;
|
||||||
|
private boolean lockNearbyView; //Determines if the nearby places needs to be refreshed
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
|
@ -60,7 +67,6 @@ public class NearbyActivity extends NavigationBaseActivity {
|
||||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
|
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
|
||||||
setContentView(R.layout.activity_nearby);
|
setContentView(R.layout.activity_nearby);
|
||||||
ButterKnife.bind(this);
|
ButterKnife.bind(this);
|
||||||
checkLocationPermission();
|
|
||||||
bundle = new Bundle();
|
bundle = new Bundle();
|
||||||
initDrawer();
|
initDrawer();
|
||||||
initViewState();
|
initViewState();
|
||||||
|
|
@ -92,7 +98,8 @@ public class NearbyActivity extends NavigationBaseActivity {
|
||||||
// Handle item selection
|
// Handle item selection
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
case R.id.action_refresh:
|
case R.id.action_refresh:
|
||||||
refreshView();
|
lockNearbyView(false);
|
||||||
|
refreshView(true);
|
||||||
return true;
|
return true;
|
||||||
case R.id.action_toggle_view:
|
case R.id.action_toggle_view:
|
||||||
viewMode = viewMode.toggle();
|
viewMode = viewMode.toggle();
|
||||||
|
|
@ -104,60 +111,9 @@ public class NearbyActivity extends NavigationBaseActivity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startLookingForNearby() {
|
private void requestLocationPermissions() {
|
||||||
locationManager = new LocationServiceManager(this);
|
if (!isFinishing()) {
|
||||||
locationManager.registerLocationManager();
|
locationManager.requestPermissions(this);
|
||||||
curLatLang = locationManager.getLatestLocation();
|
|
||||||
nearbyAsyncTask = new NearbyAsyncTask(this);
|
|
||||||
nearbyAsyncTask.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkLocationPermission() {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
if (ContextCompat.checkSelfPermission(this,
|
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
|
|
||||||
startLookingForNearby();
|
|
||||||
} else {
|
|
||||||
if (ContextCompat.checkSelfPermission(this,
|
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION)
|
|
||||||
!= PackageManager.PERMISSION_GRANTED) {
|
|
||||||
|
|
||||||
// Should we show an explanation?
|
|
||||||
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
|
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION)) {
|
|
||||||
|
|
||||||
// Show an explanation to the user *asynchronously* -- don't block
|
|
||||||
// this thread waiting for the user's response! After the user
|
|
||||||
// sees the explanation, try again to request the permission.
|
|
||||||
|
|
||||||
new AlertDialog.Builder(this)
|
|
||||||
.setMessage(getString(R.string.location_permission_rationale))
|
|
||||||
.setPositiveButton("OK", (dialog, which) -> {
|
|
||||||
ActivityCompat.requestPermissions(NearbyActivity.this,
|
|
||||||
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
|
|
||||||
LOCATION_REQUEST);
|
|
||||||
dialog.dismiss();
|
|
||||||
})
|
|
||||||
.setNegativeButton("Cancel", null)
|
|
||||||
.create()
|
|
||||||
.show();
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// No explanation needed, we can request the permission.
|
|
||||||
|
|
||||||
ActivityCompat.requestPermissions(this,
|
|
||||||
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
|
|
||||||
LOCATION_REQUEST);
|
|
||||||
|
|
||||||
// MY_PERMISSIONS_REQUEST_READ_CONTACTS is an
|
|
||||||
// app-defined int constant. The callback method gets the
|
|
||||||
// result of the request.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
startLookingForNearby();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -166,16 +122,10 @@ public class NearbyActivity extends NavigationBaseActivity {
|
||||||
switch (requestCode) {
|
switch (requestCode) {
|
||||||
case LOCATION_REQUEST: {
|
case LOCATION_REQUEST: {
|
||||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
startLookingForNearby();
|
refreshView(false);
|
||||||
} else {
|
} else {
|
||||||
//If permission not granted, go to page that says Nearby Places cannot be displayed
|
//If permission not granted, go to page that says Nearby Places cannot be displayed
|
||||||
if (nearbyAsyncTask != null) {
|
hideProgressBar();
|
||||||
nearbyAsyncTask.cancel(true);
|
|
||||||
}
|
|
||||||
if (progressBar != null) {
|
|
||||||
progressBar.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
showLocationPermissionDeniedErrorDialog();
|
showLocationPermissionDeniedErrorDialog();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -188,7 +138,7 @@ public class NearbyActivity extends NavigationBaseActivity {
|
||||||
.setCancelable(false)
|
.setCancelable(false)
|
||||||
.setPositiveButton(R.string.give_permission, (dialog, which) -> {
|
.setPositiveButton(R.string.give_permission, (dialog, which) -> {
|
||||||
//will ask for the location permission again
|
//will ask for the location permission again
|
||||||
checkLocationPermission();
|
checkGps();
|
||||||
})
|
})
|
||||||
.setNegativeButton(R.string.cancel, (dialog, which) -> {
|
.setNegativeButton(R.string.cancel, (dialog, which) -> {
|
||||||
//dismiss dialog and finish activity
|
//dismiss dialog and finish activity
|
||||||
|
|
@ -200,8 +150,7 @@ public class NearbyActivity extends NavigationBaseActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkGps() {
|
private void checkGps() {
|
||||||
LocationManager manager = (LocationManager) getSystemService(LOCATION_SERVICE);
|
if (!locationManager.isProviderEnabled()) {
|
||||||
if (!manager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
|
|
||||||
Timber.d("GPS is not enabled");
|
Timber.d("GPS is not enabled");
|
||||||
new AlertDialog.Builder(this)
|
new AlertDialog.Builder(this)
|
||||||
.setMessage(R.string.gps_disabled)
|
.setMessage(R.string.gps_disabled)
|
||||||
|
|
@ -213,11 +162,48 @@ public class NearbyActivity extends NavigationBaseActivity {
|
||||||
Timber.d("Loaded settings page");
|
Timber.d("Loaded settings page");
|
||||||
startActivityForResult(callGPSSettingIntent, 1);
|
startActivityForResult(callGPSSettingIntent, 1);
|
||||||
})
|
})
|
||||||
.setNegativeButton(R.string.menu_cancel_upload, (dialog, id) -> dialog.cancel())
|
.setNegativeButton(R.string.menu_cancel_upload, (dialog, id) -> {
|
||||||
|
showLocationPermissionDeniedErrorDialog();
|
||||||
|
dialog.cancel();
|
||||||
|
})
|
||||||
.create()
|
.create()
|
||||||
.show();
|
.show();
|
||||||
} else {
|
} else {
|
||||||
Timber.d("GPS is enabled");
|
Timber.d("GPS is enabled");
|
||||||
|
checkLocationPermission();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkLocationPermission() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
if (locationManager.isLocationPermissionGranted()) {
|
||||||
|
refreshView(false);
|
||||||
|
} else {
|
||||||
|
// Should we show an explanation?
|
||||||
|
if (locationManager.isPermissionExplanationRequired(this)) {
|
||||||
|
// Show an explanation to the user *asynchronously* -- don't block
|
||||||
|
// this thread waiting for the user's response! After the user
|
||||||
|
// sees the explanation, try again to request the permission.
|
||||||
|
new AlertDialog.Builder(this)
|
||||||
|
.setMessage(getString(R.string.location_permission_rationale_nearby))
|
||||||
|
.setPositiveButton("OK", (dialog, which) -> {
|
||||||
|
requestLocationPermissions();
|
||||||
|
dialog.dismiss();
|
||||||
|
})
|
||||||
|
.setNegativeButton("Cancel", (dialog, id) -> {
|
||||||
|
showLocationPermissionDeniedErrorDialog();
|
||||||
|
dialog.cancel();
|
||||||
|
})
|
||||||
|
.create()
|
||||||
|
.show();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// No explanation needed, we can request the permission.
|
||||||
|
requestLocationPermissions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
refreshView(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -226,78 +212,80 @@ public class NearbyActivity extends NavigationBaseActivity {
|
||||||
super.onActivityResult(requestCode, resultCode, data);
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
if (requestCode == 1) {
|
if (requestCode == 1) {
|
||||||
Timber.d("User is back from Settings page");
|
Timber.d("User is back from Settings page");
|
||||||
refreshView();
|
refreshView(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void toggleView() {
|
private void toggleView() {
|
||||||
if (nearbyAsyncTask != null) {
|
|
||||||
if (nearbyAsyncTask.getStatus() == AsyncTask.Status.FINISHED) {
|
|
||||||
if (viewMode.isMap()) {
|
if (viewMode.isMap()) {
|
||||||
setMapFragment();
|
setMapFragment();
|
||||||
} else {
|
} else {
|
||||||
setListFragment();
|
setListFragment();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
sharedPreferences.edit().putBoolean(MAP_LAST_USED_PREFERENCE, viewMode.isMap()).apply();
|
sharedPreferences.edit().putBoolean(MAP_LAST_USED_PREFERENCE, viewMode.isMap()).apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
locationManager.addLocationListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
locationManager.removeLocationListener(this);
|
||||||
|
locationManager.unregisterLocationManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
if (placesDisposable != null) {
|
||||||
|
placesDisposable.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
lockNearbyView = false;
|
||||||
checkGps();
|
checkGps();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
/**
|
||||||
protected void onPause() {
|
* This method should be the single point to load/refresh nearby places
|
||||||
super.onPause();
|
*
|
||||||
if (nearbyAsyncTask != null) {
|
* @param isHardRefresh Should display a toast if the location hasn't changed
|
||||||
nearbyAsyncTask.cancel(true);
|
*/
|
||||||
|
private void refreshView(boolean isHardRefresh) {
|
||||||
|
if (lockNearbyView) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
locationManager.registerLocationManager();
|
||||||
|
LatLng lastLocation = locationManager.getLastLocation();
|
||||||
|
if (curLatLang != null && curLatLang.equals(lastLocation)) { //refresh view only if location has changed
|
||||||
|
if (isHardRefresh) {
|
||||||
|
ViewUtil.showLongToast(this, R.string.nearby_location_has_not_changed);
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
private void refreshView() {
|
|
||||||
nearbyAsyncTask = new NearbyAsyncTask(this);
|
|
||||||
nearbyAsyncTask.execute();
|
|
||||||
}
|
}
|
||||||
|
curLatLang = lastLocation;
|
||||||
|
|
||||||
@Override
|
if (curLatLang == null) {
|
||||||
protected void onDestroy() {
|
Timber.d("Skipping update of nearby places as location is unavailable");
|
||||||
super.onDestroy();
|
|
||||||
if (locationManager != null) {
|
|
||||||
locationManager.unregisterLocationManager();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class NearbyAsyncTask extends AsyncTask<Void, Integer, List<Place>> {
|
|
||||||
|
|
||||||
private final Context mContext;
|
|
||||||
|
|
||||||
private NearbyAsyncTask(Context context) {
|
|
||||||
mContext = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onProgressUpdate(Integer... values) {
|
|
||||||
super.onProgressUpdate(values);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<Place> doInBackground(Void... params) {
|
|
||||||
return NearbyController
|
|
||||||
.loadAttractionsFromLocation(curLatLang, CommonsApplication.getInstance()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(List<Place> placeList) {
|
|
||||||
super.onPostExecute(placeList);
|
|
||||||
|
|
||||||
if (isCancelled()) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
progressBar.setVisibility(View.VISIBLE);
|
||||||
|
placesDisposable = Observable.fromCallable(() -> nearbyController
|
||||||
|
.loadAttractionsFromLocation(curLatLang, this))
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(this::populatePlaces);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void populatePlaces(List<Place> placeList) {
|
||||||
Gson gson = new GsonBuilder()
|
Gson gson = new GsonBuilder()
|
||||||
.registerTypeAdapter(Uri.class, new UriSerializer())
|
.registerTypeAdapter(Uri.class, new UriSerializer())
|
||||||
.create();
|
.create();
|
||||||
|
|
@ -306,7 +294,7 @@ public class NearbyActivity extends NavigationBaseActivity {
|
||||||
|
|
||||||
if (placeList.size() == 0) {
|
if (placeList.size() == 0) {
|
||||||
int duration = Toast.LENGTH_SHORT;
|
int duration = Toast.LENGTH_SHORT;
|
||||||
Toast toast = Toast.makeText(mContext, R.string.no_nearby, duration);
|
Toast toast = Toast.makeText(this, R.string.no_nearby, duration);
|
||||||
toast.show();
|
toast.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -314,6 +302,7 @@ public class NearbyActivity extends NavigationBaseActivity {
|
||||||
bundle.putString("PlaceList", gsonPlaceList);
|
bundle.putString("PlaceList", gsonPlaceList);
|
||||||
bundle.putString("CurLatLng", gsonCurLatLng);
|
bundle.putString("CurLatLng", gsonCurLatLng);
|
||||||
|
|
||||||
|
lockNearbyView(true);
|
||||||
// Begin the transaction
|
// Begin the transaction
|
||||||
if (viewMode.isMap()) {
|
if (viewMode.isMap()) {
|
||||||
setMapFragment();
|
setMapFragment();
|
||||||
|
|
@ -321,11 +310,26 @@ public class NearbyActivity extends NavigationBaseActivity {
|
||||||
setListFragment();
|
setListFragment();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hideProgressBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void lockNearbyView(boolean lock) {
|
||||||
|
if (lock) {
|
||||||
|
lockNearbyView = true;
|
||||||
|
locationManager.unregisterLocationManager();
|
||||||
|
locationManager.removeLocationListener(this);
|
||||||
|
} else {
|
||||||
|
lockNearbyView = false;
|
||||||
|
locationManager.registerLocationManager();
|
||||||
|
locationManager.addLocationListener(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideProgressBar() {
|
||||||
if (progressBar != null) {
|
if (progressBar != null) {
|
||||||
progressBar.setVisibility(View.GONE);
|
progressBar.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls fragment for map view.
|
* Calls fragment for map view.
|
||||||
|
|
@ -334,7 +338,7 @@ public class NearbyActivity extends NavigationBaseActivity {
|
||||||
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
|
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
|
||||||
Fragment fragment = new NearbyMapFragment();
|
Fragment fragment = new NearbyMapFragment();
|
||||||
fragment.setArguments(bundle);
|
fragment.setArguments(bundle);
|
||||||
fragmentTransaction.replace(R.id.container, fragment);
|
fragmentTransaction.replace(R.id.container, fragment, fragment.getClass().getSimpleName());
|
||||||
fragmentTransaction.commitAllowingStateLoss();
|
fragmentTransaction.commitAllowingStateLoss();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -345,12 +349,12 @@ public class NearbyActivity extends NavigationBaseActivity {
|
||||||
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
|
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
|
||||||
Fragment fragment = new NearbyListFragment();
|
Fragment fragment = new NearbyListFragment();
|
||||||
fragment.setArguments(bundle);
|
fragment.setArguments(bundle);
|
||||||
fragmentTransaction.replace(R.id.container, fragment);
|
fragmentTransaction.replace(R.id.container, fragment, fragment.getClass().getSimpleName());
|
||||||
fragmentTransaction.commitAllowingStateLoss();
|
fragmentTransaction.commitAllowingStateLoss();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void startYourself(Context context) {
|
@Override
|
||||||
Intent settingsIntent = new Intent(context, NearbyActivity.class);
|
public void onLocationChanged(LatLng latLng) {
|
||||||
context.startActivity(settingsIntent);
|
refreshView(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package fr.free.nrw.commons.nearby;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.support.graphics.drawable.VectorDrawableCompat;
|
import android.support.graphics.drawable.VectorDrawableCompat;
|
||||||
|
|
||||||
import com.mapbox.mapboxsdk.annotations.IconFactory;
|
import com.mapbox.mapboxsdk.annotations.IconFactory;
|
||||||
|
|
@ -15,7 +14,9 @@ import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.location.LatLng;
|
import fr.free.nrw.commons.location.LatLng;
|
||||||
import fr.free.nrw.commons.utils.UiUtils;
|
import fr.free.nrw.commons.utils.UiUtils;
|
||||||
|
|
@ -24,30 +25,36 @@ import timber.log.Timber;
|
||||||
import static fr.free.nrw.commons.utils.LengthUtils.computeDistanceBetween;
|
import static fr.free.nrw.commons.utils.LengthUtils.computeDistanceBetween;
|
||||||
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
|
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
|
||||||
|
|
||||||
|
|
||||||
public class NearbyController {
|
public class NearbyController {
|
||||||
private static final int MAX_RESULTS = 1000;
|
private static final int MAX_RESULTS = 1000;
|
||||||
|
private final NearbyPlaces nearbyPlaces;
|
||||||
|
private final SharedPreferences prefs;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public NearbyController(NearbyPlaces nearbyPlaces,
|
||||||
|
@Named("default_preferences") SharedPreferences prefs) {
|
||||||
|
this.nearbyPlaces = nearbyPlaces;
|
||||||
|
this.prefs = prefs;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepares Place list to make their distance information update later.
|
* Prepares Place list to make their distance information update later.
|
||||||
|
*
|
||||||
* @param curLatLng current location for user
|
* @param curLatLng current location for user
|
||||||
* @param context context
|
* @param context context
|
||||||
* @return Place list without distance information
|
* @return Place list without distance information
|
||||||
*/
|
*/
|
||||||
public static List<Place> loadAttractionsFromLocation(LatLng curLatLng, Context context) {
|
public List<Place> loadAttractionsFromLocation(LatLng curLatLng, Context context) {
|
||||||
Timber.d("Loading attractions near %s", curLatLng);
|
Timber.d("Loading attractions near %s", curLatLng);
|
||||||
if (curLatLng == null) {
|
if (curLatLng == null) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
NearbyPlaces nearbyPlaces = CommonsApplication.getInstance().getNearbyPlaces();
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
|
||||||
List<Place> places = prefs.getBoolean("useWikidata", true)
|
List<Place> places = prefs.getBoolean("useWikidata", true)
|
||||||
? nearbyPlaces.getFromWikidataQuery(curLatLng, Locale.getDefault().getLanguage())
|
? nearbyPlaces.getFromWikidataQuery(curLatLng, Locale.getDefault().getLanguage())
|
||||||
: nearbyPlaces.getFromWikiNeedsPictures();
|
: nearbyPlaces.getFromWikiNeedsPictures();
|
||||||
if (curLatLng != null) {
|
|
||||||
Timber.d("Sorting places by distance...");
|
Timber.d("Sorting places by distance...");
|
||||||
final Map<Place, Double> distances = new HashMap<>();
|
final Map<Place, Double> distances = new HashMap<>();
|
||||||
for (Place place: places) {
|
for (Place place : places) {
|
||||||
distances.put(place, computeDistanceBetween(place.location, curLatLng));
|
distances.put(place, computeDistanceBetween(place.location, curLatLng));
|
||||||
}
|
}
|
||||||
Collections.sort(places,
|
Collections.sort(places,
|
||||||
|
|
@ -57,12 +64,12 @@ public class NearbyController {
|
||||||
return (int) (lhsDistance - rhsDistance);
|
return (int) (lhsDistance - rhsDistance);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
|
||||||
return places;
|
return places;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads attractions from location for list view, we need to return Place data type.
|
* Loads attractions from location for list view, we need to return Place data type.
|
||||||
|
*
|
||||||
* @param curLatLng users current location
|
* @param curLatLng users current location
|
||||||
* @param placeList list of nearby places in Place data type
|
* @param placeList list of nearby places in Place data type
|
||||||
* @return Place list that holds nearby places
|
* @return Place list that holds nearby places
|
||||||
|
|
@ -71,7 +78,7 @@ public class NearbyController {
|
||||||
LatLng curLatLng,
|
LatLng curLatLng,
|
||||||
List<Place> placeList) {
|
List<Place> placeList) {
|
||||||
placeList = placeList.subList(0, Math.min(placeList.size(), MAX_RESULTS));
|
placeList = placeList.subList(0, Math.min(placeList.size(), MAX_RESULTS));
|
||||||
for (Place place: placeList) {
|
for (Place place : placeList) {
|
||||||
String distance = formatDistanceBetween(curLatLng, place.location);
|
String distance = formatDistanceBetween(curLatLng, place.location);
|
||||||
place.setDistance(distance);
|
place.setDistance(distance);
|
||||||
}
|
}
|
||||||
|
|
@ -79,7 +86,8 @@ public class NearbyController {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*Loads attractions from location for map view, we need to return BaseMarkerOption data type.
|
* Loads attractions from location for map view, we need to return BaseMarkerOption data type.
|
||||||
|
*
|
||||||
* @param curLatLng users current location
|
* @param curLatLng users current location
|
||||||
* @param placeList list of nearby places in Place data type
|
* @param placeList list of nearby places in Place data type
|
||||||
* @return BaseMarkerOptions list that holds nearby places
|
* @return BaseMarkerOptions list that holds nearby places
|
||||||
|
|
@ -96,12 +104,13 @@ public class NearbyController {
|
||||||
|
|
||||||
placeList = placeList.subList(0, Math.min(placeList.size(), MAX_RESULTS));
|
placeList = placeList.subList(0, Math.min(placeList.size(), MAX_RESULTS));
|
||||||
|
|
||||||
Bitmap icon = UiUtils.getBitmap(
|
VectorDrawableCompat vectorDrawable = VectorDrawableCompat.create(
|
||||||
VectorDrawableCompat.create(
|
|
||||||
context.getResources(), R.drawable.ic_custom_map_marker, context.getTheme()
|
context.getResources(), R.drawable.ic_custom_map_marker, context.getTheme()
|
||||||
));
|
);
|
||||||
|
if (vectorDrawable != null) {
|
||||||
|
Bitmap icon = UiUtils.getBitmap(vectorDrawable);
|
||||||
|
|
||||||
for (Place place: placeList) {
|
for (Place place : placeList) {
|
||||||
String distance = formatDistanceBetween(curLatLng, place.location);
|
String distance = formatDistanceBetween(curLatLng, place.location);
|
||||||
place.setDistance(distance);
|
place.setDistance(distance);
|
||||||
|
|
||||||
|
|
@ -117,6 +126,7 @@ public class NearbyController {
|
||||||
|
|
||||||
baseMarkerOptions.add(nearbyBaseMarker);
|
baseMarkerOptions.add(nearbyBaseMarker);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return baseMarkerOptions;
|
return baseMarkerOptions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ public class NearbyInfoDialog extends OverlayDialog {
|
||||||
NearbyInfoDialog mDialog = new NearbyInfoDialog();
|
NearbyInfoDialog mDialog = new NearbyInfoDialog();
|
||||||
Bundle bundle = new Bundle();
|
Bundle bundle = new Bundle();
|
||||||
bundle.putString(ARG_TITLE, place.name);
|
bundle.putString(ARG_TITLE, place.name);
|
||||||
bundle.putString(ARG_DESC, place.getDescription().getText());
|
bundle.putString(ARG_DESC, place.getLongDescription());
|
||||||
bundle.putDouble(ARG_LATITUDE, place.location.getLatitude());
|
bundle.putDouble(ARG_LATITUDE, place.location.getLatitude());
|
||||||
bundle.putDouble(ARG_LONGITUDE, place.location.getLongitude());
|
bundle.putDouble(ARG_LONGITUDE, place.location.getLongitude());
|
||||||
bundle.putParcelable(ARG_SITE_LINK, place.siteLinks);
|
bundle.putParcelable(ARG_SITE_LINK, place.siteLinks);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package fr.free.nrw.commons.nearby;
|
package fr.free.nrw.commons.nearby;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.app.Fragment;
|
import android.support.v4.app.Fragment;
|
||||||
|
|
@ -17,6 +18,7 @@ import java.lang.reflect.Type;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import dagger.android.support.AndroidSupportInjection;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.location.LatLng;
|
import fr.free.nrw.commons.location.LatLng;
|
||||||
import fr.free.nrw.commons.utils.UriDeserializer;
|
import fr.free.nrw.commons.utils.UriDeserializer;
|
||||||
|
|
@ -40,6 +42,12 @@ public class NearbyListFragment extends Fragment {
|
||||||
setRetainInstance(true);
|
setRetainInstance(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttach(Context context) {
|
||||||
|
AndroidSupportInjection.inject(this);
|
||||||
|
super.onAttach(context);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater,
|
public View onCreateView(LayoutInflater inflater,
|
||||||
ViewGroup container,
|
ViewGroup container,
|
||||||
|
|
@ -60,7 +68,7 @@ public class NearbyListFragment extends Fragment {
|
||||||
|
|
||||||
Bundle bundle = this.getArguments();
|
Bundle bundle = this.getArguments();
|
||||||
if (bundle != null) {
|
if (bundle != null) {
|
||||||
String gsonPlaceList = bundle.getString("PlaceList");
|
String gsonPlaceList = bundle.getString("PlaceList", "[]");
|
||||||
placeList = gson.fromJson(gsonPlaceList, LIST_TYPE);
|
placeList = gson.fromJson(gsonPlaceList, LIST_TYPE);
|
||||||
|
|
||||||
String gsonLatLng = bundle.getString("CurLatLng");
|
String gsonLatLng = bundle.getString("CurLatLng");
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package fr.free.nrw.commons.nearby;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
@ -77,6 +76,8 @@ public class NearbyMapFragment extends android.support.v4.app.Fragment {
|
||||||
private void setupMapView(Bundle savedInstanceState) {
|
private void setupMapView(Bundle savedInstanceState) {
|
||||||
MapboxMapOptions options = new MapboxMapOptions()
|
MapboxMapOptions options = new MapboxMapOptions()
|
||||||
.styleUrl(Style.OUTDOORS)
|
.styleUrl(Style.OUTDOORS)
|
||||||
|
.logoEnabled(false)
|
||||||
|
.attributionEnabled(false)
|
||||||
.camera(new CameraPosition.Builder()
|
.camera(new CameraPosition.Builder()
|
||||||
.target(new LatLng(curLatLng.getLatitude(), curLatLng.getLongitude()))
|
.target(new LatLng(curLatLng.getLatitude(), curLatLng.getLongitude()))
|
||||||
.zoom(11)
|
.zoom(11)
|
||||||
|
|
@ -99,11 +100,8 @@ public class NearbyMapFragment extends android.support.v4.app.Fragment {
|
||||||
|
|
||||||
addCurrentLocationMarker(mapboxMap);
|
addCurrentLocationMarker(mapboxMap);
|
||||||
});
|
});
|
||||||
if (PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean("theme",false)) {
|
|
||||||
mapView.setStyleUrl(getResources().getString(R.string.map_theme_dark));
|
mapView.setStyleUrl("asset://mapstyle.json");
|
||||||
} else {
|
|
||||||
mapView.setStyleUrl(getResources().getString(R.string.map_theme_light));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ public class NearbyPlaces {
|
||||||
|
|
||||||
public NearbyPlaces() {
|
public NearbyPlaces() {
|
||||||
try {
|
try {
|
||||||
wikidataQuery = FileUtils.readFromResource("/assets/queries/nearby_query.rq");
|
wikidataQuery = FileUtils.readFromResource("/queries/nearby_query.rq");
|
||||||
Timber.v(wikidataQuery);
|
Timber.v(wikidataQuery);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
|
|
@ -46,7 +46,7 @@ public class NearbyPlaces {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// increase the radius gradually to find a satisfactory number of nearby places
|
// increase the radius gradually to find a satisfactory number of nearby places
|
||||||
while (radius < MAX_RADIUS) {
|
while (radius <= MAX_RADIUS) {
|
||||||
places = getFromWikidataQuery(curLatLng, lang, radius);
|
places = getFromWikidataQuery(curLatLng, lang, radius);
|
||||||
Timber.d("%d results at radius: %f", places.size(), radius);
|
Timber.d("%d results at radius: %f", places.size(), radius);
|
||||||
if (places.size() >= MIN_RESULTS) {
|
if (places.size() >= MIN_RESULTS) {
|
||||||
|
|
@ -62,6 +62,11 @@ public class NearbyPlaces {
|
||||||
Timber.d("back to initial radius: %f", radius);
|
Timber.d("back to initial radius: %f", radius);
|
||||||
radius = INITIAL_RADIUS;
|
radius = INITIAL_RADIUS;
|
||||||
}
|
}
|
||||||
|
// make sure we will be able to send at least one request next time
|
||||||
|
if (radius > MAX_RADIUS) {
|
||||||
|
radius = MAX_RADIUS;
|
||||||
|
}
|
||||||
|
|
||||||
return places;
|
return places;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,7 +126,7 @@ public class NearbyPlaces {
|
||||||
|
|
||||||
places.add(new Place(
|
places.add(new Place(
|
||||||
name,
|
name,
|
||||||
Place.Description.fromText(type), // list
|
Place.Label.fromText(type), // list
|
||||||
type, // details
|
type, // details
|
||||||
Uri.parse(icon),
|
Uri.parse(icon),
|
||||||
new LatLng(latitude, longitude, 0),
|
new LatLng(latitude, longitude, 0),
|
||||||
|
|
@ -183,7 +188,7 @@ public class NearbyPlaces {
|
||||||
|
|
||||||
places.add(new Place(
|
places.add(new Place(
|
||||||
name,
|
name,
|
||||||
Place.Description.fromText(type), // list
|
Place.Label.fromText(type), // list
|
||||||
type, // details
|
type, // details
|
||||||
null,
|
null,
|
||||||
new LatLng(latitude, longitude, 0),
|
new LatLng(latitude, longitude, 0),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package fr.free.nrw.commons.nearby;
|
package fr.free.nrw.commons.nearby;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.app.Fragment;
|
import android.support.v4.app.Fragment;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
|
|
@ -7,6 +8,7 @@ import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
|
import dagger.android.support.AndroidSupportInjection;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
|
@ -18,6 +20,12 @@ public class NoPermissionsFragment extends Fragment {
|
||||||
public NoPermissionsFragment() {
|
public NoPermissionsFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttach(Context context) {
|
||||||
|
AndroidSupportInjection.inject(this);
|
||||||
|
super.onAttach(context);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||||
Bundle savedInstanceState) {
|
Bundle savedInstanceState) {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import fr.free.nrw.commons.location.LatLng;
|
||||||
public class Place {
|
public class Place {
|
||||||
|
|
||||||
public final String name;
|
public final String name;
|
||||||
private final Description description;
|
private final Label label;
|
||||||
private final String longDescription;
|
private final String longDescription;
|
||||||
private final Uri secondaryImageUrl;
|
private final Uri secondaryImageUrl;
|
||||||
public final LatLng location;
|
public final LatLng location;
|
||||||
|
|
@ -24,18 +24,22 @@ public class Place {
|
||||||
public final Sitelinks siteLinks;
|
public final Sitelinks siteLinks;
|
||||||
|
|
||||||
|
|
||||||
public Place(String name, Description description, String longDescription,
|
public Place(String name, Label label, String longDescription,
|
||||||
Uri secondaryImageUrl, LatLng location, Sitelinks siteLinks) {
|
Uri secondaryImageUrl, LatLng location, Sitelinks siteLinks) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.description = description;
|
this.label = label;
|
||||||
this.longDescription = longDescription;
|
this.longDescription = longDescription;
|
||||||
this.secondaryImageUrl = secondaryImageUrl;
|
this.secondaryImageUrl = secondaryImageUrl;
|
||||||
this.location = location;
|
this.location = location;
|
||||||
this.siteLinks = siteLinks;
|
this.siteLinks = siteLinks;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Description getDescription() {
|
public Label getLabel() {
|
||||||
return description;
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLongDescription() {
|
||||||
|
return longDescription;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDistance(String distance) {
|
public void setDistance(String distance) {
|
||||||
|
|
@ -67,10 +71,8 @@ public class Place {
|
||||||
* Most common types of desc: building, house, cottage, farmhouse,
|
* Most common types of desc: building, house, cottage, farmhouse,
|
||||||
* village, civil parish, church, railway station,
|
* village, civil parish, church, railway station,
|
||||||
* gatehouse, milestone, inn, secondary school, hotel
|
* gatehouse, milestone, inn, secondary school, hotel
|
||||||
*
|
|
||||||
* TODO Give a more accurate class name (see issue #742).
|
|
||||||
*/
|
*/
|
||||||
public enum Description {
|
public enum Label {
|
||||||
|
|
||||||
BUILDING("building", R.drawable.round_icon_generic_building),
|
BUILDING("building", R.drawable.round_icon_generic_building),
|
||||||
HOUSE("house", R.drawable.round_icon_house),
|
HOUSE("house", R.drawable.round_icon_house),
|
||||||
|
|
@ -95,19 +97,19 @@ public class Place {
|
||||||
WATERFALL("waterfall", R.drawable.round_icon_waterfall),
|
WATERFALL("waterfall", R.drawable.round_icon_waterfall),
|
||||||
UNKNOWN("?", R.drawable.round_icon_unknown);
|
UNKNOWN("?", R.drawable.round_icon_unknown);
|
||||||
|
|
||||||
private static final Map<String, Description> TEXT_TO_DESCRIPTION
|
private static final Map<String, Label> TEXT_TO_DESCRIPTION
|
||||||
= new HashMap<>(Description.values().length);
|
= new HashMap<>(Label.values().length);
|
||||||
|
|
||||||
static {
|
static {
|
||||||
for (Description description : values()) {
|
for (Label label : values()) {
|
||||||
TEXT_TO_DESCRIPTION.put(description.text, description);
|
TEXT_TO_DESCRIPTION.put(label.text, label);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final String text;
|
private final String text;
|
||||||
@DrawableRes private final int icon;
|
@DrawableRes private final int icon;
|
||||||
|
|
||||||
Description(String text, @DrawableRes int icon) {
|
Label(String text, @DrawableRes int icon) {
|
||||||
this.text = text;
|
this.text = text;
|
||||||
this.icon = icon;
|
this.icon = icon;
|
||||||
}
|
}
|
||||||
|
|
@ -121,9 +123,9 @@ public class Place {
|
||||||
return icon;
|
return icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Description fromText(String text) {
|
public static Label fromText(String text) {
|
||||||
Description description = TEXT_TO_DESCRIPTION.get(text);
|
Label label = TEXT_TO_DESCRIPTION.get(text);
|
||||||
return description == null ? UNKNOWN : description;
|
return label == null ? UNKNOWN : label;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,13 +43,13 @@ class PlaceRenderer extends Renderer<Place> {
|
||||||
public void render() {
|
public void render() {
|
||||||
Place place = getContent();
|
Place place = getContent();
|
||||||
tvName.setText(place.name);
|
tvName.setText(place.name);
|
||||||
String descriptionText = place.getDescription().getText();
|
String descriptionText = place.getLongDescription();
|
||||||
if (descriptionText.equals("?")) {
|
if (descriptionText.equals("?")) {
|
||||||
descriptionText = getContext().getString(R.string.no_description_found);
|
descriptionText = getContext().getString(R.string.no_description_found);
|
||||||
}
|
}
|
||||||
tvDesc.setText(descriptionText);
|
tvDesc.setText(descriptionText);
|
||||||
distance.setText(place.distance);
|
distance.setText(place.distance);
|
||||||
icon.setImageResource(place.getDescription().getIcon());
|
icon.setImageResource(place.getLabel().getIcon());
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PlaceClickedListener {
|
interface PlaceClickedListener {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package fr.free.nrw.commons.notification;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
|
public class MarkReadResponse {
|
||||||
|
@SuppressWarnings("unused") @Nullable
|
||||||
|
private String result;
|
||||||
|
|
||||||
|
public String result() {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class QueryMarkReadResponse {
|
||||||
|
@SuppressWarnings("unused") @Nullable private MarkReadResponse echomarkread;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package fr.free.nrw.commons.notification;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by root on 18.12.2017.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class Notification {
|
||||||
|
public NotificationType notificationType;
|
||||||
|
public String notificationText;
|
||||||
|
public String date;
|
||||||
|
public String description;
|
||||||
|
public String link;
|
||||||
|
|
||||||
|
public Notification(NotificationType notificationType, String notificationText, String date, String description, String link) {
|
||||||
|
this.notificationType = notificationType;
|
||||||
|
this.notificationText = notificationText;
|
||||||
|
this.date = date;
|
||||||
|
this.description = description;
|
||||||
|
this.link = link;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
package fr.free.nrw.commons.notification;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.pedrogomez.renderers.RVRendererAdapter;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import butterknife.BindView;
|
||||||
|
import butterknife.ButterKnife;
|
||||||
|
import fr.free.nrw.commons.R;
|
||||||
|
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
||||||
|
import io.reactivex.Observable;
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.schedulers.Schedulers;
|
||||||
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by root on 18.12.2017.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class NotificationActivity extends NavigationBaseActivity {
|
||||||
|
NotificationAdapterFactory notificationAdapterFactory;
|
||||||
|
|
||||||
|
@BindView(R.id.listView) RecyclerView recyclerView;
|
||||||
|
|
||||||
|
@Inject NotificationController controller;
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_notification);
|
||||||
|
ButterKnife.bind(this);
|
||||||
|
initListView();
|
||||||
|
initDrawer();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initListView() {
|
||||||
|
recyclerView = findViewById(R.id.listView);
|
||||||
|
recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||||
|
addNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("CheckResult")
|
||||||
|
private void addNotifications() {
|
||||||
|
Timber.d("Add notifications");
|
||||||
|
|
||||||
|
Observable.fromCallable(() -> controller.getNotifications())
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(notificationList -> {
|
||||||
|
Timber.d("Number of notifications is %d", notificationList.size());
|
||||||
|
setAdapter(notificationList);
|
||||||
|
}, throwable -> Timber.e(throwable, "Error occurred while loading notifications"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleUrl(String url) {
|
||||||
|
if (url == null || url.equals("")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setAdapter(List<Notification> notificationList) {
|
||||||
|
notificationAdapterFactory = new NotificationAdapterFactory(notification -> {
|
||||||
|
Timber.d("Notification clicked %s", notification.link);
|
||||||
|
handleUrl(notification.link);
|
||||||
|
});
|
||||||
|
RVRendererAdapter<Notification> adapter = notificationAdapterFactory.create(notificationList);
|
||||||
|
recyclerView.setAdapter(adapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void startYourself(Context context) {
|
||||||
|
Intent intent = new Intent(context, NotificationActivity.class);
|
||||||
|
context.startActivity(intent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package fr.free.nrw.commons.notification;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.pedrogomez.renderers.ListAdapteeCollection;
|
||||||
|
import com.pedrogomez.renderers.RVRendererAdapter;
|
||||||
|
import com.pedrogomez.renderers.RendererBuilder;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by root on 19.12.2017.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class NotificationAdapterFactory {
|
||||||
|
private NotificationRenderer.NotificationClicked listener;
|
||||||
|
|
||||||
|
NotificationAdapterFactory(@NonNull NotificationRenderer.NotificationClicked listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RVRendererAdapter<Notification> create(List<Notification> notifications) {
|
||||||
|
RendererBuilder<Notification> builder = new RendererBuilder<Notification>()
|
||||||
|
.bind(Notification.class, new NotificationRenderer(listener));
|
||||||
|
ListAdapteeCollection<Notification> collection = new ListAdapteeCollection<>(
|
||||||
|
notifications != null ? notifications : Collections.<Notification>emptyList());
|
||||||
|
return new RVRendererAdapter<>(builder, collection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
package fr.free.nrw.commons.notification;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.auth.SessionManager;
|
||||||
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by root on 19.12.2017.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
public class NotificationController {
|
||||||
|
|
||||||
|
private MediaWikiApi mediaWikiApi;
|
||||||
|
private SessionManager sessionManager;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public NotificationController(MediaWikiApi mediaWikiApi, SessionManager sessionManager) {
|
||||||
|
this.mediaWikiApi = mediaWikiApi;
|
||||||
|
this.sessionManager = sessionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Notification> getNotifications() throws IOException {
|
||||||
|
if (mediaWikiApi.validateLogin()) {
|
||||||
|
return mediaWikiApi.getNotifications();
|
||||||
|
} else {
|
||||||
|
Boolean authTokenValidated = sessionManager.revalidateAuthToken();
|
||||||
|
if (authTokenValidated != null && authTokenValidated) {
|
||||||
|
return mediaWikiApi.getNotifications();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
package fr.free.nrw.commons.notification;
|
||||||
|
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.pedrogomez.renderers.Renderer;
|
||||||
|
|
||||||
|
import butterknife.BindView;
|
||||||
|
import butterknife.ButterKnife;
|
||||||
|
import fr.free.nrw.commons.R;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by root on 19.12.2017.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class NotificationRenderer extends Renderer<Notification> {
|
||||||
|
@BindView(R.id.title) TextView title;
|
||||||
|
@BindView(R.id.description) TextView description;
|
||||||
|
@BindView(R.id.time) TextView time;
|
||||||
|
@BindView(R.id.icon) ImageView icon;
|
||||||
|
private NotificationClicked listener;
|
||||||
|
|
||||||
|
|
||||||
|
NotificationRenderer(NotificationClicked listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setUpView(View view) { }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void hookListeners(View rootView) {
|
||||||
|
rootView.setOnClickListener(v -> listener.notificationClicked(getContent()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected View inflate(LayoutInflater layoutInflater, ViewGroup viewGroup) {
|
||||||
|
View inflatedView = layoutInflater.inflate(R.layout.item_notification, viewGroup, false);
|
||||||
|
ButterKnife.bind(this, inflatedView);
|
||||||
|
return inflatedView;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void render() {
|
||||||
|
Notification notification = getContent();
|
||||||
|
title.setText(notification.notificationText);
|
||||||
|
time.setText(notification.date);
|
||||||
|
description.setText(notification.description);
|
||||||
|
switch (notification.notificationType) {
|
||||||
|
case THANK_YOU_EDIT:
|
||||||
|
icon.setImageResource(R.drawable.ic_edit_black_24dp);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
icon.setImageResource(R.drawable.round_icon_unknown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface NotificationClicked{
|
||||||
|
void notificationClicked(Notification notification);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package fr.free.nrw.commons.notification;
|
||||||
|
|
||||||
|
public enum NotificationType {
|
||||||
|
THANK_YOU_EDIT("thank-you-edit"),
|
||||||
|
EDIT_USER_TALK("edit-user-talk"),
|
||||||
|
MENTION("mention"),
|
||||||
|
WELCOME("welcome"),
|
||||||
|
UNKNOWN("unknown");
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
NotificationType(String type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NotificationType handledValueOf(String name) {
|
||||||
|
for (NotificationType e : values()) {
|
||||||
|
if (e.getType().equals(name)) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return UNKNOWN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
package fr.free.nrw.commons.notification;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import org.w3c.dom.Element;
|
||||||
|
import org.w3c.dom.Node;
|
||||||
|
import org.w3c.dom.NodeList;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.BuildConfig;
|
||||||
|
import fr.free.nrw.commons.R;
|
||||||
|
|
||||||
|
public class NotificationUtils {
|
||||||
|
|
||||||
|
private static final String COMMONS_WIKI = "commonswiki";
|
||||||
|
|
||||||
|
public static boolean isCommonsNotification(Node document) {
|
||||||
|
if (document == null || !document.hasAttributes()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Element element = (Element) document;
|
||||||
|
return COMMONS_WIKI.equals(element.getAttribute("wiki"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NotificationType getNotificationType(Node document) {
|
||||||
|
Element element = (Element) document;
|
||||||
|
String type = element.getAttribute("type");
|
||||||
|
return NotificationType.handledValueOf(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Notification getNotificationFromApiResult(Context context, Node document) {
|
||||||
|
NotificationType type = getNotificationType(document);
|
||||||
|
|
||||||
|
String notificationText = "";
|
||||||
|
String link = getNotificationLink(document);
|
||||||
|
String description = getNotificationDescription(document);
|
||||||
|
switch (type) {
|
||||||
|
case THANK_YOU_EDIT:
|
||||||
|
notificationText = context.getString(R.string.notifications_thank_you_edit);
|
||||||
|
break;
|
||||||
|
case EDIT_USER_TALK:
|
||||||
|
notificationText = getUserTalkMessage(context, document);
|
||||||
|
break;
|
||||||
|
case MENTION:
|
||||||
|
notificationText = getMentionMessage(context, document);
|
||||||
|
break;
|
||||||
|
case WELCOME:
|
||||||
|
notificationText = getWelcomeMessage(context, document);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return new Notification(type, notificationText, getTimestamp(document), description, link);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getMentionMessage(Context context, Node document) {
|
||||||
|
String format = context.getString(R.string.notifications_mention);
|
||||||
|
return String.format(format, getAgent(document), getNotificationDescription(document));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getUserTalkMessage(Context context, Node document) {
|
||||||
|
String format = context.getString(R.string.notifications_talk_page_message);
|
||||||
|
return String.format(format, getAgent(document));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getWelcomeMessage(Context context, Node document) {
|
||||||
|
String welcomeMessageFormat = context.getString(R.string.notifications_welcome);
|
||||||
|
return String.format(welcomeMessageFormat, getAgent(document));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getAgent(Node document) {
|
||||||
|
Element agentElement = (Element) getNode(document, "agent");
|
||||||
|
if (agentElement != null) {
|
||||||
|
return agentElement.getAttribute("name");
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getTimestamp(Node document) {
|
||||||
|
Element timestampElement = (Element) getNode(document, "timestamp");
|
||||||
|
if (timestampElement != null) {
|
||||||
|
return timestampElement.getAttribute("date");
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getNotificationLink(Node document) {
|
||||||
|
String format = "%s%s";
|
||||||
|
Element titleElement = (Element) getNode(document, "title");
|
||||||
|
if (titleElement != null) {
|
||||||
|
String fullName = titleElement.getAttribute("full");
|
||||||
|
return String.format(format, BuildConfig.HOME_URL, fullName);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getNotificationDescription(Node document) {
|
||||||
|
Element titleElement = (Element) getNode(document, "title");
|
||||||
|
if (titleElement != null) {
|
||||||
|
return titleElement.getAttribute("text");
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static Node getNode(Node node, String nodeName) {
|
||||||
|
NodeList childNodes = node.getChildNodes();
|
||||||
|
for (int i = 0; i < childNodes.getLength(); i++) {
|
||||||
|
Node nodeItem = childNodes.item(i);
|
||||||
|
Element item = (Element) nodeItem;
|
||||||
|
if (item.getTagName().equals(nodeName)) {
|
||||||
|
return nodeItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
package fr.free.nrw.commons.settings;
|
package fr.free.nrw.commons.settings;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.support.v7.app.AppCompatDelegate;
|
import android.support.v7.app.AppCompatDelegate;
|
||||||
|
|
@ -11,9 +9,16 @@ import butterknife.ButterKnife;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* allows the user to change the settings
|
||||||
|
*/
|
||||||
public class SettingsActivity extends NavigationBaseActivity {
|
public class SettingsActivity extends NavigationBaseActivity {
|
||||||
private AppCompatDelegate settingsDelegate;
|
private AppCompatDelegate settingsDelegate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* to be called when the activity starts
|
||||||
|
* @param savedInstanceState the previously saved state
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
// Check prefs on every activity starts
|
// Check prefs on every activity starts
|
||||||
|
|
@ -31,6 +36,10 @@ public class SettingsActivity extends NavigationBaseActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get an action bar
|
// Get an action bar
|
||||||
|
/**
|
||||||
|
* takes care of actions taken after the creation has happened
|
||||||
|
* @param savedInstanceState the saved state
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected void onPostCreate(Bundle savedInstanceState) {
|
protected void onPostCreate(Bundle savedInstanceState) {
|
||||||
super.onPostCreate(savedInstanceState);
|
super.onPostCreate(savedInstanceState);
|
||||||
|
|
@ -43,7 +52,11 @@ public class SettingsActivity extends NavigationBaseActivity {
|
||||||
//settingsDelegate.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
//settingsDelegate.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Handle action-bar clicks
|
/**
|
||||||
|
* Handle action-bar clicks
|
||||||
|
* @param item the selected item
|
||||||
|
* @return true on success, false on failure
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
|
|
@ -54,9 +67,4 @@ public class SettingsActivity extends NavigationBaseActivity {
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void startYourself(Context context) {
|
|
||||||
Intent settingsIntent = new Intent(context, SettingsActivity.class);
|
|
||||||
context.startActivity(settingsIntent);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,22 +1,50 @@
|
||||||
package fr.free.nrw.commons.settings;
|
package fr.free.nrw.commons.settings;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
|
import android.content.ActivityNotFoundException;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.CheckBoxPreference;
|
import android.preference.CheckBoxPreference;
|
||||||
import android.preference.EditTextPreference;
|
import android.preference.EditTextPreference;
|
||||||
import android.preference.ListPreference;
|
import android.preference.ListPreference;
|
||||||
|
import android.preference.Preference;
|
||||||
import android.preference.PreferenceFragment;
|
import android.preference.PreferenceFragment;
|
||||||
import android.preference.PreferenceManager;
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.v4.content.ContextCompat;
|
||||||
|
import android.support.v4.content.FileProvider;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.BuildConfig;
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
import fr.free.nrw.commons.CommonsApplication;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.Utils;
|
import fr.free.nrw.commons.Utils;
|
||||||
|
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||||
|
import fr.free.nrw.commons.utils.FileUtils;
|
||||||
|
|
||||||
public class SettingsFragment extends PreferenceFragment {
|
public class SettingsFragment extends PreferenceFragment {
|
||||||
|
|
||||||
|
private static final int REQUEST_CODE_WRITE_EXTERNAL_STORAGE = 100;
|
||||||
|
|
||||||
|
@Inject @Named("default_preferences") SharedPreferences prefs;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
ApplicationlessInjection
|
||||||
|
.getInstance(getActivity().getApplicationContext())
|
||||||
|
.getCommonsApplicationComponent()
|
||||||
|
.inject(this);
|
||||||
|
|
||||||
// Load the preferences from an XML resource
|
// Load the preferences from an XML resource
|
||||||
addPreferencesFromResource(R.xml.preferences);
|
addPreferencesFromResource(R.xml.preferences);
|
||||||
|
|
@ -38,14 +66,17 @@ public class SettingsFragment extends PreferenceFragment {
|
||||||
});
|
});
|
||||||
|
|
||||||
final EditTextPreference uploadLimit = (EditTextPreference) findPreference("uploads");
|
final EditTextPreference uploadLimit = (EditTextPreference) findPreference("uploads");
|
||||||
final SharedPreferences sharedPref = PreferenceManager
|
int uploads = prefs.getInt(Prefs.UPLOADS_SHOWING, 100);
|
||||||
.getDefaultSharedPreferences(CommonsApplication.getInstance());
|
|
||||||
int uploads = sharedPref.getInt(Prefs.UPLOADS_SHOWING, 100);
|
|
||||||
uploadLimit.setText(uploads + "");
|
uploadLimit.setText(uploads + "");
|
||||||
uploadLimit.setSummary(uploads + "");
|
uploadLimit.setSummary(uploads + "");
|
||||||
uploadLimit.setOnPreferenceChangeListener((preference, newValue) -> {
|
uploadLimit.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
int value = Integer.parseInt(newValue.toString());
|
int value;
|
||||||
final SharedPreferences.Editor editor = sharedPref.edit();
|
try {
|
||||||
|
value = Integer.parseInt(newValue.toString());
|
||||||
|
} catch(Exception e) {
|
||||||
|
value = 100; //Default number
|
||||||
|
}
|
||||||
|
final SharedPreferences.Editor editor = prefs.edit();
|
||||||
if (value > 500) {
|
if (value > 500) {
|
||||||
new AlertDialog.Builder(getActivity())
|
new AlertDialog.Builder(getActivity())
|
||||||
.setTitle(R.string.maximum_limit)
|
.setTitle(R.string.maximum_limit)
|
||||||
|
|
@ -58,14 +89,71 @@ public class SettingsFragment extends PreferenceFragment {
|
||||||
uploadLimit.setSummary(500 + "");
|
uploadLimit.setSummary(500 + "");
|
||||||
uploadLimit.setText(500 + "");
|
uploadLimit.setText(500 + "");
|
||||||
} else {
|
} else {
|
||||||
editor.putInt(Prefs.UPLOADS_SHOWING, Integer.parseInt(newValue.toString()));
|
editor.putInt(Prefs.UPLOADS_SHOWING, value);
|
||||||
editor.putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED,true);
|
editor.putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED,true);
|
||||||
uploadLimit.setSummary(newValue.toString());
|
uploadLimit.setSummary(String.valueOf(value));
|
||||||
}
|
}
|
||||||
editor.apply();
|
editor.apply();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Preference sendLogsPreference = findPreference("sendLogFile");
|
||||||
|
sendLogsPreference.setOnPreferenceClickListener(preference -> {
|
||||||
|
//first we need to check if we have the necessary permissions
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
if (ContextCompat.checkSelfPermission(
|
||||||
|
getActivity(),
|
||||||
|
Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
|
==
|
||||||
|
PackageManager.PERMISSION_GRANTED) {
|
||||||
|
sendAppLogsViaEmail();
|
||||||
|
} else {
|
||||||
|
//first get the necessary permission
|
||||||
|
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
|
||||||
|
REQUEST_CODE_WRITE_EXTERNAL_STORAGE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sendAppLogsViaEmail();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||||
|
if (requestCode == REQUEST_CODE_WRITE_EXTERNAL_STORAGE) {
|
||||||
|
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
sendAppLogsViaEmail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendAppLogsViaEmail() {
|
||||||
|
String appLogs = Utils.getAppLogs();
|
||||||
|
File appLogsFile = FileUtils.createAndGetAppLogsFile(appLogs);
|
||||||
|
|
||||||
|
Context applicationContext = getActivity().getApplicationContext();
|
||||||
|
Uri appLogsFilePath = FileProvider.getUriForFile(
|
||||||
|
getActivity(),
|
||||||
|
applicationContext.getPackageName() + ".provider",
|
||||||
|
appLogsFile
|
||||||
|
);
|
||||||
|
|
||||||
|
Intent feedbackIntent = new Intent(Intent.ACTION_SEND);
|
||||||
|
feedbackIntent.setType("message/rfc822");
|
||||||
|
feedbackIntent.putExtra(Intent.EXTRA_EMAIL,
|
||||||
|
new String[]{CommonsApplication.LOGS_PRIVATE_EMAIL});
|
||||||
|
feedbackIntent.putExtra(Intent.EXTRA_SUBJECT,
|
||||||
|
String.format(CommonsApplication.FEEDBACK_EMAIL_SUBJECT,
|
||||||
|
BuildConfig.VERSION_NAME));
|
||||||
|
feedbackIntent.putExtra(Intent.EXTRA_STREAM,appLogsFilePath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
startActivity(feedbackIntent);
|
||||||
|
} catch (ActivityNotFoundException e) {
|
||||||
|
Toast.makeText(getActivity(), R.string.no_email_client, Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,17 @@ package fr.free.nrw.commons.theme;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.support.v7.app.AppCompatActivity;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.Utils;
|
import fr.free.nrw.commons.di.CommonsDaggerAppCompatActivity;
|
||||||
|
|
||||||
public class BaseActivity extends AppCompatActivity {
|
public abstract class BaseActivity extends CommonsDaggerAppCompatActivity {
|
||||||
boolean currentTheme;
|
boolean currentTheme;
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
if(Utils.isDarkTheme(this)){
|
boolean currentThemeIsDark = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme", false);
|
||||||
|
if (currentThemeIsDark){
|
||||||
currentTheme = true;
|
currentTheme = true;
|
||||||
setTheme(R.style.DarkAppTheme);
|
setTheme(R.style.DarkAppTheme);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -27,8 +26,8 @@ public class BaseActivity extends AppCompatActivity {
|
||||||
@Override
|
@Override
|
||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
// Restart activity if theme is changed
|
// Restart activity if theme is changed
|
||||||
boolean newTheme = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme",false);
|
boolean newTheme = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme", false);
|
||||||
if(currentTheme!=newTheme){ //is activity theme changed
|
if (currentTheme != newTheme) { //is activity theme changed
|
||||||
Intent intent = getIntent();
|
Intent intent = getIntent();
|
||||||
finish();
|
finish();
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
package fr.free.nrw.commons.theme;
|
package fr.free.nrw.commons.theme;
|
||||||
|
|
||||||
|
import android.accounts.Account;
|
||||||
|
import android.accounts.AccountManager;
|
||||||
import android.content.ActivityNotFoundException;
|
import android.content.ActivityNotFoundException;
|
||||||
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.design.widget.NavigationView;
|
import android.support.design.widget.NavigationView;
|
||||||
|
|
@ -9,7 +12,9 @@ import android.support.v7.app.ActionBarDrawerToggle;
|
||||||
import android.support.v7.app.AlertDialog;
|
import android.support.v7.app.AlertDialog;
|
||||||
import android.support.v7.widget.Toolbar;
|
import android.support.v7.widget.Toolbar;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import butterknife.BindView;
|
import butterknife.BindView;
|
||||||
|
|
@ -18,9 +23,11 @@ import fr.free.nrw.commons.BuildConfig;
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
import fr.free.nrw.commons.CommonsApplication;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.WelcomeActivity;
|
import fr.free.nrw.commons.WelcomeActivity;
|
||||||
|
import fr.free.nrw.commons.auth.AccountUtil;
|
||||||
import fr.free.nrw.commons.auth.LoginActivity;
|
import fr.free.nrw.commons.auth.LoginActivity;
|
||||||
import fr.free.nrw.commons.contributions.ContributionsActivity;
|
import fr.free.nrw.commons.contributions.ContributionsActivity;
|
||||||
import fr.free.nrw.commons.nearby.NearbyActivity;
|
import fr.free.nrw.commons.nearby.NearbyActivity;
|
||||||
|
import fr.free.nrw.commons.notification.NotificationActivity;
|
||||||
import fr.free.nrw.commons.settings.SettingsActivity;
|
import fr.free.nrw.commons.settings.SettingsActivity;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
|
@ -47,6 +54,22 @@ public abstract class NavigationBaseActivity extends BaseActivity
|
||||||
toggle.setDrawerIndicatorEnabled(true);
|
toggle.setDrawerIndicatorEnabled(true);
|
||||||
toggle.syncState();
|
toggle.syncState();
|
||||||
setDrawerPaneWidth();
|
setDrawerPaneWidth();
|
||||||
|
setUserName();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the username in navigationHeader.
|
||||||
|
*/
|
||||||
|
private void setUserName() {
|
||||||
|
|
||||||
|
View navHeaderView = navigationView.getHeaderView(0);
|
||||||
|
TextView username = navHeaderView.findViewById(R.id.username);
|
||||||
|
|
||||||
|
AccountManager accountManager = AccountManager.get(this);
|
||||||
|
Account[] allAccounts = accountManager.getAccountsByType(AccountUtil.ACCOUNT_TYPE);
|
||||||
|
if (allAccounts.length != 0) {
|
||||||
|
username.setText(allAccounts[0].name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initBackButton() {
|
public void initBackButton() {
|
||||||
|
|
@ -70,30 +93,25 @@ public abstract class NavigationBaseActivity extends BaseActivity
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onNavigationItemSelected(@NonNull final MenuItem item) {
|
public boolean onNavigationItemSelected(@NonNull final MenuItem item) {
|
||||||
switch (item.getItemId()) {
|
final int itemId = item.getItemId();
|
||||||
|
switch (itemId) {
|
||||||
case R.id.action_home:
|
case R.id.action_home:
|
||||||
drawerLayout.closeDrawer(navigationView);
|
drawerLayout.closeDrawer(navigationView);
|
||||||
if (!(this instanceof ContributionsActivity)) {
|
startActivityWithFlags(
|
||||||
ContributionsActivity.startYourself(this);
|
this, ContributionsActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP,
|
||||||
}
|
Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||||
return true;
|
return true;
|
||||||
case R.id.action_nearby:
|
case R.id.action_nearby:
|
||||||
drawerLayout.closeDrawer(navigationView);
|
drawerLayout.closeDrawer(navigationView);
|
||||||
if (!(this instanceof NearbyActivity)) {
|
startActivityWithFlags(this, NearbyActivity.class, Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
|
||||||
NearbyActivity.startYourself(this);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
case R.id.action_about:
|
case R.id.action_about:
|
||||||
drawerLayout.closeDrawer(navigationView);
|
drawerLayout.closeDrawer(navigationView);
|
||||||
if (!(this instanceof AboutActivity)) {
|
startActivityWithFlags(this, AboutActivity.class, Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
|
||||||
AboutActivity.startYourself(this);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
case R.id.action_settings:
|
case R.id.action_settings:
|
||||||
drawerLayout.closeDrawer(navigationView);
|
drawerLayout.closeDrawer(navigationView);
|
||||||
if (!(this instanceof SettingsActivity)) {
|
startActivityWithFlags(this, SettingsActivity.class, Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
|
||||||
SettingsActivity.startYourself(this);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
case R.id.action_introduction:
|
case R.id.action_introduction:
|
||||||
drawerLayout.closeDrawer(navigationView);
|
drawerLayout.closeDrawer(navigationView);
|
||||||
|
|
@ -126,7 +144,12 @@ public abstract class NavigationBaseActivity extends BaseActivity
|
||||||
.setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel())
|
.setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel())
|
||||||
.show();
|
.show();
|
||||||
return true;
|
return true;
|
||||||
|
case R.id.action_notifications:
|
||||||
|
drawerLayout.closeDrawer(navigationView);
|
||||||
|
NotificationActivity.startYourself(this);
|
||||||
|
return true;
|
||||||
default:
|
default:
|
||||||
|
Timber.e("Unknown option [%s] selected from the navigation menu", itemId);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -143,4 +166,12 @@ public abstract class NavigationBaseActivity extends BaseActivity
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static <T> void startActivityWithFlags(Context context, Class<T> cls, int... flags) {
|
||||||
|
Intent intent = new Intent(context, cls);
|
||||||
|
for (int flag: flags) {
|
||||||
|
intent.addFlags(flag);
|
||||||
|
}
|
||||||
|
context.startActivity(intent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package fr.free.nrw.commons.ui.widget;
|
package fr.free.nrw.commons.ui.widget;
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* Created by mikel on 07/08/2017.
|
*Created by mikel on 07/08/2017.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
@ -16,22 +16,49 @@ import android.util.AttributeSet;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.utils.UiUtils;
|
import fr.free.nrw.commons.utils.UiUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a text view compatible with older versions of the platform
|
||||||
|
*/
|
||||||
public class CompatTextView extends AppCompatTextView {
|
public class CompatTextView extends AppCompatTextView {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of CompatTextView
|
||||||
|
*
|
||||||
|
* @param context the view context
|
||||||
|
*/
|
||||||
public CompatTextView(Context context) {
|
public CompatTextView(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
init(null);
|
init(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of CompatTextView
|
||||||
|
*
|
||||||
|
* @param context the view context
|
||||||
|
* @param attrs the set of attributes for the view
|
||||||
|
*/
|
||||||
public CompatTextView(Context context, AttributeSet attrs) {
|
public CompatTextView(Context context, AttributeSet attrs) {
|
||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
init(attrs);
|
init(attrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of CompatTextView
|
||||||
|
*
|
||||||
|
* @param context
|
||||||
|
* @param attrs
|
||||||
|
* @param defStyleAttr
|
||||||
|
*/
|
||||||
public CompatTextView(Context context, AttributeSet attrs, int defStyleAttr) {
|
public CompatTextView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
super(context, attrs, defStyleAttr);
|
super(context, attrs, defStyleAttr);
|
||||||
init(attrs);
|
init(attrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* initializes the view
|
||||||
|
*
|
||||||
|
* @param attrs the attribute set of the view, which can be null
|
||||||
|
*/
|
||||||
private void init(@Nullable AttributeSet attrs) {
|
private void init(@Nullable AttributeSet attrs) {
|
||||||
if (attrs != null) {
|
if (attrs != null) {
|
||||||
Context context = getContext();
|
Context context = getContext();
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,51 @@
|
||||||
package fr.free.nrw.commons.ui.widget;
|
package fr.free.nrw.commons.ui.widget;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.os.Build;
|
||||||
import android.support.v7.widget.AppCompatTextView;
|
import android.support.v7.widget.AppCompatTextView;
|
||||||
|
import android.text.Html;
|
||||||
|
import android.text.Spanned;
|
||||||
import android.text.method.LinkMovementMethod;
|
import android.text.method.LinkMovementMethod;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
import fr.free.nrw.commons.Utils;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An {@link AppCompatTextView} which formats the text to HTML displayable text and makes any
|
* An {@link AppCompatTextView} which formats the text to HTML displayable text and makes any
|
||||||
* links clickable.
|
* links clickable.
|
||||||
*/
|
*/
|
||||||
public class HtmlTextView extends AppCompatTextView {
|
public class HtmlTextView extends AppCompatTextView {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of HtmlTextView
|
||||||
|
* @param context the context of the view
|
||||||
|
* @param attrs the set of attributes for the view
|
||||||
|
*/
|
||||||
public HtmlTextView(Context context, AttributeSet attrs) {
|
public HtmlTextView(Context context, AttributeSet attrs) {
|
||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
|
|
||||||
setMovementMethod(LinkMovementMethod.getInstance());
|
setMovementMethod(LinkMovementMethod.getInstance());
|
||||||
setText(Utils.fromHtml(getText().toString()));
|
setText(fromHtml(getText().toString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the text to be displayed
|
||||||
|
* @param newText the text to be displayed
|
||||||
|
*/
|
||||||
public void setHtmlText(String newText) {
|
public void setHtmlText(String newText) {
|
||||||
setText(Utils.fromHtml(newText));
|
setText(fromHtml(newText));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix Html.fromHtml is deprecated problem
|
||||||
|
*
|
||||||
|
* @param source provided Html string
|
||||||
|
* @return returned Spanned of appropriate method according to version check
|
||||||
|
*/
|
||||||
|
private static Spanned fromHtml(String source) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
return Html.fromHtml(source, Html.FROM_HTML_MODE_LEGACY);
|
||||||
|
} else {
|
||||||
|
//noinspection deprecation
|
||||||
|
return Html.fromHtml(source);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue