mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 12:23:58 +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:
|
||||
components:
|
||||
- platform-tools
|
||||
- tools
|
||||
- build-tools-26.0.1
|
||||
- platform-tools
|
||||
- build-tools-26.0.2
|
||||
- extra-google-m2repository
|
||||
- extra-android-m2repository
|
||||
- ${ANDROID_TARGET}
|
||||
- android-25
|
||||
- android-26
|
||||
- sys-img-${ANDROID_ABI}-${ANDROID_TARGET}
|
||||
licenses:
|
||||
- 'android-sdk-license-.+'
|
||||
|
||||
before_script:
|
||||
- echo no | android create avd --force -n test -t $ANDROID_TARGET --abi $ANDROID_ABI
|
||||
|
|
@ -32,14 +35,16 @@ before_script:
|
|||
- android-wait-for-emulator
|
||||
|
||||
script:
|
||||
- ./gradlew clean check connectedCheck jacocoTestReport --stacktrace
|
||||
- ./gradlew clean check connectedCheck jacocoTestReport
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
after_failure:
|
||||
- echo '*** Connected Test Rsults ***'
|
||||
- w3m -dump ${TRAVIS_BUILD_DIR}/app/build/reports/androidTests/connected/*Test.html
|
||||
- echo '*** Debug Unit Test Results ***'
|
||||
- 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:
|
||||
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
||||
|
|
|
|||
44
CHANGELOG.md
44
CHANGELOG.md
|
|
@ -1,6 +1,48 @@
|
|||
# Wikimedia Commons for Android
|
||||
|
||||
## v2.5.0 beta
|
||||
## 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
|
||||
- Added one-time popup for beta users to provide feedback on IEG renewal proposal
|
||||
- Added link to Commons policies in ShareActivity
|
||||
- Various string fixes
|
||||
|
|
|
|||
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
|
||||
* Bruke Mekuria Mulugeta
|
||||
* Paul Hawke
|
||||
* Vishan Seru
|
||||
|
||||
3rd party open source libraries used:
|
||||
* Butterknife
|
||||
|
|
@ -38,3 +39,953 @@ their contribution to the product.
|
|||
|
||||
3rd party open source apps from which significant code has been reused:
|
||||
* 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].
|
||||
|
||||
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 :-)
|
||||
|
||||
We are currently applying for an [IEG renewal][10] to work on the app for the next 6 months. Feedback is very much welcomed.
|
||||
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 :-)
|
||||
|
||||
<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>
|
||||
|
|
|
|||
117
app/build.gradle
117
app/build.gradle
|
|
@ -1,58 +1,78 @@
|
|||
apply from: '../gitutils.gradle'
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'me.tatarka.retrolambda'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'jacoco-android'
|
||||
apply from: 'quality.gradle'
|
||||
apply plugin: 'com.getkeepsafe.dexcount'
|
||||
|
||||
dependencies {
|
||||
compile 'com.github.nicolas-raoul:Quadtree:ac16ea8035bf07'
|
||||
compile 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar'
|
||||
compile 'in.yuvi:http.fluent:1.3'
|
||||
compile 'com.android.volley:volley:1.0.0'
|
||||
compile 'ch.acra:acra:4.7.0'
|
||||
compile 'org.mediawiki:api:1.3'
|
||||
compile 'commons-codec:commons-codec:1.10'
|
||||
compile 'com.github.pedrovgs:renderers:3.3.3'
|
||||
compile 'com.google.code.gson:gson:2.8.0'
|
||||
compile 'com.jakewharton.timber:timber:4.5.1'
|
||||
compile 'info.debatty:java-string-similarity:0.24'
|
||||
compile ('com.mapbox.mapboxsdk:mapbox-android-sdk:5.1.0@aar'){
|
||||
implementation 'com.github.nicolas-raoul:Quadtree:ac16ea8035bf07'
|
||||
implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar'
|
||||
implementation 'in.yuvi:http.fluent:1.3'
|
||||
implementation 'com.android.volley:volley:1.0.0'
|
||||
implementation 'ch.acra:acra:4.7.0'
|
||||
implementation 'org.mediawiki:api:1.3'
|
||||
implementation 'commons-codec:commons-codec:1.10'
|
||||
implementation 'com.github.pedrovgs:renderers:3.3.3'
|
||||
implementation 'com.google.code.gson:gson:2.8.1'
|
||||
implementation 'com.jakewharton.timber:timber:4.5.1'
|
||||
implementation 'info.debatty:java-string-similarity:0.24'
|
||||
implementation ('com.mapbox.mapboxsdk:mapbox-android-sdk:5.2.1@aar'){
|
||||
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"
|
||||
annotationProcessor "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
|
||||
implementation "com.android.support:support-v4:$SUPPORT_LIB_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'
|
||||
compile 'com.squareup.okio:okio:1.13.0'
|
||||
implementation "com.android.support:cardview-v7:$SUPPORT_LIB_VERSION"
|
||||
|
||||
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
|
||||
// explicitly depend on RxJava's latest version for bug fixes and new features.
|
||||
compile 'io.reactivex.rxjava2:rxjava:2.1.2'
|
||||
compile 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
|
||||
compile 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.0.0'
|
||||
compile 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.0.0'
|
||||
compile 'com.jakewharton.rxbinding2:rxbinding-design:2.0.0'
|
||||
implementation 'io.reactivex.rxjava2:rxjava:2.1.2'
|
||||
implementation 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
|
||||
implementation 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.0.0'
|
||||
implementation 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.0.0'
|
||||
implementation 'com.jakewharton.rxbinding2:rxbinding-design:2.0.0'
|
||||
|
||||
compile 'com.facebook.fresco:fresco:1.3.0'
|
||||
compile 'com.facebook.stetho:stetho:1.5.0'
|
||||
implementation 'com.facebook.fresco:fresco:1.5.0'
|
||||
implementation 'com.facebook.stetho:stetho:1.5.0'
|
||||
|
||||
testCompile 'junit:junit:4.12'
|
||||
testCompile 'org.robolectric:robolectric:3.3.2'
|
||||
implementation "com.google.dagger:dagger:$DAGGER_VERSION"
|
||||
implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
|
||||
|
||||
testCompile 'com.squareup.okhttp3:mockwebserver:3.8.1'
|
||||
androidTestCompile 'com.squareup.okhttp3:mockwebserver:3.8.1'
|
||||
androidTestCompile "com.android.support:support-annotations:${project.supportLibVersion}"
|
||||
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'
|
||||
kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
|
||||
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
|
||||
|
||||
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1'
|
||||
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
|
||||
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
|
||||
testImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
|
||||
androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
|
||||
|
||||
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 {
|
||||
|
|
@ -63,24 +83,38 @@ android {
|
|||
|
||||
defaultConfig {
|
||||
applicationId 'fr.free.nrw.commons'
|
||||
versionCode 74
|
||||
versionName '2.5.0'
|
||||
versionCode 82
|
||||
versionName '2.6.7'
|
||||
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
|
||||
|
||||
minSdkVersion project.minSdkVersion
|
||||
targetSdkVersion project.targetSdkVersion
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
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 {
|
||||
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.
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
testCoverageEnabled true
|
||||
versionNameSuffix "-debug-" + getBranchName() + "~" + getBuildVersion()
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions 'tier'
|
||||
productFlavors {
|
||||
prod {
|
||||
buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.org/w/api.php\""
|
||||
|
|
@ -92,6 +126,7 @@ android {
|
|||
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_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Main_Page&welcome=yes\""
|
||||
dimension 'tier'
|
||||
}
|
||||
|
||||
beta {
|
||||
|
|
@ -105,6 +140,7 @@ android {
|
|||
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_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
|
||||
configurations.all {
|
||||
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">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<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_STATS"/>
|
||||
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/>
|
||||
<uses-permission android:name="android.permission.WRITE_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_STATS" />
|
||||
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
|
||||
<uses-permission android:name="android.permission.GET_ACCOUNTS"/>
|
||||
<uses-permission android:name="android.permission.USE_CREDENTIALS"/>
|
||||
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS"/>
|
||||
<uses-permission android:name="android.permission.MANAGE_DOCUMENTS"/>
|
||||
<uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS"/>
|
||||
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
|
||||
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.MANAGE_DOCUMENTS" />
|
||||
<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
|
||||
android:name=".CommonsApplication"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.AppCompat"
|
||||
android:theme="@style/LightAppTheme"
|
||||
android:supportsRtl="true" >
|
||||
<activity android:name="org.acra.CrashReportDialog"
|
||||
android:theme="@android:style/Theme.Dialog"
|
||||
|
|
@ -27,23 +31,19 @@
|
|||
android:excludeFromRecents="true"
|
||||
android:finishOnTaskLaunch="true" />
|
||||
|
||||
<activity
|
||||
android:name=".auth.LoginActivity"
|
||||
>
|
||||
<activity android:name=".auth.LoginActivity">
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".WelcomeActivity"
|
||||
>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".WelcomeActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".upload.ShareActivity"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
>
|
||||
android:label="@string/app_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
|
@ -51,11 +51,11 @@
|
|||
<data android:mimeType="audio/ogg" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".upload.MultipleShareActivity"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
>
|
||||
android:name=".upload.MultipleShareActivity"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
|
@ -65,33 +65,38 @@
|
|||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".contributions.ContributionsActivity"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
>
|
||||
</activity>
|
||||
android:name=".contributions.ContributionsActivity"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name" />
|
||||
|
||||
<activity
|
||||
android:name=".settings.SettingsActivity"
|
||||
android:label="@string/title_activity_settings"
|
||||
/>
|
||||
android:label="@string/title_activity_settings" />
|
||||
|
||||
<activity
|
||||
android:name=".AboutActivity"
|
||||
android:label="@string/title_activity_about"
|
||||
android:parentActivityName=".contributions.ContributionsActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".auth.SignupActivity"
|
||||
android:label="@string/title_activity_signup"/>
|
||||
android:label="@string/title_activity_signup" />
|
||||
|
||||
<activity
|
||||
android:name=".nearby.NearbyActivity"
|
||||
android:label="@string/title_activity_nearby"
|
||||
android:parentActivityName=".contributions.ContributionsActivity" />
|
||||
|
||||
<service android:name=".upload.UploadService" >
|
||||
</service>
|
||||
<activity
|
||||
android:name=".notification.NotificationActivity"
|
||||
android:label="@string/navigation_item_notification" />
|
||||
|
||||
<service android:name=".upload.UploadService" />
|
||||
|
||||
<service
|
||||
android:name=".auth.WikiAccountAuthenticatorService"
|
||||
android:exported="true"
|
||||
android:process=":auth" >
|
||||
android:process=":auth">
|
||||
<intent-filter>
|
||||
<action android:name="android.accounts.AccountAuthenticator" />
|
||||
</intent-filter>
|
||||
|
|
@ -102,27 +107,25 @@
|
|||
</service>
|
||||
|
||||
<service
|
||||
android:name=".contributions.ContributionsSyncService"
|
||||
android:exported="true">
|
||||
android:name=".contributions.ContributionsSyncService"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action
|
||||
android:name="android.content.SyncAdapter" />
|
||||
<action android:name="android.content.SyncAdapter" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/contributions_sync_adapter" />
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/contributions_sync_adapter" />
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".modifications.ModificationsSyncService"
|
||||
android:exported="true">
|
||||
android:name=".modifications.ModificationsSyncService"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action
|
||||
android:name="android.content.SyncAdapter" />
|
||||
<action android:name="android.content.SyncAdapter" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/modifications_sync_adapter" />
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/modifications_sync_adapter" />
|
||||
</service>
|
||||
|
||||
<provider
|
||||
|
|
@ -132,31 +135,29 @@
|
|||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/provider_paths"/>
|
||||
android:resource="@xml/provider_paths" />
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
android:name=".contributions.ContributionsContentProvider"
|
||||
android:label="@string/provider_contributions"
|
||||
android:syncable="true"
|
||||
android:authorities="fr.free.nrw.commons.contributions.contentprovider"
|
||||
android:exported="false">
|
||||
</provider>
|
||||
android:name=".contributions.ContributionsContentProvider"
|
||||
android:authorities="fr.free.nrw.commons.contributions.contentprovider"
|
||||
android:exported="false"
|
||||
android:label="@string/provider_contributions"
|
||||
android:syncable="true" />
|
||||
|
||||
<provider
|
||||
android:name=".modifications.ModificationsContentProvider"
|
||||
android:label="@string/provider_modifications"
|
||||
android:syncable="true"
|
||||
android:authorities="fr.free.nrw.commons.modifications.contentprovider"
|
||||
android:exported="false">
|
||||
</provider>
|
||||
android:name=".modifications.ModificationsContentProvider"
|
||||
android:authorities="fr.free.nrw.commons.modifications.contentprovider"
|
||||
android:exported="false"
|
||||
android:label="@string/provider_modifications"
|
||||
android:syncable="true" />
|
||||
|
||||
<provider
|
||||
android:name=".category.CategoryContentProvider"
|
||||
android:label="@string/provider_categories"
|
||||
android:syncable="false"
|
||||
android:authorities="fr.free.nrw.commons.categories.contentprovider"
|
||||
android:exported="false">
|
||||
</provider>
|
||||
android:name=".category.CategoryContentProvider"
|
||||
android:authorities="fr.free.nrw.commons.categories.contentprovider"
|
||||
android:exported="false"
|
||||
android:label="@string/provider_categories"
|
||||
android:syncable="false" />
|
||||
|
||||
</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;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import butterknife.OnClick;
|
||||
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
||||
import fr.free.nrw.commons.ui.widget.HtmlTextView;
|
||||
|
||||
/**
|
||||
* Represents about screen of this app
|
||||
*/
|
||||
public class AboutActivity extends NavigationBaseActivity {
|
||||
@BindView(R.id.about_version) TextView versionText;
|
||||
@BindView(R.id.about_license) HtmlTextView aboutLicenseText;
|
||||
|
||||
/**
|
||||
* This method helps in the creation About screen
|
||||
*
|
||||
* @param savedInstanceState Data bundle
|
||||
*/
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
|
@ -21,15 +31,38 @@ public class AboutActivity extends NavigationBaseActivity {
|
|||
|
||||
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);
|
||||
|
||||
versionText.setText(BuildConfig.VERSION_NAME);
|
||||
initDrawer();
|
||||
}
|
||||
|
||||
public static void startYourself(Context context) {
|
||||
Intent settingsIntent = new Intent(context, AboutActivity.class);
|
||||
context.startActivity(settingsIntent);
|
||||
@OnClick(R.id.facebook_launch_icon)
|
||||
public void launchFacebook(View view) {
|
||||
|
||||
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;
|
||||
|
||||
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.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
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.stetho.Stetho;
|
||||
import com.squareup.leakcanary.LeakCanary;
|
||||
import com.squareup.leakcanary.RefWatcher;
|
||||
|
||||
import org.acra.ACRA;
|
||||
import org.acra.ReportingInteractionMode;
|
||||
import org.acra.annotation.ReportsCrashes;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import fr.free.nrw.commons.auth.AccountUtil;
|
||||
import fr.free.nrw.commons.caching.CacheController;
|
||||
import fr.free.nrw.commons.contributions.Contribution;
|
||||
import fr.free.nrw.commons.data.Category;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
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.modifications.ModifierSequence;
|
||||
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.di.ApplicationlessInjection;
|
||||
import fr.free.nrw.commons.di.CommonsApplicationComponent;
|
||||
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
|
||||
import fr.free.nrw.commons.utils.FileUtils;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import timber.log.Timber;
|
||||
|
||||
// TODO: Use ProGuard to rip out reporting when publishing
|
||||
|
|
@ -48,86 +42,43 @@ import timber.log.Timber;
|
|||
)
|
||||
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};
|
||||
public static final Object[] EVENT_LOGIN_ATTEMPT = {"MobileAppLoginAttempts", 5257721L};
|
||||
public static final Object[] EVENT_SHARE_ATTEMPT = {"MobileAppShareAttempts", 5346170L};
|
||||
public static final Object[] EVENT_CATEGORIZATION_ATTEMPT = {"MobileAppCategorizationAttempts", 5359208L};
|
||||
@Inject @Named("default_preferences") SharedPreferences defaultPrefs;
|
||||
@Inject @Named("application_preferences") SharedPreferences applicationPrefs;
|
||||
@Inject @Named("prefs") SharedPreferences otherPrefs;
|
||||
|
||||
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 LOGS_PRIVATE_EMAIL = "commons-app-android-private@googlegroups.com";
|
||||
|
||||
public static final String FEEDBACK_EMAIL_SUBJECT = "Commons Android App (%s) Feedback";
|
||||
|
||||
private static CommonsApplication instance = null;
|
||||
private MediaWikiApi api = null;
|
||||
private LruCache<String, String> thumbnailUrlCache = new LruCache<>(1024);
|
||||
private CacheController cacheData = null;
|
||||
private DBOpenHelper dbOpenHelper = null;
|
||||
private NearbyPlaces nearbyPlaces = null;
|
||||
private RefWatcher refWatcher;
|
||||
|
||||
|
||||
/**
|
||||
* This should not be called by ANY application code (other than the magic Android glue)
|
||||
* Use CommonsApplication.getInstance() instead to get the singleton.
|
||||
* Used to declare and initialize various components and dependencies
|
||||
*/
|
||||
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
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
if (LeakCanary.isInAnalyzerProcess(this)) {
|
||||
// This process is dedicated to LeakCanary for heap analysis.
|
||||
// You should not init your app in this process.
|
||||
|
||||
ApplicationlessInjection
|
||||
.getInstance(this)
|
||||
.getCommonsApplicationComponent()
|
||||
.inject(this);
|
||||
|
||||
Fresco.initialize(this);
|
||||
if (setupLeakCanary() == RefWatcher.DISABLED) {
|
||||
return;
|
||||
}
|
||||
LeakCanary.install(this);
|
||||
|
||||
Timber.plant(new Timber.DebugTree());
|
||||
|
||||
|
||||
|
||||
if (!BuildConfig.DEBUG) {
|
||||
ACRA.init(this);
|
||||
} else {
|
||||
|
|
@ -136,52 +87,36 @@ public class CommonsApplication extends Application {
|
|||
|
||||
// Fire progress callbacks for every 3% of uploaded content
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a way to get member refWatcher
|
||||
*
|
||||
* @param context Application context
|
||||
* @return application member refWatcher
|
||||
*/
|
||||
public static RefWatcher getRefWatcher(Context context) {
|
||||
CommonsApplication application = (CommonsApplication) context.getApplicationContext();
|
||||
return application.refWatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Account|null
|
||||
* clears data of current application
|
||||
* @param context Application context
|
||||
* @param logoutListener Implementation of interface LogoutListener
|
||||
*/
|
||||
public Account getCurrentAccount() {
|
||||
if (currentAccount == null) {
|
||||
AccountManager accountManager = AccountManager.get(this);
|
||||
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);
|
||||
}
|
||||
|
||||
public void clearApplicationData(Context context, LogoutListener logoutListener) {
|
||||
File cacheDirectory = context.getCacheDir();
|
||||
File applicationDirectory = new File(cacheDirectory.getParent());
|
||||
|
|
@ -194,70 +129,37 @@ public class CommonsApplication extends Application {
|
|||
}
|
||||
}
|
||||
|
||||
AccountManager accountManager = AccountManager.get(this);
|
||||
Account[] allAccounts = accountManager.getAccountsByType(AccountUtil.accountType());
|
||||
|
||||
AccountManagerCallback<Boolean> amCallback = new AccountManagerCallback<Boolean>() {
|
||||
|
||||
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) {
|
||||
sessionManager.clearAllAccounts()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(() -> {
|
||||
Timber.d("All accounts have been removed");
|
||||
//TODO: fix preference manager
|
||||
PreferenceManager.getDefaultSharedPreferences(getInstance())
|
||||
.edit().clear().commit();
|
||||
SharedPreferences preferences = context
|
||||
.getSharedPreferences("fr.free.nrw.commons", MODE_PRIVATE);
|
||||
preferences.edit().clear().commit();
|
||||
context.getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
||||
.edit().clear().commit();
|
||||
preferences.edit().putBoolean("firstrun", false).apply();
|
||||
defaultPrefs.edit().clear().apply();
|
||||
applicationPrefs.edit().clear().apply();
|
||||
applicationPrefs.edit().putBoolean("firstrun", false).apply();
|
||||
otherPrefs.edit().clear().apply();
|
||||
updateAllDatabases();
|
||||
currentAccount = null;
|
||||
|
||||
logoutListener.onLogoutComplete();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (Account account : allAccounts) {
|
||||
accountManager.removeAccount(account, amCallback, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all tables and re-creates them.
|
||||
*/
|
||||
public void updateAllDatabases() {
|
||||
DBOpenHelper dbOpenHelper = CommonsApplication.getInstance().getDBOpenHelper();
|
||||
private void updateAllDatabases() {
|
||||
dbOpenHelper.getReadableDatabase().close();
|
||||
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
|
||||
|
||||
ModifierSequence.Table.onDelete(db);
|
||||
Category.Table.onDelete(db);
|
||||
Contribution.Table.onDelete(db);
|
||||
ModifierSequenceDao.Table.onDelete(db);
|
||||
CategoryDao.Table.onDelete(db);
|
||||
ContributionDao.Table.onDelete(db);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface used to get log-out events
|
||||
*/
|
||||
public interface LogoutListener {
|
||||
void onLogoutComplete();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.os.Binder;
|
||||
import android.os.Handler;
|
||||
|
|
@ -9,7 +8,9 @@ import android.os.IBinder;
|
|||
import android.os.Looper;
|
||||
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 ServiceHandler threadHandler;
|
||||
private String serviceName;
|
||||
|
|
|
|||
|
|
@ -2,12 +2,25 @@ package fr.free.nrw.commons;
|
|||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* represents Licence object
|
||||
*/
|
||||
public class License {
|
||||
private String key;
|
||||
private String template;
|
||||
private String url;
|
||||
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) {
|
||||
if (key == null) {
|
||||
throw new RuntimeException("License.key must not be null");
|
||||
|
|
@ -21,10 +34,18 @@ public class License {
|
|||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the license key.
|
||||
* @return license key as a String.
|
||||
*/
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the license template.
|
||||
* @return license template as a String.
|
||||
*/
|
||||
public String getTemplate() {
|
||||
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) {
|
||||
if (url == null) {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -5,21 +5,31 @@ import android.content.res.Resources;
|
|||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Represents a list of Licenses
|
||||
*/
|
||||
public class LicenseList {
|
||||
private Map<String, License> licenses = new HashMap<>();
|
||||
private Resources res;
|
||||
|
||||
/**
|
||||
* Constructs new instance of LicenceList
|
||||
*
|
||||
* @param activity License activity
|
||||
*/
|
||||
public LicenseList(Activity activity) {
|
||||
res = activity.getResources();
|
||||
XmlPullParser parser = res.getXml(R.xml.wikimedia_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 template = parser.getAttributeValue(null, "template");
|
||||
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() {
|
||||
return licenses.values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets license
|
||||
* @param key License key
|
||||
* @return License that matches key
|
||||
*/
|
||||
public License get(String key) {
|
||||
return licenses.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a license from template
|
||||
* @param template License template
|
||||
* @return null
|
||||
*/
|
||||
@Nullable
|
||||
License licenseForTemplate(String template) {
|
||||
String ucTemplate = new PageTitle(template).getDisplayText();
|
||||
|
|
@ -48,6 +72,11 @@ public class LicenseList {
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets template name id
|
||||
* @param template License template
|
||||
* @return name id of template
|
||||
*/
|
||||
private String nameIdForTemplate(String template) {
|
||||
// hack :D (converts dashes and periods to underscores)
|
||||
// cc-by-sa-3.0 -> cc_by_sa_3_0
|
||||
|
|
@ -55,9 +84,44 @@ public class LicenseList {
|
|||
"_").replace(".", "_");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets name of given template
|
||||
* @param template License template
|
||||
* @return name of template
|
||||
*/
|
||||
private String nameForTemplate(String template) {
|
||||
int nameId = res.getIdentifier("fr.free.nrw.commons:string/"
|
||||
+ nameIdForTemplate(template), null, null);
|
||||
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 @Nullable LatLng coordinates;
|
||||
|
||||
/**
|
||||
* Provides local constructor
|
||||
*/
|
||||
protected Media() {
|
||||
this.categories = new ArrayList<>();
|
||||
this.descriptions = new HashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a minimal constructor
|
||||
*
|
||||
* @param filename Media filename
|
||||
*/
|
||||
public Media(String filename) {
|
||||
this();
|
||||
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,
|
||||
long dataLength, Date dateCreated, @Nullable Date dateUploaded, String creator) {
|
||||
this();
|
||||
|
|
@ -90,19 +109,33 @@ public class Media implements Parcelable {
|
|||
descriptions = in.readHashMap(ClassLoader.getSystemClassLoader());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets tag of media
|
||||
* @param key Media key
|
||||
* @return Media tag
|
||||
*/
|
||||
public Object getTag(String 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) {
|
||||
tags.put(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets media display title
|
||||
* @return Media title
|
||||
*/
|
||||
public String getDisplayTitle() {
|
||||
if (filename == null) {
|
||||
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:", "");
|
||||
Matcher matcher = displayTitlePattern.matcher(title);
|
||||
if (matcher.matches()) {
|
||||
|
|
@ -112,109 +145,215 @@ public class Media implements Parcelable {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets file page title
|
||||
* @return New media page title
|
||||
*/
|
||||
public PageTitle getFilePageTitle() {
|
||||
return new PageTitle("File:" + getFilename().replaceFirst("^File:", ""));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets local URI
|
||||
* @return Media local URI
|
||||
*/
|
||||
public Uri getLocalUri() {
|
||||
return localUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets image URL
|
||||
* can be null.
|
||||
* @return Image URL
|
||||
*/
|
||||
@Nullable
|
||||
public String getImageUrl() {
|
||||
if (imageUrl == null) {
|
||||
if (imageUrl == null && this.getFilename() != null) {
|
||||
imageUrl = Utils.makeThumbBaseUrl(this.getFilename());
|
||||
}
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of the file.
|
||||
* @return file name as a string
|
||||
*/
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name of the file.
|
||||
* @param filename the new name of the file
|
||||
*/
|
||||
public void setFilename(String filename) {
|
||||
this.filename = filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the file description.
|
||||
* @return file description as a string
|
||||
*/
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the file description.
|
||||
* @param description the new description of the file
|
||||
*/
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the datalength of the file.
|
||||
* @return file datalength as a long
|
||||
*/
|
||||
public long getDataLength() {
|
||||
return dataLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the datalength of the file.
|
||||
* @param dataLength as a long
|
||||
*/
|
||||
public void setDataLength(long dataLength) {
|
||||
this.dataLength = dataLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the creation date of the file.
|
||||
* @return creation date as a Date
|
||||
*/
|
||||
public Date getDateCreated() {
|
||||
return dateCreated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the creation date of the file.
|
||||
* @param date creation date as a Date
|
||||
*/
|
||||
public void setDateCreated(Date date) {
|
||||
this.dateCreated = date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the upload date of the file.
|
||||
* Can be null.
|
||||
* @return upload date as a Date
|
||||
*/
|
||||
public @Nullable
|
||||
Date getDateUploaded() {
|
||||
return dateUploaded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of the creator of the file.
|
||||
* @return creator name as a String
|
||||
*/
|
||||
public String getCreator() {
|
||||
return creator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the creator name of the file.
|
||||
* @param creator creator name as a string
|
||||
*/
|
||||
public void setCreator(String creator) {
|
||||
this.creator = creator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the width of the media.
|
||||
* @return file width as an int
|
||||
*/
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the width of the media.
|
||||
* @param width file width as an int
|
||||
*/
|
||||
public void setWidth(int width) {
|
||||
this.width = width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the height of the media.
|
||||
* @return file height as an int
|
||||
*/
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the height of the media.
|
||||
* @param height file height as an int
|
||||
*/
|
||||
public void setHeight(int height) {
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the license name of the file.
|
||||
* @return license as a String
|
||||
*/
|
||||
public String getLicense() {
|
||||
return license;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the license name of the file.
|
||||
* @param license license name as a String
|
||||
*/
|
||||
public void setLicense(String license) {
|
||||
this.license = license;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the coordinates of where the file was created.
|
||||
* @return file coordinates as a LatLng
|
||||
*/
|
||||
public @Nullable
|
||||
LatLng getCoordinates() {
|
||||
return coordinates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the coordinates of where the file was created.
|
||||
* @param coordinates file coordinates as a LatLng
|
||||
*/
|
||||
public void setCoordinates(@Nullable LatLng coordinates) {
|
||||
this.coordinates = coordinates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the categories the file falls under.
|
||||
* @return file categories as an ArrayList of Strings
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public ArrayList<String> getCategories() {
|
||||
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) {
|
||||
this.categories.clear();
|
||||
this.categories.addAll(categories);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies (or sets) media descriptions
|
||||
* @param descriptions Media descriptions
|
||||
*/
|
||||
void setDescriptions(Map<String, String> descriptions) {
|
||||
for (String key : this.descriptions.keySet()) {
|
||||
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) {
|
||||
if (descriptions.containsKey(preferredLanguage)) {
|
||||
// See if the requested language is there.
|
||||
|
|
@ -240,11 +384,21 @@ public class Media implements Parcelable {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method of Parcelable interface
|
||||
* @return zero
|
||||
*/
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a way to transfer information between two or more
|
||||
* activities.
|
||||
* @param parcel Instance of Parcel
|
||||
* @param flags Parcel flag
|
||||
*/
|
||||
@Override
|
||||
public void writeToParcel(Parcel parcel, int flags) {
|
||||
parcel.writeParcelable(localUri, flags);
|
||||
|
|
|
|||
|
|
@ -11,12 +11,12 @@ import org.xml.sax.SAXException;
|
|||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
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.
|
||||
*/
|
||||
public class MediaDataExtractor {
|
||||
private final MediaWikiApi mediaWikiApi;
|
||||
private boolean fetched;
|
||||
|
||||
private String filename;
|
||||
private ArrayList<String> categories;
|
||||
private Map<String, String> descriptions;
|
||||
private Date date;
|
||||
private String license;
|
||||
private @Nullable LatLng coordinates;
|
||||
private LicenseList licenseList;
|
||||
|
||||
/**
|
||||
* @param filename of the target media object, should include 'File:' prefix
|
||||
*/
|
||||
public MediaDataExtractor(String filename, LicenseList licenseList) {
|
||||
this.filename = filename;
|
||||
categories = new ArrayList<>();
|
||||
descriptions = new HashMap<>();
|
||||
fetched = false;
|
||||
this.licenseList = licenseList;
|
||||
@Inject
|
||||
public MediaDataExtractor(MediaWikiApi mwApi) {
|
||||
this.categories = new ArrayList<>();
|
||||
this.descriptions = new HashMap<>();
|
||||
this.fetched = false;
|
||||
this.mediaWikiApi = mwApi;
|
||||
}
|
||||
|
||||
/**
|
||||
/*
|
||||
* Actually fetch the data over the network.
|
||||
* todo: use local caching?
|
||||
*
|
||||
* 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) {
|
||||
throw new IllegalStateException("Tried to call MediaDataExtractor.fetch() again.");
|
||||
}
|
||||
|
||||
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
|
||||
MediaResult result = api.fetchMediaByFilename(filename);
|
||||
MediaResult result = mediaWikiApi.fetchMediaByFilename(filename);
|
||||
|
||||
// In-page category links are extracted from source, as XML doesn't cover [[links]]
|
||||
extractCategories(result.getWikiSource());
|
||||
|
||||
// Description template info is extracted from preprocessor XML
|
||||
processWikiParseTree(result.getParseTreeXmlSource());
|
||||
processWikiParseTree(result.getParseTreeXmlSource(), licenseList);
|
||||
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;
|
||||
try {
|
||||
DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
|
||||
|
|
@ -153,7 +146,7 @@ public class MediaDataExtractor {
|
|||
}
|
||||
|
||||
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();
|
||||
for (int i = 0, length = nodes.getLength(); i < length; i++) {
|
||||
Node node = nodes.item(i);
|
||||
|
|
@ -179,7 +172,7 @@ public class MediaDataExtractor {
|
|||
}
|
||||
|
||||
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 {
|
||||
|
|
@ -290,7 +283,7 @@ public class MediaDataExtractor {
|
|||
/**
|
||||
* Take our metadata and inject it into a live Media object.
|
||||
* Media object might contain stale or cached data, or emptiness.
|
||||
* @param media
|
||||
* @param media Media object to inject into
|
||||
*/
|
||||
public void fill(Media media) {
|
||||
if (!fetched) {
|
||||
|
|
|
|||
|
|
@ -7,16 +7,17 @@ import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
|||
|
||||
class MediaThumbnailFetchTask extends AsyncTask<String, String, String> {
|
||||
protected final Media media;
|
||||
private MediaWikiApi mediaWikiApi;
|
||||
|
||||
public MediaThumbnailFetchTask(@NonNull Media media) {
|
||||
public MediaThumbnailFetchTask(@NonNull Media media, MediaWikiApi mwApi) {
|
||||
this.media = media;
|
||||
this.mediaWikiApi = mwApi;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String doInBackground(String... params) {
|
||||
try {
|
||||
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
|
||||
return api.findThumbnailByFilename(params[0]);
|
||||
return mediaWikiApi.findThumbnailByFilename(params[0]);
|
||||
} catch (Exception e) {
|
||||
// Do something better!
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import android.content.Context;
|
|||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.graphics.drawable.VectorDrawableCompat;
|
||||
import android.support.v4.util.LruCache;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.Toast;
|
||||
|
|
@ -11,9 +12,16 @@ import android.widget.Toast;
|
|||
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
|
||||
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;
|
||||
|
||||
public class MediaWikiImageView extends SimpleDraweeView {
|
||||
@Inject MediaWikiApi mwApi;
|
||||
@Inject LruCache<String, String> thumbnailUrlCache;
|
||||
|
||||
private ThumbnailFetchTask currentThumbnailTask;
|
||||
|
||||
public MediaWikiImageView(Context context) {
|
||||
|
|
@ -31,19 +39,23 @@ public class MediaWikiImageView extends SimpleDraweeView {
|
|||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the media. Fetches its thumbnail if necessary.
|
||||
* @param media the new media
|
||||
*/
|
||||
public void setMedia(Media media) {
|
||||
if (currentThumbnailTask != null) {
|
||||
currentThumbnailTask.cancel(true);
|
||||
}
|
||||
if(media == null) {
|
||||
if (media == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (CommonsApplication.getInstance().getThumbnailUrlCache().get(media.getFilename()) != null) {
|
||||
setImageUrl(CommonsApplication.getInstance().getThumbnailUrlCache().get(media.getFilename()));
|
||||
if (thumbnailUrlCache.get(media.getFilename()) != null) {
|
||||
setImageUrl(thumbnailUrlCache.get(media.getFilename()));
|
||||
} else {
|
||||
setImageUrl(null);
|
||||
currentThumbnailTask = new ThumbnailFetchTask(media);
|
||||
currentThumbnailTask = new ThumbnailFetchTask(media, mwApi);
|
||||
currentThumbnailTask.execute(media.getFilename());
|
||||
}
|
||||
}
|
||||
|
|
@ -56,7 +68,15 @@ public class MediaWikiImageView extends SimpleDraweeView {
|
|||
super.onDetachedFromWindow();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes MediaWikiImageView.
|
||||
*/
|
||||
private void init() {
|
||||
ApplicationlessInjection
|
||||
.getInstance(getContext()
|
||||
.getApplicationContext())
|
||||
.getCommonsApplicationComponent()
|
||||
.inject(this);
|
||||
setHierarchy(GenericDraweeHierarchyBuilder
|
||||
.newInstance(getResources())
|
||||
.setPlaceholderImage(VectorDrawableCompat.create(getResources(),
|
||||
|
|
@ -66,13 +86,17 @@ public class MediaWikiImageView extends SimpleDraweeView {
|
|||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the image from the URL.
|
||||
* @param url the URL of the image
|
||||
*/
|
||||
private void setImageUrl(@Nullable String url) {
|
||||
setImageURI(url);
|
||||
}
|
||||
|
||||
private class ThumbnailFetchTask extends MediaThumbnailFetchTask {
|
||||
ThumbnailFetchTask(@NonNull Media media) {
|
||||
super(media);
|
||||
ThumbnailFetchTask(@NonNull Media media, @NonNull MediaWikiApi mwApi) {
|
||||
super(media, mwApi);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -85,7 +109,7 @@ public class MediaWikiImageView extends SimpleDraweeView {
|
|||
} else {
|
||||
// only cache meaningful thumbnails received from network.
|
||||
try {
|
||||
CommonsApplication.getInstance().getThumbnailUrlCache().put(media.getFilename(), result);
|
||||
thumbnailUrlCache.put(media.getFilename(), result);
|
||||
} catch (NullPointerException npe) {
|
||||
Timber.e("error when adding pic to cache " + npe);
|
||||
|
||||
|
|
|
|||
|
|
@ -84,6 +84,12 @@ public class PageTitle {
|
|||
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
|
||||
public String toString() {
|
||||
return getPrefixedText();
|
||||
|
|
|
|||
|
|
@ -1,99 +1,26 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.text.Html;
|
||||
import android.text.Spanned;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
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.InputStream;
|
||||
import java.io.StringWriter;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.math.BigInteger;
|
||||
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.TimeZone;
|
||||
import java.util.regex.Matcher;
|
||||
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 timber.log.Timber;
|
||||
|
||||
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.
|
||||
* 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
|
||||
try {
|
||||
return isoFormat.parse(mwDate);
|
||||
} catch (ParseException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
/**
|
||||
* Creates an URL for thumbnail
|
||||
*
|
||||
* @param filename Thumbnail file name
|
||||
* @return URL of thumbnail
|
||||
*/
|
||||
public static String makeThumbBaseUrl(@NonNull String filename) {
|
||||
String name = new PageTitle(filename).getPrefixedText();
|
||||
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));
|
||||
}
|
||||
|
||||
public static String getStringFromDOM(Node dom) {
|
||||
Transformer transformer = null;
|
||||
try {
|
||||
transformer = TransformerFactory.newInstance().newTransformer();
|
||||
} 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* URL Encode an URL in UTF-8 format
|
||||
* @param url Unformatted URL
|
||||
* @return Encoded URL
|
||||
*/
|
||||
public static String urlEncode(String url) {
|
||||
try {
|
||||
return URLEncoder.encode(url, "utf-8");
|
||||
|
|
@ -161,39 +62,21 @@ public class Utils {
|
|||
}
|
||||
}
|
||||
|
||||
public static long countBytes(InputStream stream) throws IOException {
|
||||
long count = 0;
|
||||
BufferedInputStream bis = new BufferedInputStream(stream);
|
||||
while (bis.read() != -1) {
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Capitalizes the first character of a string.
|
||||
*
|
||||
* @param string String to alter
|
||||
* @return string with capitalized first character
|
||||
*/
|
||||
public static String capitalize(String string) {
|
||||
return string.substring(0, 1).toUpperCase(Locale.getDefault()) + string.substring(1);
|
||||
}
|
||||
|
||||
public static String licenseTemplateFor(String license) {
|
||||
switch (license) {
|
||||
case Prefs.Licenses.CC_BY_3:
|
||||
return "{{self|cc-by-3.0}}";
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates licence name with given ID
|
||||
* @param license License ID
|
||||
* @return Name of license
|
||||
*/
|
||||
public static int licenseNameFor(String license) {
|
||||
switch (license) {
|
||||
case Prefs.Licenses.CC_BY_3:
|
||||
|
|
@ -214,51 +97,12 @@ public class Utils {
|
|||
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
|
||||
* in the input stream (namespaced).
|
||||
*
|
||||
* @param parser
|
||||
* @param namespace
|
||||
* @param element
|
||||
* @return true on match, false on failure
|
||||
* Fixing incorrect extension
|
||||
* @param title File name
|
||||
* @param extension Correct extension
|
||||
* @return File with correct extension
|
||||
*/
|
||||
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) {
|
||||
Pattern jpegPattern = Pattern.compile("\\.jpeg$", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
|
|
@ -274,11 +118,45 @@ public class Utils {
|
|||
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) {
|
||||
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();
|
||||
|
||||
/**
|
||||
* Initialises exiting fields and dependencies
|
||||
*
|
||||
* @param savedInstanceState WelcomeActivity bundled data
|
||||
*/
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
|
@ -30,12 +35,20 @@ public class WelcomeActivity extends BaseActivity {
|
|||
adapter.setCallback(this::finish);
|
||||
}
|
||||
|
||||
/**
|
||||
* References WelcomePageAdapter to null before the activity is destroyed
|
||||
*/
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
adapter.setCallback(null);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a way to change current activity to WelcomeActivity
|
||||
*
|
||||
* @param context Activity context
|
||||
*/
|
||||
public static void startYourself(Context context) {
|
||||
Intent welcomeIntent = new Intent(context, WelcomeActivity.class);
|
||||
context.startActivity(welcomeIntent);
|
||||
|
|
|
|||
|
|
@ -10,13 +10,6 @@ import butterknife.ButterKnife;
|
|||
import butterknife.OnClick;
|
||||
|
||||
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[]{
|
||||
R.layout.welcome_wikipedia,
|
||||
R.layout.welcome_do_upload,
|
||||
|
|
@ -24,16 +17,34 @@ public class WelcomePagerAdapter extends PagerAdapter {
|
|||
R.layout.welcome_image_details,
|
||||
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) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets total number of layouts
|
||||
* @return Number of layouts
|
||||
*/
|
||||
@Override
|
||||
public int getCount() {
|
||||
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
|
||||
public boolean isViewFromObject(View view, Object object) {
|
||||
return (view == object);
|
||||
|
|
@ -52,16 +63,29 @@ public class WelcomePagerAdapter extends PagerAdapter {
|
|||
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
|
||||
public void destroyItem(ViewGroup container, int position, Object obj) {
|
||||
container.removeView((View) obj);
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void onYesClicked();
|
||||
}
|
||||
|
||||
class ViewHolder {
|
||||
ViewHolder(View view) {
|
||||
ButterKnife.bind(this, view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers on click callback on button click
|
||||
*/
|
||||
@OnClick(R.id.welcomeYesButton)
|
||||
void onClicked() {
|
||||
if (callback != null) {
|
||||
|
|
|
|||
|
|
@ -4,21 +4,33 @@ import android.accounts.Account;
|
|||
import android.accounts.AccountAuthenticatorResponse;
|
||||
import android.accounts.AccountManager;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
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 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 static void createAccount(@Nullable AccountAuthenticatorResponse response,
|
||||
String username, String password) {
|
||||
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;
|
||||
|
||||
Account account = new Account(username, accountType());
|
||||
public AccountUtil(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public void createAccount(@Nullable AccountAuthenticatorResponse response,
|
||||
String username, String password) {
|
||||
|
||||
Account account = new Account(username, ACCOUNT_TYPE);
|
||||
boolean created = accountManager().addAccountExplicitly(account, password, null);
|
||||
|
||||
Timber.d("account creation " + (created ? "successful" : "failure"));
|
||||
|
|
@ -26,8 +38,8 @@ public class AccountUtil {
|
|||
if (created) {
|
||||
if (response != null) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(AccountManager.KEY_ACCOUNT_NAME, username);
|
||||
bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, accountType());
|
||||
bundle.putString(KEY_ACCOUNT_NAME, username);
|
||||
bundle.putString(KEY_ACCOUNT_TYPE, ACCOUNT_TYPE);
|
||||
|
||||
|
||||
response.onResult(bundle);
|
||||
|
|
@ -35,28 +47,18 @@ public class AccountUtil {
|
|||
|
||||
} else {
|
||||
if (response != null) {
|
||||
response.onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, "");
|
||||
response.onError(ERROR_CODE_REMOTE_EXCEPTION, "");
|
||||
}
|
||||
Timber.d("account creation failure");
|
||||
}
|
||||
|
||||
// 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, ModificationsContentProvider.AUTHORITY, true); // Enable sync by default!
|
||||
ContentResolver.setSyncAutomatically(account, CONTRIBUTION_AUTHORITY, true); // Enable sync by default!
|
||||
ContentResolver.setSyncAutomatically(account, MODIFICATIONS_AUTHORITY, true); // Enable sync by default!
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String accountType() {
|
||||
return "fr.free.nrw.commons";
|
||||
}
|
||||
|
||||
private static AccountManager accountManager() {
|
||||
return AccountManager.get(app());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static CommonsApplication app() {
|
||||
return CommonsApplication.getInstance();
|
||||
private AccountManager accountManager() {
|
||||
return AccountManager.get(context);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,84 +1,45 @@
|
|||
package fr.free.nrw.commons.auth;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountManager;
|
||||
import android.accounts.AccountManagerFuture;
|
||||
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 io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static fr.free.nrw.commons.auth.AccountUtil.AUTH_COOKIE;
|
||||
|
||||
public abstract class AuthenticatedActivity extends NavigationBaseActivity {
|
||||
|
||||
private String accountType;
|
||||
CommonsApplication app;
|
||||
|
||||
@Inject SessionManager sessionManager;
|
||||
@Inject
|
||||
MediaWikiApi mediaWikiApi;
|
||||
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() {
|
||||
if (authCookie != null) {
|
||||
onAuthCookieAcquired(authCookie);
|
||||
return;
|
||||
}
|
||||
AccountManager accountManager = AccountManager.get(this);
|
||||
Account curAccount = app.getCurrentAccount();
|
||||
if (curAccount == null) {
|
||||
addAccount(accountManager);
|
||||
} else {
|
||||
getAuthCookie(curAccount, accountManager);
|
||||
authCookie = sessionManager.getAuthCookie();
|
||||
if (authCookie != null) {
|
||||
onAuthCookieAcquired(authCookie);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
app = CommonsApplication.getInstance();
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
authCookie = savedInstanceState.getString("authCookie");
|
||||
authCookie = savedInstanceState.getString(AUTH_COOKIE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putString("authCookie", authCookie);
|
||||
outState.putString(AUTH_COOKIE, authCookie);
|
||||
}
|
||||
|
||||
protected abstract void onAuthCookieAcquired(String authCookie);
|
||||
|
|
|
|||
|
|
@ -1,115 +1,118 @@
|
|||
package fr.free.nrw.commons.auth;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountAuthenticatorActivity;
|
||||
import android.accounts.AccountAuthenticatorResponse;
|
||||
import android.accounts.AccountManager;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
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.content.ContextCompat;
|
||||
import android.support.v7.app.AppCompatDelegate;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
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.CommonsApplication;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.WelcomeActivity;
|
||||
|
||||
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.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 static android.view.KeyEvent.KEYCODE_ENTER;
|
||||
import static android.view.View.VISIBLE;
|
||||
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 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;
|
||||
private EditText usernameEdit;
|
||||
private EditText passwordEdit;
|
||||
private EditText twoFactorEdit;
|
||||
@BindView(R.id.loginButton) Button loginButton;
|
||||
@BindView(R.id.signupButton) Button signupButton;
|
||||
@BindView(R.id.loginUsername) EditText usernameEdit;
|
||||
@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;
|
||||
private AppCompatDelegate delegate;
|
||||
private LoginTextWatcher textWatcher = new LoginTextWatcher();
|
||||
|
||||
private CommonsApplication app;
|
||||
|
||||
@Override
|
||||
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);
|
||||
|
||||
loginButton = (Button) findViewById(R.id.loginButton);
|
||||
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);
|
||||
ButterKnife.bind(this);
|
||||
|
||||
usernameEdit.addTextChangedListener(textWatcher);
|
||||
passwordEdit.addTextChangedListener(textWatcher);
|
||||
twoFactorEdit.addTextChangedListener(textWatcher);
|
||||
passwordEdit.setOnEditorActionListener(newLoginInputActionListener());
|
||||
|
||||
loginButton.setOnClickListener(this::performLogin);
|
||||
signupButton.setOnClickListener(this::signUp);
|
||||
loginButton.setOnClickListener(view -> performLogin());
|
||||
signupButton.setOnClickListener(view -> signUp());
|
||||
}
|
||||
|
||||
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) {
|
||||
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;
|
||||
};
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
getDelegate().onPostCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (prefs.getBoolean("firstrun", true)) {
|
||||
WelcomeActivity.startYourself(this);
|
||||
prefs.edit().putBoolean("firstrun", false).apply();
|
||||
}
|
||||
if (app.getCurrentAccount() != null) {
|
||||
if (sessionManager.getCurrentAccount() != null) {
|
||||
startMainActivity();
|
||||
}
|
||||
}
|
||||
|
|
@ -127,22 +130,113 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
usernameEdit.removeTextChangedListener(textWatcher);
|
||||
passwordEdit.removeTextChangedListener(textWatcher);
|
||||
twoFactorEdit.removeTextChangedListener(textWatcher);
|
||||
delegate.onDestroy();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
private void performLogin(View view) {
|
||||
private void performLogin() {
|
||||
Timber.d("Login to start!");
|
||||
LoginTask task = getLoginTask();
|
||||
task.execute();
|
||||
final String username = canonicializeUsername(usernameEdit.getText().toString());
|
||||
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() {
|
||||
return new LoginTask(
|
||||
this,
|
||||
canonicializeUsername(usernameEdit.getText().toString()),
|
||||
passwordEdit.getText().toString(),
|
||||
twoFactorEdit.getText().toString()
|
||||
);
|
||||
private String login(String username, String password, String twoFactorCode) {
|
||||
try {
|
||||
if (twoFactorCode.isEmpty()) {
|
||||
return mwApi.login(username, password);
|
||||
} else {
|
||||
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();
|
||||
}
|
||||
|
||||
@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
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
|
|
@ -164,36 +281,26 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when Sign Up button is clicked.
|
||||
* @param view View
|
||||
*/
|
||||
public void signUp(View view) {
|
||||
Intent intent = new Intent(this, SignupActivity.class);
|
||||
startActivity(intent);
|
||||
@Override
|
||||
@NonNull
|
||||
public MenuInflater getMenuInflater() {
|
||||
return getDelegate().getMenuInflater();
|
||||
}
|
||||
|
||||
public void askUserForTwoFactorAuth() {
|
||||
if (BuildConfig.DEBUG) {
|
||||
twoFactorEdit.setVisibility(View.VISIBLE);
|
||||
showUserToastAndCancelDialog(R.string.login_failed_2fa_needed);
|
||||
} else {
|
||||
showUserToastAndCancelDialog(R.string.login_failed_2fa_not_supported);
|
||||
}
|
||||
progressDialog.dismiss();
|
||||
twoFactorContainer.setVisibility(VISIBLE);
|
||||
twoFactorEdit.setVisibility(VISIBLE);
|
||||
showMessageAndCancelDialog(R.string.login_failed_2fa_needed);
|
||||
}
|
||||
|
||||
public void showUserToastAndCancelDialog(int resId) {
|
||||
showUserToast(resId);
|
||||
public void showMessageAndCancelDialog(@StringRes int resId) {
|
||||
showMessage(resId, R.color.secondaryDarkColor);
|
||||
progressDialog.cancel();
|
||||
}
|
||||
|
||||
private void showUserToast(int resId) {
|
||||
Toast.makeText(this, resId, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
public void showSuccessToastAndDismissDialog() {
|
||||
Toast successToast = Toast.makeText(this, R.string.login_success, Toast.LENGTH_SHORT);
|
||||
successToast.show();
|
||||
public void showSuccessAndDismissDialog() {
|
||||
showMessage(R.string.login_success, R.color.primaryDarkColor);
|
||||
progressDialog.dismiss();
|
||||
}
|
||||
|
||||
|
|
@ -203,8 +310,57 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
}
|
||||
|
||||
public void startMainActivity() {
|
||||
ContributionsActivity.startYourself(this);
|
||||
NavigationBaseActivity.startActivityWithFlags(this, ContributionsActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
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 fr.free.nrw.commons.BuildConfig;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.theme.BaseActivity;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
|
@ -39,11 +38,8 @@ public class SignupActivity extends BaseActivity {
|
|||
//Signup success, so clear cookies, notify user, and load LoginActivity again
|
||||
Timber.d("Overriding URL %s", url);
|
||||
|
||||
Toast toast = Toast.makeText(
|
||||
CommonsApplication.getInstance(),
|
||||
"Account created!",
|
||||
Toast.LENGTH_LONG
|
||||
);
|
||||
Toast toast = Toast.makeText(SignupActivity.this,
|
||||
"Account created!", Toast.LENGTH_LONG);
|
||||
toast.show();
|
||||
// terminate on task completion.
|
||||
finish();
|
||||
|
|
|
|||
|
|
@ -5,51 +5,37 @@ import android.accounts.Account;
|
|||
import android.accounts.AccountAuthenticatorResponse;
|
||||
import android.accounts.AccountManager;
|
||||
import android.accounts.NetworkErrorException;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
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 fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
|
||||
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;
|
||||
import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE;
|
||||
import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE;
|
||||
|
||||
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);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
private Bundle unsupportedOperation() {
|
||||
@Override
|
||||
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt(KEY_ERROR_CODE, ERROR_CODE_UNSUPPORTED_OPERATION);
|
||||
|
||||
// HACK: the docs indicate that this is a required key bit it's not displayed to the user.
|
||||
bundle.putString(KEY_ERROR_MESSAGE, "");
|
||||
|
||||
bundle.putString("test", "editProperties");
|
||||
return bundle;
|
||||
}
|
||||
|
||||
private boolean supportedAccountType(@Nullable String type) {
|
||||
return AccountUtil.accountType().equals(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle addAccount(@NonNull AccountAuthenticatorResponse response,
|
||||
@NonNull String accountType, @Nullable String authTokenType,
|
||||
|
|
@ -57,87 +43,48 @@ public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
|
|||
throws NetworkErrorException {
|
||||
|
||||
if (!supportedAccountType(accountType)) {
|
||||
return unsupportedOperation();
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("test", "addAccount");
|
||||
return bundle;
|
||||
}
|
||||
|
||||
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
|
||||
public Bundle confirmCredentials(@NonNull AccountAuthenticatorResponse response,
|
||||
@NonNull Account account, @Nullable Bundle options)
|
||||
throws NetworkErrorException {
|
||||
return unsupportedOperation();
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("test", "confirmCredentials");
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
|
||||
return unsupportedOperation();
|
||||
}
|
||||
|
||||
private String getAuthCookie(String username, String password) throws IOException {
|
||||
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
|
||||
//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);
|
||||
public Bundle getAuthToken(@NonNull AccountAuthenticatorResponse response,
|
||||
@NonNull Account account, @NonNull String authTokenType,
|
||||
@Nullable Bundle options)
|
||||
throws NetworkErrorException {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("test", "getAuthToken");
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getAuthTokenLabel(@NonNull String authTokenType) {
|
||||
//Note: the wikipedia app actually returns a string here....
|
||||
//return supportedAccountType(authTokenType) ? context.getString(R.string.wikimedia) : null;
|
||||
return null;
|
||||
return supportedAccountType(authTokenType) ? AUTH_TOKEN_TYPE : 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
|
||||
|
|
@ -146,16 +93,50 @@ public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
|
|||
@NonNull Account account, @NonNull String[] features)
|
||||
throws NetworkErrorException {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putBoolean(KEY_BOOLEAN_RESULT, false);
|
||||
bundle.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Bundle updateCredentials(@NonNull AccountAuthenticatorResponse response,
|
||||
@NonNull Account account, @Nullable String authTokenType,
|
||||
@Nullable Bundle options) throws NetworkErrorException {
|
||||
return unsupportedOperation();
|
||||
private boolean supportedAccountType(@Nullable String type) {
|
||||
return ACCOUNT_TYPE.equals(type);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
import android.accounts.AccountManager;
|
||||
import android.app.Service;
|
||||
import android.accounts.AbstractAccountAuthenticator;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
public class WikiAccountAuthenticatorService extends Service {
|
||||
import fr.free.nrw.commons.di.CommonsDaggerService;
|
||||
|
||||
public class WikiAccountAuthenticatorService extends CommonsDaggerService {
|
||||
|
||||
@Nullable
|
||||
private AbstractAccountAuthenticator authenticator;
|
||||
|
||||
private static WikiAccountAuthenticator wikiAccountAuthenticator = null;
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
if (!intent.getAction().equals(AccountManager.ACTION_AUTHENTICATOR_INTENT)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (wikiAccountAuthenticator == null) {
|
||||
wikiAccountAuthenticator = new WikiAccountAuthenticator(this);
|
||||
}
|
||||
return wikiAccountAuthenticator.getIBinder();
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
authenticator = new WikiAccountAuthenticator(this);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return authenticator == null ? null : authenticator.getIBinder();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
package fr.free.nrw.commons.category;
|
||||
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
|
|
@ -31,11 +28,14 @@ import java.util.HashMap;
|
|||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
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.utils.StringSortingUtils;
|
||||
import io.reactivex.Observable;
|
||||
|
|
@ -45,12 +45,11 @@ import timber.log.Timber;
|
|||
|
||||
import static android.view.KeyEvent.ACTION_UP;
|
||||
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.
|
||||
*/
|
||||
public class CategorizationFragment extends Fragment {
|
||||
public class CategorizationFragment extends CommonsDaggerSupportFragment {
|
||||
|
||||
public static final int SEARCH_CATS_LIMIT = 25;
|
||||
|
||||
|
|
@ -65,16 +64,19 @@ public class CategorizationFragment extends Fragment {
|
|||
@BindView(R.id.categoriesExplanation)
|
||||
TextView categoriesSkip;
|
||||
|
||||
@Inject MediaWikiApi mwApi;
|
||||
@Inject @Named("default_preferences") SharedPreferences prefs;
|
||||
@Inject CategoryDao categoryDao;
|
||||
|
||||
private RVRendererAdapter<CategoryItem> categoriesAdapter;
|
||||
private OnCategoriesSaveHandler onCategoriesSaveHandler;
|
||||
private HashMap<String, ArrayList<String>> categoriesCache;
|
||||
private List<CategoryItem> selectedCategories = new ArrayList<>();
|
||||
private ContentProviderClient databaseClient;
|
||||
|
||||
private final CategoriesAdapterFactory adapterFactory = new CategoriesAdapterFactory(item -> {
|
||||
if (item.isSelected()) {
|
||||
selectedCategories.add(item);
|
||||
updateCategoryCount(item, databaseClient);
|
||||
updateCategoryCount(item);
|
||||
} else {
|
||||
selectedCategories.remove(item);
|
||||
}
|
||||
|
|
@ -88,13 +90,6 @@ public class CategorizationFragment extends Fragment {
|
|||
|
||||
categoriesList.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
|
||||
RxView.clicks(categoriesSkip)
|
||||
.takeUntil(RxView.detaches(categoriesSkip))
|
||||
.subscribe(o -> {
|
||||
getActivity().onBackPressed();
|
||||
getActivity().finish();
|
||||
});
|
||||
|
||||
ArrayList<CategoryItem> items = new ArrayList<>();
|
||||
categoriesCache = new HashMap<>();
|
||||
if (savedInstanceState != null) {
|
||||
|
|
@ -139,12 +134,6 @@ public class CategorizationFragment extends Fragment {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
databaseClient.release();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
|
|
@ -180,7 +169,6 @@ public class CategorizationFragment extends Fragment {
|
|||
setHasOptionsMenu(true);
|
||||
onCategoriesSaveHandler = (OnCategoriesSaveHandler) getActivity();
|
||||
getActivity().setTitle(R.string.categories_activity_title);
|
||||
databaseClient = getActivity().getContentResolver().acquireContentProviderClient(AUTHORITY);
|
||||
}
|
||||
|
||||
private void updateCategoryList(String filter) {
|
||||
|
|
@ -205,7 +193,9 @@ public class CategorizationFragment extends Fragment {
|
|||
.sorted(sortBySimilarity(filter))
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
s -> categoriesAdapter.add(s), Timber::e, () -> {
|
||||
s -> categoriesAdapter.add(s),
|
||||
Timber::e,
|
||||
() -> {
|
||||
categoriesAdapter.notifyDataSetChanged();
|
||||
categoriesSearchInProgress.setVisibility(View.GONE);
|
||||
|
||||
|
|
@ -253,16 +243,15 @@ public class CategorizationFragment extends Fragment {
|
|||
|
||||
private Observable<CategoryItem> titleCategories() {
|
||||
//Retrieve the title that was saved when user tapped submit icon
|
||||
SharedPreferences titleDesc = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
||||
String title = titleDesc.getString("Title", "");
|
||||
String title = prefs.getString("Title", "");
|
||||
|
||||
return CommonsApplication.getInstance().getMWApi()
|
||||
return mwApi
|
||||
.searchTitles(title, SEARCH_CATS_LIMIT)
|
||||
.map(name -> new CategoryItem(name, false));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
|
|
@ -279,7 +268,7 @@ public class CategorizationFragment extends Fragment {
|
|||
}
|
||||
|
||||
//otherwise, search API for matching categories
|
||||
return CommonsApplication.getInstance().getMWApi()
|
||||
return mwApi
|
||||
.allCategories(term, SEARCH_CATS_LIMIT)
|
||||
.map(name -> new CategoryItem(name, false));
|
||||
}
|
||||
|
|
@ -290,7 +279,7 @@ public class CategorizationFragment extends Fragment {
|
|||
return Observable.empty();
|
||||
}
|
||||
|
||||
return CommonsApplication.getInstance().getMWApi()
|
||||
return mwApi
|
||||
.searchCategories(term, SEARCH_CATS_LIMIT)
|
||||
.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)
|
||||
//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)
|
||||
//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))
|
||||
|| 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) {
|
||||
Category cat = lookupCategory(item.getName());
|
||||
cat.incTimesUsed();
|
||||
cat.save(client);
|
||||
}
|
||||
private void updateCategoryCount(CategoryItem item) {
|
||||
Category category = categoryDao.find(item.getName());
|
||||
|
||||
private Category lookupCategory(String name) {
|
||||
Category cat = Category.find(databaseClient, name);
|
||||
|
||||
if (cat == null) {
|
||||
// Newly used category...
|
||||
cat = new Category();
|
||||
cat.setName(name);
|
||||
cat.setLastUsed(new Date());
|
||||
cat.setTimesUsed(0);
|
||||
// Newly used category...
|
||||
if (category == null) {
|
||||
category = new Category(null, item.getName(), new Date(), 0);
|
||||
}
|
||||
|
||||
return cat;
|
||||
category.incTimesUsed();
|
||||
categoryDao.save(category);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
|
|
@ -10,17 +9,18 @@ import android.net.Uri;
|
|||
import android.support.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.data.Category;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import fr.free.nrw.commons.data.DBOpenHelper;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerContentProvider;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static android.content.UriMatcher.NO_MATCH;
|
||||
import static fr.free.nrw.commons.data.Category.Table.ALL_FIELDS;
|
||||
import static fr.free.nrw.commons.data.Category.Table.COLUMN_ID;
|
||||
import static fr.free.nrw.commons.data.Category.Table.TABLE_NAME;
|
||||
import static fr.free.nrw.commons.category.CategoryDao.Table.ALL_FIELDS;
|
||||
import static fr.free.nrw.commons.category.CategoryDao.Table.COLUMN_ID;
|
||||
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";
|
||||
// For URI matcher
|
||||
|
|
@ -37,19 +37,11 @@ public class CategoryContentProvider extends ContentProvider {
|
|||
uriMatcher.addURI(AUTHORITY, BASE_PATH + "/#", CATEGORIES_ID);
|
||||
}
|
||||
|
||||
private DBOpenHelper dbOpenHelper;
|
||||
|
||||
public static Uri uriForId(int id) {
|
||||
return Uri.parse(BASE_URI.toString() + "/" + id);
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
CommonsApplication app = ((CommonsApplication) getContext().getApplicationContext());
|
||||
dbOpenHelper = app.getDBOpenHelper();
|
||||
return false;
|
||||
}
|
||||
@Inject DBOpenHelper dbOpenHelper;
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -1,115 +1,64 @@
|
|||
package fr.free.nrw.commons.data;
|
||||
package fr.free.nrw.commons.category;
|
||||
|
||||
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.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
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 {
|
||||
private Uri contentUri;
|
||||
public class CategoryDao {
|
||||
|
||||
private String name;
|
||||
private Date lastUsed;
|
||||
private int timesUsed;
|
||||
private final Provider<ContentProviderClient> clientProvider;
|
||||
|
||||
// Getters/setters
|
||||
public String getName() {
|
||||
return name;
|
||||
@Inject
|
||||
public CategoryDao(@Named("category") Provider<ContentProviderClient> clientProvider) {
|
||||
this.clientProvider = clientProvider;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
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) {
|
||||
public void save(Category category) {
|
||||
ContentProviderClient db = clientProvider.get();
|
||||
try {
|
||||
if (contentUri == null) {
|
||||
contentUri = client.insert(CategoryContentProvider.BASE_URI, this.toContentValues());
|
||||
if (category.getContentUri() == null) {
|
||||
category.setContentUri(db.insert(CategoryContentProvider.BASE_URI, toContentValues(category)));
|
||||
} else {
|
||||
client.update(contentUri, toContentValues(), null, null);
|
||||
db.update(category.getContentUri(), toContentValues(category), null, null);
|
||||
}
|
||||
} catch (RemoteException 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.
|
||||
* @param client ContentProviderClient to handle DB connection
|
||||
*
|
||||
* @param name Category's name
|
||||
* @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;
|
||||
ContentProviderClient db = clientProvider.get();
|
||||
try {
|
||||
cursor = client.query(
|
||||
cursor = db.query(
|
||||
CategoryContentProvider.BASE_URI,
|
||||
Category.Table.ALL_FIELDS,
|
||||
Category.Table.COLUMN_NAME + "=?",
|
||||
Table.ALL_FIELDS,
|
||||
Table.COLUMN_NAME + "=?",
|
||||
new String[]{name},
|
||||
null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return Category.fromCursor(cursor);
|
||||
return fromCursor(cursor);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
// This feels lazy, but to hell with checked exceptions. :)
|
||||
|
|
@ -118,29 +67,32 @@ public class Category {
|
|||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
db.release();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve recently-used categories, ordered by descending date.
|
||||
*
|
||||
* @return a list containing recent categories
|
||||
*/
|
||||
public static @NonNull ArrayList<String> recentCategories(ContentProviderClient client, int limit) {
|
||||
ArrayList<String> items = new ArrayList<>();
|
||||
@NonNull
|
||||
List<String> recentCategories(int limit) {
|
||||
List<String> items = new ArrayList<>();
|
||||
Cursor cursor = null;
|
||||
ContentProviderClient db = clientProvider.get();
|
||||
try {
|
||||
cursor = client.query(
|
||||
cursor = db.query(
|
||||
CategoryContentProvider.BASE_URI,
|
||||
Category.Table.ALL_FIELDS,
|
||||
Table.ALL_FIELDS,
|
||||
null,
|
||||
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?
|
||||
while (cursor != null && cursor.moveToNext()
|
||||
&& cursor.getPosition() < limit) {
|
||||
Category cat = Category.fromCursor(cursor);
|
||||
items.add(cat.getName());
|
||||
items.add(fromCursor(cursor).getName());
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
|
|
@ -148,17 +100,36 @@ public class Category {
|
|||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
db.release();
|
||||
}
|
||||
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 final String TABLE_NAME = "categories";
|
||||
|
||||
public static final String COLUMN_ID = "_id";
|
||||
public static final String COLUMN_NAME = "name";
|
||||
public static final String COLUMN_LAST_USED = "last_used";
|
||||
public static final String COLUMN_TIMES_USED = "times_used";
|
||||
static final String COLUMN_NAME = "name";
|
||||
static final String COLUMN_LAST_USED = "last_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.
|
||||
public static final String[] ALL_FIELDS = {
|
||||
|
|
@ -168,7 +139,9 @@ public class Category {
|
|||
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_NAME + " STRING,"
|
||||
+ COLUMN_LAST_USED + " INTEGER,"
|
||||
|
|
@ -180,7 +153,7 @@ public class Category {
|
|||
}
|
||||
|
||||
public static void onDelete(SQLiteDatabase db) {
|
||||
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
|
||||
db.execSQL(DROP_TABLE_STATEMENT);
|
||||
onCreate(db);
|
||||
}
|
||||
|
||||
|
|
@ -208,5 +181,4 @@ public class Category {
|
|||
}
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
}
|
||||
|
|
@ -1,13 +1,8 @@
|
|||
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.Parcel;
|
||||
import android.os.RemoteException;
|
||||
import android.text.TextUtils;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
|
@ -16,7 +11,6 @@ import java.util.Locale;
|
|||
import fr.free.nrw.commons.BuildConfig;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.Utils;
|
||||
import fr.free.nrw.commons.settings.Prefs;
|
||||
|
||||
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_EXTERNAL = "external";
|
||||
|
||||
private ContentProviderClient client;
|
||||
private Uri contentUri;
|
||||
private String source;
|
||||
private String editSummary;
|
||||
|
|
@ -51,24 +44,42 @@ public class Contribution extends Media {
|
|||
private int state;
|
||||
private long transferred;
|
||||
private String decimalCoords;
|
||||
|
||||
private boolean isMultiple;
|
||||
|
||||
public boolean getMultiple() {
|
||||
return isMultiple;
|
||||
public Contribution(Uri contentUri, String filename, Uri localUri, String imageUrl, Date timestamp,
|
||||
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) {
|
||||
isMultiple = multiple;
|
||||
}
|
||||
|
||||
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);
|
||||
public Contribution(Uri localUri, String imageUrl, String filename, String description, long dataLength,
|
||||
Date dateCreated, Date dateUploaded, String creator, String editSummary, String decimalCoords) {
|
||||
super(localUri, imageUrl, filename, description, dataLength, dateCreated, dateUploaded, creator);
|
||||
this.decimalCoords = decimalCoords;
|
||||
this.editSummary = editSummary;
|
||||
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
|
||||
public void writeToParcel(Parcel parcel, int flags) {
|
||||
super.writeToParcel(parcel, flags);
|
||||
|
|
@ -80,14 +91,12 @@ public class Contribution extends Media {
|
|||
parcel.writeInt(isMultiple ? 1 : 0);
|
||||
}
|
||||
|
||||
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;
|
||||
public boolean getMultiple() {
|
||||
return isMultiple;
|
||||
}
|
||||
|
||||
public void setMultiple(boolean multiple) {
|
||||
isMultiple = multiple;
|
||||
}
|
||||
|
||||
public long getTransferred() {
|
||||
|
|
@ -106,10 +115,18 @@ public class Contribution extends Media {
|
|||
return contentUri;
|
||||
}
|
||||
|
||||
public void setContentUri(Uri contentUri) {
|
||||
this.contentUri = contentUri;
|
||||
}
|
||||
|
||||
public Date getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(Date timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public int getState() {
|
||||
return state;
|
||||
}
|
||||
|
|
@ -149,68 +166,12 @@ public class Contribution extends Media {
|
|||
}
|
||||
|
||||
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(getTrackingTemplates());
|
||||
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
|
||||
public void setFilename(String filename) {
|
||||
this.filename = filename;
|
||||
|
|
@ -224,33 +185,6 @@ public class Contribution extends Media {
|
|||
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() {
|
||||
return source;
|
||||
}
|
||||
|
|
@ -267,118 +201,25 @@ public class Contribution extends Media {
|
|||
this.decimalCoords = decimalCoords;
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
|
||||
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);
|
||||
@NonNull
|
||||
private String licenseTemplateFor(String license) {
|
||||
switch (license) {
|
||||
case Prefs.Licenses.CC_BY_3:
|
||||
return "{{self|cc-by-3.0}}";
|
||||
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}}";
|
||||
}
|
||||
|
||||
public static void onDelete(SQLiteDatabase db) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
throw new RuntimeException("Unrecognized license value: " + license);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,10 +93,15 @@ class ContributionController {
|
|||
shareIntent.putExtra(EXTRA_SOURCE, SOURCE_GALLERY);
|
||||
break;
|
||||
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_SOURCE, SOURCE_CAMERA);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
Timber.i("Image selected");
|
||||
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.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.content.CursorLoader;
|
||||
|
|
@ -23,13 +22,17 @@ import android.widget.AdapterView;
|
|||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import butterknife.ButterKnife;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.HandlerService;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
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.mwapi.MediaWikiApi;
|
||||
import fr.free.nrw.commons.settings.Prefs;
|
||||
import fr.free.nrw.commons.upload.UploadService;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
|
|
@ -39,15 +42,22 @@ import timber.log.Timber;
|
|||
|
||||
import static android.content.ContentResolver.requestSync;
|
||||
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.ContributionsContentProvider.AUTHORITY;
|
||||
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.settings.Prefs.UPLOADS_SHOWING;
|
||||
|
||||
public class ContributionsActivity extends AuthenticatedActivity
|
||||
implements LoaderManager.LoaderCallbacks<Cursor>, AdapterView.OnItemClickListener,
|
||||
MediaDetailPagerFragment.MediaDetailProvider, FragmentManager.OnBackStackChangedListener,
|
||||
ContributionsListFragment.SourceRefresher {
|
||||
public class ContributionsActivity
|
||||
extends AuthenticatedActivity
|
||||
implements LoaderManager.LoaderCallbacks<Cursor>,
|
||||
AdapterView.OnItemClickListener,
|
||||
MediaDetailPagerFragment.MediaDetailProvider,
|
||||
FragmentManager.OnBackStackChangedListener,
|
||||
ContributionsListFragment.SourceRefresher {
|
||||
|
||||
@Inject MediaWikiApi mediaWikiApi;
|
||||
@Inject SessionManager sessionManager;
|
||||
@Inject @Named("default_preferences") SharedPreferences prefs;
|
||||
@Inject ContributionDao contributionDao;
|
||||
|
||||
private Cursor allContributions;
|
||||
private ContributionsListFragment contributionsList;
|
||||
|
|
@ -55,21 +65,6 @@ public class ContributionsActivity extends AuthenticatedActivity
|
|||
private UploadService uploadService;
|
||||
private boolean isUploadServiceConnected;
|
||||
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();
|
||||
|
||||
|
|
@ -84,7 +79,7 @@ public class ContributionsActivity extends AuthenticatedActivity
|
|||
@Override
|
||||
public void onServiceDisconnected(ComponentName componentName) {
|
||||
// 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
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
boolean isSettingsChanged =
|
||||
sharedPreferences.getBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false);
|
||||
SharedPreferences.Editor editor = sharedPreferences.edit();
|
||||
editor.putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false);
|
||||
editor.apply();
|
||||
boolean isSettingsChanged = prefs.getBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false);
|
||||
prefs.edit().putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false).apply();
|
||||
if (isSettingsChanged) {
|
||||
refreshSource();
|
||||
}
|
||||
|
|
@ -114,16 +105,14 @@ public class ContributionsActivity extends AuthenticatedActivity
|
|||
|
||||
@Override
|
||||
protected void onAuthCookieAcquired(String authCookie) {
|
||||
// Do a sync every time we get here!
|
||||
CommonsApplication app = ((CommonsApplication) getApplication());
|
||||
requestSync(app.getCurrentAccount(), AUTHORITY, new Bundle());
|
||||
// Do a sync everytime we get here!
|
||||
requestSync(sessionManager.getCurrentAccount(), ContributionsContentProvider.CONTRIBUTION_AUTHORITY, new Bundle());
|
||||
Intent uploadServiceIntent = new Intent(this, UploadService.class);
|
||||
uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE);
|
||||
startService(uploadServiceIntent);
|
||||
bindService(uploadServiceIntent, uploadServiceConnection, Context.BIND_AUTO_CREATE);
|
||||
|
||||
allContributions = getContentResolver().query(BASE_URI, ALL_FIELDS,
|
||||
CONTRIBUTION_SELECTION, null, CONTRIBUTION_SORT);
|
||||
allContributions = contributionDao.loadAllContributions();
|
||||
|
||||
getSupportLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
|
|
@ -137,17 +126,20 @@ public class ContributionsActivity extends AuthenticatedActivity
|
|||
// Activity can call methods in the fragment by acquiring a
|
||||
// reference to the Fragment from FragmentManager, using findFragmentById()
|
||||
FragmentManager supportFragmentManager = getSupportFragmentManager();
|
||||
contributionsList = (ContributionsListFragment) supportFragmentManager
|
||||
contributionsList = (ContributionsListFragment)supportFragmentManager
|
||||
.findFragmentById(R.id.contributionsListFragment);
|
||||
|
||||
supportFragmentManager.addOnBackStackChangedListener(this);
|
||||
if (savedInstanceState != null) {
|
||||
mediaDetails = (MediaDetailPagerFragment) supportFragmentManager
|
||||
mediaDetails = (MediaDetailPagerFragment)supportFragmentManager
|
||||
.findFragmentById(R.id.contributionsFragmentContainer);
|
||||
|
||||
getSupportLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
requestAuthToken();
|
||||
initDrawer();
|
||||
setTitle(getString(R.string.title_activity_contributions));
|
||||
setUploadCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -178,24 +170,23 @@ public class ContributionsActivity extends AuthenticatedActivity
|
|||
|
||||
public void retryUpload(int i) {
|
||||
allContributions.moveToPosition(i);
|
||||
Contribution c = Contribution.fromCursor(allContributions);
|
||||
Contribution c = contributionDao.fromCursor(allContributions);
|
||||
if (c.getState() == STATE_FAILED) {
|
||||
uploadService.queue(UploadService.ACTION_UPLOAD_FILE, c);
|
||||
Timber.d("Restarting for %s", c.toContentValues());
|
||||
Timber.d("Restarting for %s", c.toString());
|
||||
} 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) {
|
||||
allContributions.moveToPosition(i);
|
||||
Contribution c = Contribution.fromCursor(allContributions);
|
||||
Contribution c = contributionDao.fromCursor(allContributions);
|
||||
if (c.getState() == STATE_FAILED) {
|
||||
Timber.d("Deleting failed contrib %s", c.toContentValues());
|
||||
c.setContentProviderClient(getContentResolver().acquireContentProviderClient(AUTHORITY));
|
||||
c.delete();
|
||||
Timber.d("Deleting failed contrib %s", c.toString());
|
||||
contributionDao.delete(c);
|
||||
} 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
|
||||
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
|
||||
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
int uploads = sharedPref.getInt(UPLOADS_SHOWING, 100);
|
||||
int uploads = prefs.getInt(UPLOADS_SHOWING, 100);
|
||||
return new CursorLoader(this, BASE_URI,
|
||||
ALL_FIELDS, CONTRIBUTION_SELECTION, null,
|
||||
CONTRIBUTION_SORT + "LIMIT " + uploads);
|
||||
ALL_FIELDS, "", null,
|
||||
ContributionDao.CONTRIBUTION_SORT + "LIMIT " + uploads);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
|
||||
contributionsList.changeProgressBarVisibility(false);
|
||||
|
||||
if (contributionsList.getAdapter() == null) {
|
||||
contributionsList.setAdapter(new ContributionsListAdapter(getApplicationContext(),
|
||||
cursor, 0));
|
||||
cursor, 0, contributionDao));
|
||||
} else {
|
||||
((CursorAdapter) contributionsList.getAdapter()).swapCursor(cursor);
|
||||
}
|
||||
|
||||
setUploadCount();
|
||||
|
||||
contributionsList.clearSyncMessage();
|
||||
notifyAndMigrateDataSetObservers();
|
||||
}
|
||||
|
|
@ -263,7 +253,7 @@ public class ContributionsActivity extends AuthenticatedActivity
|
|||
// not yet ready to return data
|
||||
return null;
|
||||
} else {
|
||||
return Contribution.fromCursor((Cursor) contributionsList.getAdapter().getItem(i));
|
||||
return contributionDao.fromCursor((Cursor) contributionsList.getAdapter().getItem(i));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -277,19 +267,16 @@ public class ContributionsActivity extends AuthenticatedActivity
|
|||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
private void setUploadCount() {
|
||||
CommonsApplication app = ((CommonsApplication) getApplication());
|
||||
compositeDisposable.add(
|
||||
app.getMWApi()
|
||||
.getUploadCount(app.getCurrentAccount().name)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
uploadCount -> getSupportActionBar().setSubtitle(getResources()
|
||||
.getQuantityString(R.plurals.contributions_subtitle,
|
||||
uploadCount, uploadCount)),
|
||||
t -> Timber.e(t, "Fetching upload count failed")
|
||||
)
|
||||
);
|
||||
compositeDisposable.add(mediaWikiApi
|
||||
.getUploadCount(sessionManager.getCurrentAccount().name)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
uploadCount -> getSupportActionBar().setSubtitle(getResources()
|
||||
.getQuantityString(R.plurals.contributions_subtitle,
|
||||
uploadCount, uploadCount)),
|
||||
t -> Timber.e(t, "Fetching upload count failed")
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -341,9 +328,4 @@ public class ContributionsActivity extends AuthenticatedActivity
|
|||
public void refreshSource() {
|
||||
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;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
|
|
@ -10,36 +9,36 @@ import android.net.Uri;
|
|||
import android.support.annotation.NonNull;
|
||||
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 static android.content.UriMatcher.NO_MATCH;
|
||||
import static fr.free.nrw.commons.contributions.Contribution.Table.ALL_FIELDS;
|
||||
import static fr.free.nrw.commons.contributions.Contribution.Table.TABLE_NAME;
|
||||
import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS;
|
||||
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_ID = 2;
|
||||
private static final String BASE_PATH = "contributions";
|
||||
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 {
|
||||
uriMatcher.addURI(AUTHORITY, BASE_PATH, CONTRIBUTIONS);
|
||||
uriMatcher.addURI(AUTHORITY, BASE_PATH + "/#", CONTRIBUTIONS_ID);
|
||||
uriMatcher.addURI(CONTRIBUTION_AUTHORITY, BASE_PATH, CONTRIBUTIONS);
|
||||
uriMatcher.addURI(CONTRIBUTION_AUTHORITY, BASE_PATH + "/#", CONTRIBUTIONS_ID);
|
||||
}
|
||||
|
||||
public static Uri uriForId(int id) {
|
||||
return Uri.parse(BASE_URI.toString() + "/" + id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
return false;
|
||||
}
|
||||
@Inject DBOpenHelper dbOpenHelper;
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Override
|
||||
|
|
@ -50,8 +49,7 @@ public class ContributionsContentProvider extends ContentProvider {
|
|||
|
||||
int uriType = uriMatcher.match(uri);
|
||||
|
||||
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext();
|
||||
SQLiteDatabase db = app.getDBOpenHelper().getReadableDatabase();
|
||||
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
|
||||
Cursor cursor;
|
||||
|
||||
switch (uriType) {
|
||||
|
|
@ -87,8 +85,7 @@ public class ContributionsContentProvider extends ContentProvider {
|
|||
@Override
|
||||
public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
|
||||
int uriType = uriMatcher.match(uri);
|
||||
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext();
|
||||
SQLiteDatabase sqlDB = app.getDBOpenHelper().getWritableDatabase();
|
||||
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||
long id;
|
||||
switch (uriType) {
|
||||
case CONTRIBUTIONS:
|
||||
|
|
@ -107,13 +104,12 @@ public class ContributionsContentProvider extends ContentProvider {
|
|||
int rows;
|
||||
int uriType = uriMatcher.match(uri);
|
||||
|
||||
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext();
|
||||
SQLiteDatabase sqlDB = app.getDBOpenHelper().getWritableDatabase();
|
||||
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
|
||||
|
||||
switch (uriType) {
|
||||
case CONTRIBUTIONS_ID:
|
||||
Timber.d("Deleting contribution id %s", uri.getLastPathSegment());
|
||||
rows = sqlDB.delete(TABLE_NAME,
|
||||
rows = db.delete(TABLE_NAME,
|
||||
"_id = ?",
|
||||
new String[]{uri.getLastPathSegment()}
|
||||
);
|
||||
|
|
@ -130,8 +126,7 @@ public class ContributionsContentProvider extends ContentProvider {
|
|||
public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
|
||||
Timber.d("Hello, bulk insert! (ContributionsContentProvider)");
|
||||
int uriType = uriMatcher.match(uri);
|
||||
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext();
|
||||
SQLiteDatabase sqlDB = app.getDBOpenHelper().getWritableDatabase();
|
||||
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||
sqlDB.beginTransaction();
|
||||
switch (uriType) {
|
||||
case CONTRIBUTIONS:
|
||||
|
|
@ -162,8 +157,7 @@ public class ContributionsContentProvider extends ContentProvider {
|
|||
error out otherwise.
|
||||
*/
|
||||
int uriType = uriMatcher.match(uri);
|
||||
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext();
|
||||
SQLiteDatabase sqlDB = app.getDBOpenHelper().getWritableDatabase();
|
||||
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||
int rowsUpdated;
|
||||
switch (uriType) {
|
||||
case CONTRIBUTIONS:
|
||||
|
|
@ -175,7 +169,7 @@ public class ContributionsContentProvider extends ContentProvider {
|
|||
if (TextUtils.isEmpty(selection)) {
|
||||
rowsUpdated = sqlDB.update(TABLE_NAME,
|
||||
contentValues,
|
||||
Contribution.Table.COLUMN_ID + " = ?",
|
||||
ContributionDao.Table.COLUMN_ID + " = ?",
|
||||
new String[]{String.valueOf(id)});
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
|
|
|
|||
|
|
@ -11,8 +11,11 @@ import fr.free.nrw.commons.R;
|
|||
|
||||
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);
|
||||
this.contributionDao = contributionDao;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -26,7 +29,7 @@ class ContributionsListAdapter extends CursorAdapter {
|
|||
@Override
|
||||
public void bindView(View view, Context context, Cursor cursor) {
|
||||
final ContributionViewHolder views = (ContributionViewHolder)view.getTag();
|
||||
final Contribution contribution = Contribution.fromCursor(cursor);
|
||||
final Contribution contribution = contributionDao.fromCursor(cursor);
|
||||
|
||||
views.imageView.setMedia(contribution);
|
||||
views.titleView.setText(contribution.getDisplayTitle());
|
||||
|
|
@ -34,7 +37,7 @@ class ContributionsListAdapter extends CursorAdapter {
|
|||
views.seqNumView.setText(String.valueOf(cursor.getPosition() + 1));
|
||||
views.seqNumView.setVisibility(View.VISIBLE);
|
||||
|
||||
switch(contribution.getState()) {
|
||||
switch (contribution.getState()) {
|
||||
case Contribution.STATE_COMPLETED:
|
||||
views.stateView.setVisibility(View.GONE);
|
||||
views.progressView.setVisibility(View.GONE);
|
||||
|
|
@ -50,7 +53,7 @@ class ContributionsListAdapter extends CursorAdapter {
|
|||
views.progressView.setVisibility(View.VISIBLE);
|
||||
long total = contribution.getDataLength();
|
||||
long transferred = contribution.getTransferred();
|
||||
if(transferred == 0 || transferred >= total) {
|
||||
if (transferred == 0 || transferred >= total) {
|
||||
views.progressView.setIndeterminate(true);
|
||||
} else {
|
||||
views.progressView.setProgress((int)(((double)transferred / (double)total) * 100));
|
||||
|
|
|
|||
|
|
@ -5,9 +5,7 @@ import android.content.SharedPreferences;
|
|||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.view.LayoutInflater;
|
||||
|
|
@ -19,30 +17,43 @@ import android.view.ViewGroup;
|
|||
import android.widget.AdapterView;
|
||||
import android.widget.GridView;
|
||||
import android.widget.ListAdapter;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||
import fr.free.nrw.commons.nearby.NearbyActivity;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
|
||||
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
|
||||
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.view.View.GONE;
|
||||
|
||||
public class ContributionsListFragment extends Fragment {
|
||||
public class ContributionsListFragment extends CommonsDaggerSupportFragment {
|
||||
|
||||
@BindView(R.id.contributionsList)
|
||||
GridView contributionsList;
|
||||
@BindView(R.id.waitingMessage)
|
||||
TextView waitingMessage;
|
||||
@BindView(R.id.emptyMessage)
|
||||
TextView emptyMessage;
|
||||
@BindView(R.id.loadingContributionsProgressBar)
|
||||
ProgressBar progressBar;
|
||||
|
||||
@Inject
|
||||
@Named("prefs")
|
||||
SharedPreferences prefs;
|
||||
@Inject
|
||||
@Named("default_preferences")
|
||||
SharedPreferences defaultPrefs;
|
||||
|
||||
private ContributionController controller;
|
||||
|
||||
@Override
|
||||
|
|
@ -57,7 +68,6 @@ public class ContributionsListFragment extends Fragment {
|
|||
}
|
||||
|
||||
//TODO: Should this be in onResume?
|
||||
SharedPreferences prefs = getActivity().getSharedPreferences("prefs", MODE_PRIVATE);
|
||||
String lastModified = prefs.getString("lastSyncTimestamp", "");
|
||||
Timber.d("Last Sync Timestamp: %s", lastModified);
|
||||
|
||||
|
|
@ -67,6 +77,7 @@ public class ContributionsListFragment extends Fragment {
|
|||
waitingMessage.setVisibility(GONE);
|
||||
}
|
||||
|
||||
changeProgressBarVisibility(true);
|
||||
return v;
|
||||
}
|
||||
|
||||
|
|
@ -78,6 +89,10 @@ public class ContributionsListFragment extends Fragment {
|
|||
this.contributionsList.setAdapter(adapter);
|
||||
}
|
||||
|
||||
public void changeProgressBarVisibility(boolean isVisible) {
|
||||
this.progressBar.setVisibility(isVisible ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
if (outState == null) {
|
||||
|
|
@ -155,9 +170,7 @@ public class ContributionsListFragment extends Fragment {
|
|||
|
||||
return true;
|
||||
case R.id.menu_from_camera:
|
||||
SharedPreferences sharedPref = PreferenceManager
|
||||
.getDefaultSharedPreferences(CommonsApplication.getInstance());
|
||||
boolean useExtStorage = sharedPref.getBoolean("useExternalStorage", true);
|
||||
boolean useExtStorage = defaultPrefs.getBoolean("useExternalStorage", true);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && useExtStorage) {
|
||||
// Here, thisActivity is the current activity
|
||||
if (ContextCompat.checkSelfPermission(getActivity(), WRITE_EXTERNAL_STORAGE)
|
||||
|
|
@ -201,7 +214,7 @@ public class ContributionsListFragment extends Fragment {
|
|||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
|
||||
@NonNull int[] grantResults) {
|
||||
Timber.d("onRequestPermissionsResult: req code = " + " perm = "
|
||||
+ permissions + " grant =" + grantResults);
|
||||
+ Arrays.toString(permissions) + " grant =" + Arrays.toString(grantResults));
|
||||
|
||||
switch (requestCode) {
|
||||
// 1 = Storage allowed when gallery selected
|
||||
|
|
@ -235,12 +248,17 @@ public class ContributionsListFragment extends Fragment {
|
|||
menu.clear(); // See http://stackoverflow.com/a/8495697/17865
|
||||
inflater.inflate(R.menu.fragment_contributions_list, menu);
|
||||
|
||||
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext();
|
||||
if (!app.deviceHasCamera()) {
|
||||
if (!deviceHasCamera()) {
|
||||
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
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
|
|
|||
|
|
@ -13,21 +13,27 @@ import android.os.RemoteException;
|
|||
import android.text.TextUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
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.di.ApplicationlessInjection;
|
||||
import fr.free.nrw.commons.mwapi.LogEventResult;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
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.Table.COLUMN_FILENAME;
|
||||
import static fr.free.nrw.commons.contributions.ContributionDao.Table.COLUMN_FILENAME;
|
||||
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||
|
||||
private static final String[] existsQuery = {COLUMN_FILENAME};
|
||||
|
|
@ -35,6 +41,10 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
|
|||
private static final ContentValues[] EMPTY = {};
|
||||
private static int COMMIT_THRESHOLD = 10;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@Inject MediaWikiApi mwApi;
|
||||
@Inject @Named("prefs") SharedPreferences prefs;
|
||||
|
||||
public ContributionsSyncAdapter(Context context, boolean autoInitialize) {
|
||||
super(context, autoInitialize);
|
||||
}
|
||||
|
|
@ -47,6 +57,9 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
|
|||
}
|
||||
|
||||
private boolean fileExists(ContentProviderClient client, String filename) {
|
||||
if (filename == null) {
|
||||
return false;
|
||||
}
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = client.query(BASE_URI,
|
||||
|
|
@ -68,19 +81,23 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
|
|||
@Override
|
||||
public void onPerformSync(Account account, Bundle bundle, String authority,
|
||||
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!
|
||||
String user = account.name;
|
||||
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
|
||||
SharedPreferences prefs = getContext().getSharedPreferences("prefs", MODE_PRIVATE);
|
||||
String lastModified = prefs.getString("lastSyncTimestamp", "");
|
||||
Date curTime = new Date();
|
||||
LogEventResult result;
|
||||
Boolean done = false;
|
||||
String queryContinue = null;
|
||||
ContributionDao contributionDao = new ContributionDao(() -> contentProviderClient);
|
||||
while (!done) {
|
||||
|
||||
try {
|
||||
result = api.logEvents(user, lastModified, queryContinue, getLimit());
|
||||
result = mwApi.logEvents(user, lastModified, queryContinue, getLimit());
|
||||
} catch (IOException e) {
|
||||
// There isn't really much we can do, eh?
|
||||
// FIXME: Perhaps add EventLogging?
|
||||
|
|
@ -109,7 +126,7 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
|
|||
"", -1, dateUpdated, dateUpdated, user,
|
||||
"", "");
|
||||
contrib.setState(STATE_COMPLETED);
|
||||
imageValues.add(contrib.toContentValues());
|
||||
imageValues.add(contributionDao.toContentValues(contrib));
|
||||
|
||||
if (imageValues.size() % COMMIT_THRESHOLD == 0) {
|
||||
try {
|
||||
|
|
@ -134,8 +151,13 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
|
|||
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!");
|
||||
}
|
||||
|
||||
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.SQLiteOpenHelper;
|
||||
|
||||
import fr.free.nrw.commons.contributions.Contribution;
|
||||
import fr.free.nrw.commons.modifications.ModifierSequence;
|
||||
import fr.free.nrw.commons.category.CategoryDao;
|
||||
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 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) {
|
||||
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
||||
|
|
@ -21,15 +23,15 @@ public class DBOpenHelper extends SQLiteOpenHelper{
|
|||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase sqLiteDatabase) {
|
||||
Contribution.Table.onCreate(sqLiteDatabase);
|
||||
ModifierSequence.Table.onCreate(sqLiteDatabase);
|
||||
Category.Table.onCreate(sqLiteDatabase);
|
||||
ContributionDao.Table.onCreate(sqLiteDatabase);
|
||||
ModifierSequenceDao.Table.onCreate(sqLiteDatabase);
|
||||
CategoryDao.Table.onCreate(sqLiteDatabase);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int from, int to) {
|
||||
Contribution.Table.onUpdate(sqLiteDatabase, from, to);
|
||||
ModifierSequence.Table.onUpdate(sqLiteDatabase, from, to);
|
||||
Category.Table.onUpdate(sqLiteDatabase, from, to);
|
||||
ContributionDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||
ModifierSequenceDao.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,125 +1,156 @@
|
|||
package fr.free.nrw.commons.location;
|
||||
|
||||
public class LatLng {
|
||||
|
||||
private final double latitude;
|
||||
private final double longitude;
|
||||
private final float accuracy;
|
||||
|
||||
/** Accepts latitude and longitude.
|
||||
* North and South values are cut off at 90°
|
||||
*
|
||||
* @param latitude double value
|
||||
* @param longitude double value
|
||||
*/
|
||||
public LatLng(double latitude, double longitude, float accuracy) {
|
||||
if(-180.0D <= longitude && longitude < 180.0D) {
|
||||
this.longitude = longitude;
|
||||
} else {
|
||||
this.longitude = ((longitude - 180.0D) % 360.0D + 360.0D) % 360.0D - 180.0D;
|
||||
}
|
||||
this.latitude = Math.max(-90.0D, Math.min(90.0D, latitude));
|
||||
this.accuracy = accuracy;
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
boolean var1 = true;
|
||||
byte var2 = 1;
|
||||
long var3 = Double.doubleToLongBits(this.latitude);
|
||||
int var5 = 31 * var2 + (int)(var3 ^ var3 >>> 32);
|
||||
var3 = Double.doubleToLongBits(this.longitude);
|
||||
var5 = 31 * var5 + (int)(var3 ^ var3 >>> 32);
|
||||
return var5;
|
||||
}
|
||||
|
||||
public boolean equals(Object o) {
|
||||
if(this == o) {
|
||||
return true;
|
||||
} else if(!(o instanceof LatLng)) {
|
||||
return false;
|
||||
} else {
|
||||
LatLng var2 = (LatLng)o;
|
||||
return Double.doubleToLongBits(this.latitude) == Double.doubleToLongBits(var2.latitude) && Double.doubleToLongBits(this.longitude) == Double.doubleToLongBits(var2.longitude);
|
||||
}
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "lat/lng: (" + this.latitude + "," + this.longitude + ")";
|
||||
}
|
||||
|
||||
/**
|
||||
* Rounds the float to 4 digits and returns absolute value.
|
||||
*
|
||||
* @param coordinate A coordinate value as string.
|
||||
* @return String of the rounded number.
|
||||
*/
|
||||
private String formatCoordinate(double coordinate) {
|
||||
double roundedNumber = Math.round(coordinate * 10000d) / 10000d;
|
||||
double absoluteNumber = Math.abs(roundedNumber);
|
||||
return String.valueOf(absoluteNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns "N" or "S" depending on the latitude.
|
||||
*
|
||||
* @return "N" or "S".
|
||||
*/
|
||||
private String getNorthSouth() {
|
||||
if (this.latitude < 0) {
|
||||
return "S";
|
||||
}
|
||||
|
||||
return "N";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns "E" or "W" depending on the longitude.
|
||||
*
|
||||
* @return "E" or "W".
|
||||
*/
|
||||
private String getEastWest() {
|
||||
if (this.longitude >= 0 && this.longitude < 180) {
|
||||
return "E";
|
||||
}
|
||||
|
||||
return "W";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a nicely formatted coordinate string. Used e.g. in
|
||||
* the detail view.
|
||||
*
|
||||
* @return The formatted string.
|
||||
*/
|
||||
public String getPrettyCoordinateString() {
|
||||
return formatCoordinate(this.latitude) + " " + this.getNorthSouth() + ", "
|
||||
+ formatCoordinate(this.longitude) + " " + this.getEastWest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the location accuracy in meter.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public float getAccuracy() {
|
||||
return accuracy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the longitude in degrees.
|
||||
*
|
||||
* @return double
|
||||
*/
|
||||
public double getLongitude() {
|
||||
return longitude;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the latitude in degrees.
|
||||
*
|
||||
* @return double
|
||||
*/
|
||||
public double getLatitude() {
|
||||
return latitude;
|
||||
}
|
||||
}
|
||||
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 {
|
||||
|
||||
private final double latitude;
|
||||
private final double longitude;
|
||||
private final float accuracy;
|
||||
|
||||
/**
|
||||
* Accepts latitude and longitude.
|
||||
* North and South values are cut off at 90°
|
||||
*
|
||||
* @param latitude the latitude
|
||||
* @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) {
|
||||
if (-180.0D <= longitude && longitude < 180.0D) {
|
||||
this.longitude = longitude;
|
||||
} else {
|
||||
this.longitude = ((longitude - 180.0D) % 360.0D + 360.0D) % 360.0D - 180.0D;
|
||||
}
|
||||
this.latitude = Math.max(-90.0D, Math.min(90.0D, latitude));
|
||||
this.accuracy = accuracy;
|
||||
}
|
||||
|
||||
/**
|
||||
* gets the latitude and longitude of a given non-null location
|
||||
* @param location the non-null location of the user
|
||||
* @return LatLng the Latitude and Longitude of a given location
|
||||
*/
|
||||
public static LatLng from(@NonNull Location location) {
|
||||
return new LatLng(location.getLatitude(), location.getLongitude(), location.getAccuracy());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
} else if (!(o instanceof LatLng)) {
|
||||
return false;
|
||||
} else {
|
||||
LatLng var2 = (LatLng)o;
|
||||
return Double.doubleToLongBits(this.latitude) == Double.doubleToLongBits(var2.latitude) && Double.doubleToLongBits(this.longitude) == Double.doubleToLongBits(var2.longitude);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* returns a string representation of the latitude and longitude
|
||||
*/
|
||||
public String toString() {
|
||||
return "lat/lng: (" + this.latitude + "," + this.longitude + ")";
|
||||
}
|
||||
|
||||
/**
|
||||
* Rounds the float to 4 digits and returns absolute value.
|
||||
*
|
||||
* @param coordinate A coordinate value as string.
|
||||
* @return String of the rounded number.
|
||||
*/
|
||||
private String formatCoordinate(double coordinate) {
|
||||
double roundedNumber = Math.round(coordinate * 10000d) / 10000d;
|
||||
double absoluteNumber = Math.abs(roundedNumber);
|
||||
return String.valueOf(absoluteNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns "N" or "S" depending on the latitude.
|
||||
*
|
||||
* @return "N" or "S".
|
||||
*/
|
||||
private String getNorthSouth() {
|
||||
if (this.latitude < 0) {
|
||||
return "S";
|
||||
}
|
||||
|
||||
return "N";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns "E" or "W" depending on the longitude.
|
||||
*
|
||||
* @return "E" or "W".
|
||||
*/
|
||||
private String getEastWest() {
|
||||
if (this.longitude >= 0 && this.longitude < 180) {
|
||||
return "E";
|
||||
}
|
||||
|
||||
return "W";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a nicely formatted coordinate string. Used e.g. in
|
||||
* the detail view.
|
||||
*
|
||||
* @return The formatted string.
|
||||
*/
|
||||
public String getPrettyCoordinateString() {
|
||||
return formatCoordinate(this.latitude) + " " + this.getNorthSouth() + ", "
|
||||
+ formatCoordinate(this.longitude) + " " + this.getEastWest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the location accuracy in meter.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public float getAccuracy() {
|
||||
return accuracy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the longitude in degrees.
|
||||
*
|
||||
* @return double
|
||||
*/
|
||||
public double getLongitude() {
|
||||
return longitude;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the latitude in degrees.
|
||||
*
|
||||
* @return double
|
||||
*/
|
||||
public double getLatitude() {
|
||||
return latitude;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,62 +1,184 @@
|
|||
package fr.free.nrw.commons.location;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.location.Criteria;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.location.Location;
|
||||
import android.location.LocationListener;
|
||||
import android.location.LocationManager;
|
||||
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;
|
||||
|
||||
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 LatLng latestLocation;
|
||||
private Float latestLocationAccuracy;
|
||||
private Location lastLocation;
|
||||
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) {
|
||||
this.context = context;
|
||||
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
|
||||
* given as a radius in meter of 68 % confidence.
|
||||
* Returns the current status of the GPS provider.
|
||||
*
|
||||
* @return Float
|
||||
* @return true if the GPS provider is enabled
|
||||
*/
|
||||
public Float getLatestLocationAccuracy() {
|
||||
return latestLocationAccuracy;
|
||||
public boolean isProviderEnabled() {
|
||||
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() {
|
||||
if (!isLocationManagerRegistered)
|
||||
isLocationManagerRegistered = requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER)
|
||||
&& requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests location updates from the specified provider.
|
||||
*
|
||||
* @param locationProvider the location provider
|
||||
* @return true if successful
|
||||
*/
|
||||
private boolean requestLocationUpdatesFromProvider(String locationProvider) {
|
||||
try {
|
||||
locationManager.requestLocationUpdates(provider, 400, 1, this);
|
||||
Location location = locationManager.getLastKnownLocation(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);
|
||||
}
|
||||
locationManager.requestLocationUpdates(locationProvider,
|
||||
MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS,
|
||||
MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS,
|
||||
this);
|
||||
return true;
|
||||
} catch (IllegalArgumentException e) {
|
||||
Timber.e(e, "Illegal argument exception");
|
||||
return false;
|
||||
} catch (SecurityException e) {
|
||||
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() {
|
||||
isLocationManagerRegistered = false;
|
||||
try {
|
||||
locationManager.removeUpdates(this);
|
||||
} 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
|
||||
public void onLocationChanged(Location location) {
|
||||
double currentLatitude = location.getLatitude();
|
||||
double currentLongitude = location.getLongitude();
|
||||
latestLocationAccuracy = location.getAccuracy();
|
||||
Timber.d("Latitude: %f Longitude: %f Accuracy %f",
|
||||
currentLatitude, currentLongitude, latestLocationAccuracy);
|
||||
|
||||
latestLocation = new LatLng(currentLatitude, currentLongitude, latestLocationAccuracy);
|
||||
if (isBetterLocation(location, lastLocation)) {
|
||||
lastLocation = location;
|
||||
for (LocationUpdateListener listener : locationListeners) {
|
||||
listener.onLocationChanged(LatLng.from(lastLocation));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.util.TypedValue;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
|
@ -22,6 +21,9 @@ import java.util.ArrayList;
|
|||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
|
||||
import fr.free.nrw.commons.License;
|
||||
import fr.free.nrw.commons.LicenseList;
|
||||
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.PageTitle;
|
||||
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.ui.widget.CompatTextView;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class MediaDetailFragment extends Fragment {
|
||||
public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
||||
|
||||
private boolean editable;
|
||||
private MediaDetailPagerFragment.MediaDetailProvider detailProvider;
|
||||
|
|
@ -53,6 +56,9 @@ public class MediaDetailFragment extends Fragment {
|
|||
return mf;
|
||||
}
|
||||
|
||||
@Inject
|
||||
Provider<MediaDataExtractor> mediaDataExtractorProvider;
|
||||
|
||||
private MediaWikiImageView image;
|
||||
private MediaDetailSpacer spacer;
|
||||
private int initialListTop = 0;
|
||||
|
|
@ -69,8 +75,8 @@ public class MediaDetailFragment extends Fragment {
|
|||
private boolean categoriesPresent = false;
|
||||
private ViewTreeObserver.OnGlobalLayoutListener layoutListener; // for layout stuff, only used once!
|
||||
private ViewTreeObserver.OnScrollChangedListener scrollListener;
|
||||
DataSetObserver dataObserver;
|
||||
private AsyncTask<Void,Void,Boolean> detailFetchTask;
|
||||
private DataSetObserver dataObserver;
|
||||
private AsyncTask<Void, Void, Boolean> detailFetchTask;
|
||||
private LicenseList licenseList;
|
||||
|
||||
@Override
|
||||
|
|
@ -89,7 +95,7 @@ public class MediaDetailFragment extends Fragment {
|
|||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
detailProvider = (MediaDetailPagerFragment.MediaDetailProvider)getActivity();
|
||||
detailProvider = (MediaDetailPagerFragment.MediaDetailProvider) getActivity();
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
editable = savedInstanceState.getBoolean("editable");
|
||||
|
|
@ -150,7 +156,8 @@ public class MediaDetailFragment extends Fragment {
|
|||
return view;
|
||||
}
|
||||
|
||||
@Override public void onResume() {
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
Media media = detailProvider.getMediaAtPosition(index);
|
||||
if (media == null) {
|
||||
|
|
@ -188,13 +195,13 @@ public class MediaDetailFragment extends Fragment {
|
|||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
extractor = new MediaDataExtractor(media.getFilename(), licenseList);
|
||||
extractor = mediaDataExtractorProvider.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... voids) {
|
||||
try {
|
||||
extractor.fetch();
|
||||
extractor.fetch(media.getFilename(), licenseList);
|
||||
return Boolean.TRUE;
|
||||
} catch (IOException e) {
|
||||
Timber.d(e);
|
||||
|
|
@ -232,13 +239,13 @@ public class MediaDetailFragment extends Fragment {
|
|||
detailFetchTask.cancel(true);
|
||||
detailFetchTask = null;
|
||||
}
|
||||
if (layoutListener != null) {
|
||||
if (layoutListener != null && getView() != null) {
|
||||
getView().getViewTreeObserver().removeGlobalOnLayoutListener(layoutListener); // old Android was on crack. CRACK IS WHACK
|
||||
layoutListener = null;
|
||||
}
|
||||
if (scrollListener != null) {
|
||||
if (scrollListener != null && getView() != null) {
|
||||
getView().getViewTreeObserver().removeOnScrollChangedListener(scrollListener);
|
||||
scrollListener = null;
|
||||
scrollListener = null;
|
||||
}
|
||||
if (dataObserver != null) {
|
||||
detailProvider.unregisterDataSetObserver(dataObserver);
|
||||
|
|
@ -283,7 +290,7 @@ public class MediaDetailFragment extends Fragment {
|
|||
|
||||
private View buildCatLabel(final String catName, ViewGroup categoryContainer) {
|
||||
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);
|
||||
if (categoriesLoaded && categoriesPresent) {
|
||||
|
|
@ -302,7 +309,7 @@ public class MediaDetailFragment extends Fragment {
|
|||
// You must face the darkness alone
|
||||
int scrollY = scrollView.getScrollY();
|
||||
int scrollMax = getView().getHeight();
|
||||
float scrollPercentage = (float)scrollY / (float)scrollMax;
|
||||
float scrollPercentage = (float) scrollY / (float) scrollMax;
|
||||
final float transparencyMax = 0.75f;
|
||||
if (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();
|
||||
if (licenseKey == null || licenseKey.equals("")) {
|
||||
return null;
|
||||
|
|
@ -377,7 +385,7 @@ public class MediaDetailFragment extends Fragment {
|
|||
private void openMap(LatLng coordinates) {
|
||||
//Open map app at given position
|
||||
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);
|
||||
|
||||
if (mapIntent.resolveActivity(getActivity().getPackageManager()) != null) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package fr.free.nrw.commons.media;
|
|||
import android.annotation.SuppressLint;
|
||||
import android.app.DownloadManager;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.DataSetObserver;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
|
|
@ -24,20 +25,31 @@ import android.view.MenuItem;
|
|||
import android.view.View;
|
||||
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.R;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.contributions.Contribution;
|
||||
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.content.Context.DOWNLOAD_SERVICE;
|
||||
import static android.content.Intent.ACTION_VIEW;
|
||||
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 Boolean editable;
|
||||
|
|
@ -99,12 +111,7 @@ public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPa
|
|||
Media m = provider.getMediaAtPosition(pager.getCurrentItem());
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_share_current_image:
|
||||
// Share - this is just logs it, 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();
|
||||
// Share - intent set in onCreateOptionsMenu, around line 252
|
||||
return true;
|
||||
case R.id.menu_browser_current_image:
|
||||
// View in browser
|
||||
|
|
@ -141,8 +148,14 @@ public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPa
|
|||
private void downloadMedia(Media m) {
|
||||
String imageUrl = m.getImageUrl(),
|
||||
fileName = m.getFilename();
|
||||
|
||||
if (imageUrl == null || fileName == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Strip 'File:' from beginning of filename, we really shouldn't store it
|
||||
fileName = fileName.replaceFirst("^File:", "");
|
||||
|
||||
Uri imageUri = Uri.parse(imageUrl);
|
||||
|
||||
DownloadManager.Request req = new DownloadManager.Request(imageUri);
|
||||
|
|
@ -155,15 +168,19 @@ public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPa
|
|||
req.allowScanningByMediaScanner();
|
||||
req.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
&& !(ContextCompat.checkSelfPermission(getContext(),
|
||||
READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED)) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
|
||||
ContextCompat.checkSelfPermission(getContext(), READ_EXTERNAL_STORAGE)
|
||||
!= PERMISSION_GRANTED
|
||||
&& getView() != null) {
|
||||
Snackbar.make(getView(), R.string.read_storage_permission_rationale,
|
||||
Snackbar.LENGTH_INDEFINITE).setAction(R.string.ok,
|
||||
view -> ActivityCompat.requestPermissions(getActivity(),
|
||||
new String[]{READ_EXTERNAL_STORAGE}, 1)).show();
|
||||
} 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) {
|
||||
super(MODIFIER_NAME);
|
||||
JSONArray categoriesArray = new JSONArray();
|
||||
for(String category: categories) {
|
||||
for (String category: categories) {
|
||||
categoriesArray.put(category);
|
||||
}
|
||||
try {
|
||||
|
|
@ -34,7 +34,7 @@ public class CategoryModifier extends PageModifier {
|
|||
categories = params.optJSONArray(PARAM_CATEGORIES);
|
||||
|
||||
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);
|
||||
categoriesString.append("\n[[Category:").append(category).append("]]");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package fr.free.nrw.commons.modifications;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
|
|
@ -10,50 +9,51 @@ import android.net.Uri;
|
|||
import android.support.annotation.NonNull;
|
||||
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;
|
||||
|
||||
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_ID = 2;
|
||||
|
||||
public static final String AUTHORITY = "fr.free.nrw.commons.modifications.contentprovider";
|
||||
private static final String BASE_PATH = "modifications";
|
||||
public static final String MODIFICATIONS_AUTHORITY = "fr.free.nrw.commons.modifications.contentprovider";
|
||||
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);
|
||||
static {
|
||||
uriMatcher.addURI(AUTHORITY, BASE_PATH, MODIFICATIONS);
|
||||
uriMatcher.addURI(AUTHORITY, BASE_PATH + "/#", MODIFICATIONS_ID);
|
||||
uriMatcher.addURI(MODIFICATIONS_AUTHORITY, BASE_PATH, MODIFICATIONS);
|
||||
uriMatcher.addURI(MODIFICATIONS_AUTHORITY, BASE_PATH + "/#", MODIFICATIONS_ID);
|
||||
}
|
||||
|
||||
public static Uri uriForId(int id) {
|
||||
return Uri.parse(BASE_URI.toString() + "/" + id);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
return false;
|
||||
}
|
||||
@Inject DBOpenHelper dbOpenHelper;
|
||||
|
||||
@Override
|
||||
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
||||
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
|
||||
queryBuilder.setTables(ModifierSequence.Table.TABLE_NAME);
|
||||
queryBuilder.setTables(TABLE_NAME);
|
||||
|
||||
int uriType = uriMatcher.match(uri);
|
||||
|
||||
switch(uriType) {
|
||||
switch (uriType) {
|
||||
case MODIFICATIONS:
|
||||
break;
|
||||
default:
|
||||
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.setNotificationUri(getContext().getContentResolver(), uri);
|
||||
|
|
@ -69,11 +69,11 @@ public class ModificationsContentProvider extends ContentProvider{
|
|||
@Override
|
||||
public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
|
||||
int uriType = uriMatcher.match(uri);
|
||||
SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase();
|
||||
long id = 0;
|
||||
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||
long id;
|
||||
switch (uriType) {
|
||||
case MODIFICATIONS:
|
||||
id = sqlDB.insert(ModifierSequence.Table.TABLE_NAME, null, contentValues);
|
||||
id = sqlDB.insert(TABLE_NAME, null, contentValues);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URI: " + uri);
|
||||
|
|
@ -85,11 +85,11 @@ public class ModificationsContentProvider extends ContentProvider{
|
|||
@Override
|
||||
public int delete(@NonNull Uri uri, String s, String[] strings) {
|
||||
int uriType = uriMatcher.match(uri);
|
||||
SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase();
|
||||
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||
switch (uriType) {
|
||||
case MODIFICATIONS_ID:
|
||||
String id = uri.getLastPathSegment();
|
||||
sqlDB.delete(ModifierSequence.Table.TABLE_NAME,
|
||||
sqlDB.delete(TABLE_NAME,
|
||||
"_id = ?",
|
||||
new String[] { id }
|
||||
);
|
||||
|
|
@ -103,13 +103,13 @@ public class ModificationsContentProvider extends ContentProvider{
|
|||
public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
|
||||
Timber.d("Hello, bulk insert! (ModificationsContentProvider)");
|
||||
int uriType = uriMatcher.match(uri);
|
||||
SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase();
|
||||
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||
sqlDB.beginTransaction();
|
||||
switch (uriType) {
|
||||
case MODIFICATIONS:
|
||||
for(ContentValues value: values) {
|
||||
for (ContentValues value: values) {
|
||||
Timber.d("Inserting! %s", value);
|
||||
sqlDB.insert(ModifierSequence.Table.TABLE_NAME, null, value);
|
||||
sqlDB.insert(TABLE_NAME, null, value);
|
||||
}
|
||||
break;
|
||||
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.
|
||||
*/
|
||||
int uriType = uriMatcher.match(uri);
|
||||
SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase();
|
||||
int rowsUpdated = 0;
|
||||
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||
int rowsUpdated;
|
||||
switch (uriType) {
|
||||
case MODIFICATIONS:
|
||||
rowsUpdated = sqlDB.update(ModifierSequence.Table.TABLE_NAME,
|
||||
rowsUpdated = sqlDB.update(TABLE_NAME,
|
||||
contentValues,
|
||||
selection,
|
||||
selectionArgs);
|
||||
|
|
@ -144,9 +144,9 @@ public class ModificationsContentProvider extends ContentProvider{
|
|||
int id = Integer.valueOf(uri.getLastPathSegment());
|
||||
|
||||
if (TextUtils.isEmpty(selection)) {
|
||||
rowsUpdated = sqlDB.update(ModifierSequence.Table.TABLE_NAME,
|
||||
rowsUpdated = sqlDB.update(TABLE_NAME,
|
||||
contentValues,
|
||||
ModifierSequence.Table.COLUMN_ID + " = ?",
|
||||
ModifierSequenceDao.Table.COLUMN_ID + " = ?",
|
||||
new String[] { String.valueOf(id) } );
|
||||
} else {
|
||||
throw new IllegalArgumentException("Parameter `selection` should be empty when updating an ID");
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
package fr.free.nrw.commons.modifications;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountManager;
|
||||
import android.accounts.AuthenticatorException;
|
||||
import android.accounts.OperationCanceledException;
|
||||
import android.content.AbstractThreadedSyncAdapter;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.Context;
|
||||
|
|
@ -14,15 +11,24 @@ import android.os.RemoteException;
|
|||
|
||||
import java.io.IOException;
|
||||
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.Utils;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
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.di.ApplicationlessInjection;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||
|
||||
@Inject MediaWikiApi mwApi;
|
||||
@Inject ContributionDao contributionDao;
|
||||
@Inject ModifierSequenceDao modifierSequenceDao;
|
||||
@Inject
|
||||
SessionManager sessionManager;
|
||||
|
||||
public ModificationsSyncAdapter(Context context, boolean autoInitialize) {
|
||||
super(context, autoInitialize);
|
||||
}
|
||||
|
|
@ -30,6 +36,11 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
|
|||
@Override
|
||||
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!
|
||||
ApplicationlessInjection
|
||||
.getInstance(getContext()
|
||||
.getApplicationContext())
|
||||
.getCommonsApplicationComponent()
|
||||
.inject(this);
|
||||
|
||||
Cursor allModifications;
|
||||
try {
|
||||
|
|
@ -44,27 +55,17 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
|
|||
return;
|
||||
}
|
||||
|
||||
String authCookie;
|
||||
try {
|
||||
authCookie = AccountManager.get(getContext()).blockingGetAuthToken(account, "", false);
|
||||
} catch (OperationCanceledException | AuthenticatorException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (IOException e) {
|
||||
String authCookie = sessionManager.getAuthCookie();
|
||||
if (isNullOrWhiteSpace(authCookie)) {
|
||||
Timber.d("Could not authenticate :(");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Utils.isNullOrWhiteSpace(authCookie)) {
|
||||
Timber.d("Could not authenticate :(");
|
||||
return;
|
||||
}
|
||||
|
||||
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
|
||||
api.setAuthCookie(authCookie);
|
||||
mwApi.setAuthCookie(authCookie);
|
||||
String editToken;
|
||||
|
||||
try {
|
||||
editToken = api.getEditToken();
|
||||
editToken = mwApi.getEditToken();
|
||||
} catch (IOException e) {
|
||||
Timber.d("Can not retreive edit token!");
|
||||
return;
|
||||
|
|
@ -76,28 +77,36 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
|
|||
|
||||
ContentProviderClient contributionsClient = null;
|
||||
try {
|
||||
contributionsClient = getContext().getContentResolver().acquireContentProviderClient(ContributionsContentProvider.AUTHORITY);
|
||||
contributionsClient = getContext().getContentResolver().acquireContentProviderClient(ContributionsContentProvider.CONTRIBUTION_AUTHORITY);
|
||||
|
||||
while (!allModifications.isAfterLast()) {
|
||||
ModifierSequence sequence = ModifierSequence.fromCursor(allModifications);
|
||||
sequence.setContentProviderClient(contentProviderClient);
|
||||
ModifierSequence sequence = modifierSequenceDao.fromCursor(allModifications);
|
||||
Contribution contrib;
|
||||
|
||||
Cursor contributionCursor;
|
||||
|
||||
if (contributionsClient == null) {
|
||||
Timber.e("ContributionsClient is null. This should not happen!");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
contributionCursor = contributionsClient.query(sequence.getMediaUri(), null, null, null, null);
|
||||
} catch (RemoteException 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;
|
||||
try {
|
||||
pageContent = api.revisionsByFilename(contrib.getFilename());
|
||||
pageContent = mwApi.revisionsByFilename(contrib.getFilename());
|
||||
} catch (IOException e) {
|
||||
Timber.d("Network fuckup on modifications sync!");
|
||||
Timber.d("Network messed up on modifications sync!");
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -106,19 +115,19 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
|
|||
|
||||
String editResult;
|
||||
try {
|
||||
editResult = api.edit(editToken, processedPageContent, contrib.getFilename(), sequence.getEditSummary());
|
||||
editResult = mwApi.edit(editToken, processedPageContent, contrib.getFilename(), sequence.getEditSummary());
|
||||
} catch (IOException e) {
|
||||
Timber.d("Network fuckup on modifications sync!");
|
||||
Timber.d("Network messed up on modifications sync!");
|
||||
continue;
|
||||
}
|
||||
|
||||
Timber.d("Response is %s", editResult);
|
||||
|
||||
if (!editResult.equals("Success")) {
|
||||
if (!"Success".equals(editResult)) {
|
||||
// FIXME: Log this somewhere else
|
||||
Timber.d("Non success result! %s", editResult);
|
||||
} else {
|
||||
sequence.delete();
|
||||
modifierSequenceDao.delete(sequence);
|
||||
}
|
||||
}
|
||||
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;
|
||||
|
||||
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 java.util.ArrayList;
|
||||
|
|
@ -17,22 +11,21 @@ public class ModifierSequence {
|
|||
private Uri mediaUri;
|
||||
private ArrayList<PageModifier> modifiers;
|
||||
private Uri contentUri;
|
||||
private ContentProviderClient client;
|
||||
|
||||
public ModifierSequence(Uri mediaUri) {
|
||||
this.mediaUri = mediaUri;
|
||||
modifiers = new ArrayList<>();
|
||||
}
|
||||
|
||||
public ModifierSequence(Uri mediaUri, JSONObject data) {
|
||||
ModifierSequence(Uri mediaUri, JSONObject data) {
|
||||
this(mediaUri);
|
||||
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)));
|
||||
}
|
||||
}
|
||||
|
||||
public Uri getMediaUri() {
|
||||
Uri getMediaUri() {
|
||||
return mediaUri;
|
||||
}
|
||||
|
||||
|
|
@ -40,113 +33,32 @@ public class ModifierSequence {
|
|||
modifiers.add(modifier);
|
||||
}
|
||||
|
||||
public String executeModifications(String pageName, String pageContents) {
|
||||
String executeModifications(String pageName, String pageContents) {
|
||||
for (PageModifier modifier: modifiers) {
|
||||
pageContents = modifier.doModification(pageName, pageContents);
|
||||
}
|
||||
return pageContents;
|
||||
}
|
||||
|
||||
public String getEditSummary() {
|
||||
String getEditSummary() {
|
||||
StringBuilder editSummary = new StringBuilder();
|
||||
for(PageModifier modifier: modifiers) {
|
||||
for (PageModifier modifier: modifiers) {
|
||||
editSummary.append(modifier.getEditSumary()).append(" ");
|
||||
}
|
||||
editSummary.append("Via Commons Mobile App");
|
||||
return editSummary.toString();
|
||||
}
|
||||
|
||||
public JSONObject toJSON() {
|
||||
JSONObject data = new JSONObject();
|
||||
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);
|
||||
}
|
||||
ArrayList<PageModifier> getModifiers() {
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
public ContentValues toContentValues() {
|
||||
ContentValues cv = new ContentValues();
|
||||
cv.put(Table.COLUMN_MEDIA_URI, mediaUri.toString());
|
||||
cv.put(Table.COLUMN_DATA, toJSON().toString());
|
||||
return cv;
|
||||
Uri getContentUri() {
|
||||
return contentUri;
|
||||
}
|
||||
|
||||
public static ModifierSequence fromCursor(Cursor cursor) {
|
||||
// Hardcoding column positions!
|
||||
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;
|
||||
void setContentUri(Uri contentUri) {
|
||||
this.contentUri = contentUri;
|
||||
}
|
||||
|
||||
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) {
|
||||
String name = data.optString("name");
|
||||
if(name.equals(CategoryModifier.MODIFIER_NAME)) {
|
||||
if (name.equals(CategoryModifier.MODIFIER_NAME)) {
|
||||
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"));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,18 +41,18 @@ public class TemplateRemoveModifier extends PageModifier {
|
|||
Pattern templateStartPattern = Pattern.compile("\\{\\{" + templateNormalized, Pattern.CASE_INSENSITIVE);
|
||||
Matcher matcher = templateStartPattern.matcher(pageContents);
|
||||
|
||||
while(matcher.find()) {
|
||||
while (matcher.find()) {
|
||||
int braceCount = 1;
|
||||
int startIndex = matcher.start();
|
||||
int curIndex = matcher.end();
|
||||
Matcher openMatch = PATTERN_TEMPLATE_OPEN.matcher(pageContents);
|
||||
Matcher closeMatch = PATTERN_TEMPLATE_CLOSE.matcher(pageContents);
|
||||
|
||||
while(curIndex < pageContents.length()) {
|
||||
while (curIndex < pageContents.length()) {
|
||||
boolean openFound = openMatch.find(curIndex);
|
||||
boolean closeFound = closeMatch.find(curIndex);
|
||||
|
||||
if(openFound && (!closeFound || openMatch.start() < closeMatch.start())) {
|
||||
if (openFound && (!closeFound || openMatch.start() < closeMatch.start())) {
|
||||
braceCount++;
|
||||
curIndex = openMatch.end();
|
||||
} else if (closeFound) {
|
||||
|
|
@ -71,8 +71,8 @@ public class TemplateRemoveModifier extends PageModifier {
|
|||
}
|
||||
|
||||
// Strip trailing whitespace
|
||||
while(curIndex < pageContents.length()) {
|
||||
if(pageContents.charAt(curIndex) == ' ' || pageContents.charAt(curIndex) == '\n') {
|
||||
while (curIndex < pageContents.length()) {
|
||||
if (pageContents.charAt(curIndex) == ' ' || pageContents.charAt(curIndex) == '\n') {
|
||||
curIndex++;
|
||||
} else {
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package fr.free.nrw.commons.mwapi;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
|
@ -21,10 +23,14 @@ import org.apache.http.params.CoreProtocolPNames;
|
|||
import org.apache.http.util.EntityUtils;
|
||||
import org.mediawiki.api.ApiResult;
|
||||
import org.mediawiki.api.MWApi;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
|
|
@ -34,12 +40,17 @@ import java.util.concurrent.Callable;
|
|||
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
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 io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
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
|
||||
*/
|
||||
|
|
@ -49,17 +60,27 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
|||
private static final String THUMB_SIZE = "640";
|
||||
private AbstractHttpClient httpClient;
|
||||
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();
|
||||
SchemeRegistry schemeRegistry = new SchemeRegistry();
|
||||
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
|
||||
final SSLSocketFactory sslSocketFactory = SSLSocketFactory.getSocketFactory();
|
||||
schemeRegistry.register(new Scheme("https", sslSocketFactory, 443));
|
||||
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);
|
||||
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
|
||||
|
|
@ -74,11 +95,13 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
|||
* @throws IOException On api request IO issue
|
||||
*/
|
||||
public String login(String username, String password) throws IOException {
|
||||
String loginToken = getLoginToken();
|
||||
Timber.d("Login token is %s", loginToken);
|
||||
return getErrorCodeToReturn(api.action("clientlogin")
|
||||
.param("rememberMe", "1")
|
||||
.param("username", username)
|
||||
.param("password", password)
|
||||
.param("logintoken", getLoginToken())
|
||||
.param("logintoken", loginToken)
|
||||
.param("loginreturnurl", "https://commons.wikimedia.org")
|
||||
.post());
|
||||
}
|
||||
|
|
@ -91,12 +114,14 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
|||
* @throws IOException On api request IO issue
|
||||
*/
|
||||
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")
|
||||
.param("rememberMe", "1")
|
||||
.param("rememberMe", "true")
|
||||
.param("username", username)
|
||||
.param("password", password)
|
||||
.param("logintoken", getLoginToken())
|
||||
.param("logincontinue", "1")
|
||||
.param("logintoken", loginToken)
|
||||
.param("logincontinue", "true")
|
||||
.param("OATHToken", twoFactorCode)
|
||||
.post());
|
||||
}
|
||||
|
|
@ -121,14 +146,17 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
|||
String status = loginApiResult.getString("/api/clientlogin/@status");
|
||||
if (status.equals("PASS")) {
|
||||
api.isLoggedIn = true;
|
||||
setAuthCookieOnLogin(true);
|
||||
return status;
|
||||
} else if (status.equals("FAIL")) {
|
||||
setAuthCookieOnLogin(false);
|
||||
return loginApiResult.getString("/api/clientlogin/@messagecode");
|
||||
} else if (
|
||||
status.equals("UI")
|
||||
&& loginApiResult.getString("/api/clientlogin/requests/_v/@id").equals("TOTPAuthenticationRequest")
|
||||
&& loginApiResult.getString("/api/clientlogin/requests/_v/@provider").equals("Two-factor authentication (OATH).")
|
||||
) {
|
||||
setAuthCookieOnLogin(false);
|
||||
return "2FA";
|
||||
}
|
||||
|
||||
|
|
@ -136,6 +164,18 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
|||
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
|
||||
public String getAuthCookie() {
|
||||
return api.getAuthCookie();
|
||||
|
|
@ -335,7 +375,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
|||
logEvents.add(new LogEventResult.LogEvent(
|
||||
image.getString("@pageid"),
|
||||
image.getString("@title"),
|
||||
Utils.parseMWDate(image.getString("@timestamp")))
|
||||
parseMWDate(image.getString("@timestamp")))
|
||||
);
|
||||
}
|
||||
return logEvents;
|
||||
|
|
@ -352,6 +392,42 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
|||
.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
|
||||
public boolean existingFile(String fileSha1) throws IOException {
|
||||
return api.action("query")
|
||||
|
|
@ -387,17 +463,22 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
|||
|
||||
@Override
|
||||
@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);
|
||||
|
||||
Log.e("WTF", "Result: " +result.toString());
|
||||
Log.e("WTF", "Result: " + result.toString());
|
||||
|
||||
String resultStatus = result.getString("/api/upload/@result");
|
||||
if (!resultStatus.equals("Success")) {
|
||||
String errorCode = result.getString("/api/error/@code");
|
||||
return new UploadResult(resultStatus, errorCode);
|
||||
} 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 imageUrl = result.getString("/api/upload/imageinfo/@url");
|
||||
return new UploadResult(resultStatus, dateUploaded, canonicalFilename, imageUrl);
|
||||
|
|
@ -423,4 +504,13 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
|||
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;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
|
||||
import fr.free.nrw.commons.Utils;
|
||||
|
|
@ -15,14 +16,14 @@ public class EventLog {
|
|||
}
|
||||
}
|
||||
|
||||
private static LogBuilder schema(String schema, long revision) {
|
||||
return new LogBuilder(schema, revision);
|
||||
private static LogBuilder schema(String schema, long revision, MediaWikiApi mwApi, SharedPreferences prefs) {
|
||||
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) {
|
||||
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.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
|
@ -12,21 +11,39 @@ import java.net.MalformedURLException;
|
|||
import java.net.URL;
|
||||
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.Utils;
|
||||
import fr.free.nrw.commons.settings.Prefs;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class LogBuilder {
|
||||
private JSONObject data;
|
||||
private long rev;
|
||||
private String schema;
|
||||
private final MediaWikiApi mwApi;
|
||||
private final JSONObject data;
|
||||
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.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) {
|
||||
try {
|
||||
data.put(key, value);
|
||||
|
|
@ -36,6 +53,10 @@ public class LogBuilder {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes JSON object to URL
|
||||
* @return URL to JSON object
|
||||
*/
|
||||
URL toUrl() {
|
||||
JSONObject fullData = new JSONObject();
|
||||
try {
|
||||
|
|
@ -56,14 +77,13 @@ public class LogBuilder {
|
|||
// Use *only* for tracking the user preference change for EventLogging
|
||||
// Attempting to use anywhere else will cause kitten explosions
|
||||
public void log(boolean force) {
|
||||
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(CommonsApplication.getInstance());
|
||||
if (!settings.getBoolean(Prefs.TRACKING_ENABLED, true) && !force) {
|
||||
if (!prefs.getBoolean(Prefs.TRACKING_ENABLED, true) && !force) {
|
||||
return; // User has disabled tracking
|
||||
}
|
||||
LogTask logTask = new LogTask();
|
||||
LogTask logTask = new LogTask(mwApi);
|
||||
logTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, this);
|
||||
}
|
||||
|
||||
|
||||
public void log() {
|
||||
log(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,26 @@ package fr.free.nrw.commons.mwapi;
|
|||
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
|
||||
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
|
||||
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 parseTreeXmlSource;
|
||||
|
||||
/**
|
||||
* Full-fledged constructor of MediaResult
|
||||
*
|
||||
* @param wikiSource Media wiki source
|
||||
* @param parseTreeXmlSource Media tree parsed in XML
|
||||
*/
|
||||
MediaResult(String wikiSource, String parseTreeXmlSource) {
|
||||
this.wikiSource = wikiSource;
|
||||
this.parseTreeXmlSource = parseTreeXmlSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets wiki source
|
||||
* @return Wiki source
|
||||
*/
|
||||
public String getWikiSource() {
|
||||
return wikiSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets tree parsed in XML
|
||||
* @return XML parsed tree
|
||||
*/
|
||||
public String getParseTreeXmlSource() {
|
||||
return parseTreeXmlSource;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,15 @@ import android.support.annotation.Nullable;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
import fr.free.nrw.commons.notification.Notification;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
|
||||
public interface MediaWikiApi {
|
||||
String getUserAgent();
|
||||
|
||||
String getAuthCookie();
|
||||
|
||||
void setAuthCookie(String authCookie);
|
||||
|
|
@ -43,6 +47,9 @@ public interface MediaWikiApi {
|
|||
@NonNull
|
||||
Observable<String> allCategories(String filter, int searchCatsLimit);
|
||||
|
||||
@NonNull
|
||||
List<Notification> getNotifications() throws IOException;
|
||||
|
||||
@NonNull
|
||||
Observable<String> searchTitles(String title, int searchCatsLimit);
|
||||
|
||||
|
|
@ -51,6 +58,8 @@ public interface MediaWikiApi {
|
|||
|
||||
boolean existingFile(String fileSha1) throws IOException;
|
||||
|
||||
|
||||
|
||||
@NonNull
|
||||
LogEventResult logEvents(String user, String lastModified, String queryContinue, int limit) throws IOException;
|
||||
|
||||
|
|
|
|||
|
|
@ -9,11 +9,24 @@ public class UploadResult {
|
|||
private String imageUrl;
|
||||
private String canonicalFilename;
|
||||
|
||||
/**
|
||||
* Minimal constructor
|
||||
*
|
||||
* @param resultStatus Upload result status
|
||||
* @param errorCode Upload error code
|
||||
*/
|
||||
UploadResult(String resultStatus, String errorCode) {
|
||||
this.resultStatus = resultStatus;
|
||||
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) {
|
||||
this.resultStatus = resultStatus;
|
||||
this.dateUploaded = dateUploaded;
|
||||
|
|
@ -21,22 +34,42 @@ public class UploadResult {
|
|||
this.imageUrl = imageUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets uploaded date
|
||||
* @return Upload date
|
||||
*/
|
||||
public Date getDateUploaded() {
|
||||
return dateUploaded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets image url
|
||||
* @return Uploaded image url
|
||||
*/
|
||||
public String getImageUrl() {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets canonical file name
|
||||
* @return Uploaded file name
|
||||
*/
|
||||
public String getCanonicalFilename() {
|
||||
return canonicalFilename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets upload error code
|
||||
* @return Error code
|
||||
*/
|
||||
public String getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets upload result status
|
||||
* @return Upload result status
|
||||
*/
|
||||
public String getResultStatus() {
|
||||
return resultStatus;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,15 @@
|
|||
package fr.free.nrw.commons.nearby;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.location.LocationManager;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentTransaction;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
|
|
@ -29,30 +23,43 @@ import com.google.gson.GsonBuilder;
|
|||
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
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.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;
|
||||
|
||||
|
||||
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 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 Bundle bundle;
|
||||
private NearbyAsyncTask nearbyAsyncTask;
|
||||
private SharedPreferences sharedPreferences;
|
||||
private NearbyActivityMode viewMode;
|
||||
private Disposable placesDisposable;
|
||||
private boolean lockNearbyView; //Determines if the nearby places needs to be refreshed
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
|
|
@ -60,7 +67,6 @@ public class NearbyActivity extends NavigationBaseActivity {
|
|||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
|
||||
setContentView(R.layout.activity_nearby);
|
||||
ButterKnife.bind(this);
|
||||
checkLocationPermission();
|
||||
bundle = new Bundle();
|
||||
initDrawer();
|
||||
initViewState();
|
||||
|
|
@ -92,7 +98,8 @@ public class NearbyActivity extends NavigationBaseActivity {
|
|||
// Handle item selection
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_refresh:
|
||||
refreshView();
|
||||
lockNearbyView(false);
|
||||
refreshView(true);
|
||||
return true;
|
||||
case R.id.action_toggle_view:
|
||||
viewMode = viewMode.toggle();
|
||||
|
|
@ -104,60 +111,9 @@ public class NearbyActivity extends NavigationBaseActivity {
|
|||
}
|
||||
}
|
||||
|
||||
private void startLookingForNearby() {
|
||||
locationManager = new LocationServiceManager(this);
|
||||
locationManager.registerLocationManager();
|
||||
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();
|
||||
private void requestLocationPermissions() {
|
||||
if (!isFinishing()) {
|
||||
locationManager.requestPermissions(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -166,16 +122,10 @@ public class NearbyActivity extends NavigationBaseActivity {
|
|||
switch (requestCode) {
|
||||
case LOCATION_REQUEST: {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
startLookingForNearby();
|
||||
refreshView(false);
|
||||
} else {
|
||||
//If permission not granted, go to page that says Nearby Places cannot be displayed
|
||||
if (nearbyAsyncTask != null) {
|
||||
nearbyAsyncTask.cancel(true);
|
||||
}
|
||||
if (progressBar != null) {
|
||||
progressBar.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
hideProgressBar();
|
||||
showLocationPermissionDeniedErrorDialog();
|
||||
}
|
||||
}
|
||||
|
|
@ -188,7 +138,7 @@ public class NearbyActivity extends NavigationBaseActivity {
|
|||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.give_permission, (dialog, which) -> {
|
||||
//will ask for the location permission again
|
||||
checkLocationPermission();
|
||||
checkGps();
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, (dialog, which) -> {
|
||||
//dismiss dialog and finish activity
|
||||
|
|
@ -200,8 +150,7 @@ public class NearbyActivity extends NavigationBaseActivity {
|
|||
}
|
||||
|
||||
private void checkGps() {
|
||||
LocationManager manager = (LocationManager) getSystemService(LOCATION_SERVICE);
|
||||
if (!manager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
|
||||
if (!locationManager.isProviderEnabled()) {
|
||||
Timber.d("GPS is not enabled");
|
||||
new AlertDialog.Builder(this)
|
||||
.setMessage(R.string.gps_disabled)
|
||||
|
|
@ -213,11 +162,48 @@ public class NearbyActivity extends NavigationBaseActivity {
|
|||
Timber.d("Loaded settings page");
|
||||
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()
|
||||
.show();
|
||||
} else {
|
||||
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,104 +212,122 @@ public class NearbyActivity extends NavigationBaseActivity {
|
|||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (requestCode == 1) {
|
||||
Timber.d("User is back from Settings page");
|
||||
refreshView();
|
||||
refreshView(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void toggleView() {
|
||||
if (nearbyAsyncTask != null) {
|
||||
if (nearbyAsyncTask.getStatus() == AsyncTask.Status.FINISHED) {
|
||||
if (viewMode.isMap()) {
|
||||
setMapFragment();
|
||||
} else {
|
||||
setListFragment();
|
||||
}
|
||||
}
|
||||
sharedPreferences.edit().putBoolean(MAP_LAST_USED_PREFERENCE, viewMode.isMap()).apply();
|
||||
if (viewMode.isMap()) {
|
||||
setMapFragment();
|
||||
} else {
|
||||
setListFragment();
|
||||
}
|
||||
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
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
lockNearbyView = false;
|
||||
checkGps();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
if (nearbyAsyncTask != null) {
|
||||
nearbyAsyncTask.cancel(true);
|
||||
/**
|
||||
* This method should be the single point to load/refresh nearby places
|
||||
*
|
||||
* @param isHardRefresh Should display a toast if the location hasn't changed
|
||||
*/
|
||||
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;
|
||||
}
|
||||
curLatLang = lastLocation;
|
||||
|
||||
if (curLatLang == null) {
|
||||
Timber.d("Skipping update of nearby places as location is unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
placesDisposable = Observable.fromCallable(() -> nearbyController
|
||||
.loadAttractionsFromLocation(curLatLang, this))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::populatePlaces);
|
||||
}
|
||||
|
||||
private void refreshView() {
|
||||
nearbyAsyncTask = new NearbyAsyncTask(this);
|
||||
nearbyAsyncTask.execute();
|
||||
private void populatePlaces(List<Place> placeList) {
|
||||
Gson gson = new GsonBuilder()
|
||||
.registerTypeAdapter(Uri.class, new UriSerializer())
|
||||
.create();
|
||||
String gsonPlaceList = gson.toJson(placeList);
|
||||
String gsonCurLatLng = gson.toJson(curLatLang);
|
||||
|
||||
if (placeList.size() == 0) {
|
||||
int duration = Toast.LENGTH_SHORT;
|
||||
Toast toast = Toast.makeText(this, R.string.no_nearby, duration);
|
||||
toast.show();
|
||||
}
|
||||
|
||||
bundle.clear();
|
||||
bundle.putString("PlaceList", gsonPlaceList);
|
||||
bundle.putString("CurLatLng", gsonCurLatLng);
|
||||
|
||||
lockNearbyView(true);
|
||||
// Begin the transaction
|
||||
if (viewMode.isMap()) {
|
||||
setMapFragment();
|
||||
} else {
|
||||
setListFragment();
|
||||
}
|
||||
|
||||
hideProgressBar();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (locationManager != null) {
|
||||
private void lockNearbyView(boolean lock) {
|
||||
if (lock) {
|
||||
lockNearbyView = true;
|
||||
locationManager.unregisterLocationManager();
|
||||
locationManager.removeLocationListener(this);
|
||||
} else {
|
||||
lockNearbyView = false;
|
||||
locationManager.registerLocationManager();
|
||||
locationManager.addLocationListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Gson gson = new GsonBuilder()
|
||||
.registerTypeAdapter(Uri.class, new UriSerializer())
|
||||
.create();
|
||||
String gsonPlaceList = gson.toJson(placeList);
|
||||
String gsonCurLatLng = gson.toJson(curLatLang);
|
||||
|
||||
if (placeList.size() == 0) {
|
||||
int duration = Toast.LENGTH_SHORT;
|
||||
Toast toast = Toast.makeText(mContext, R.string.no_nearby, duration);
|
||||
toast.show();
|
||||
}
|
||||
|
||||
bundle.clear();
|
||||
bundle.putString("PlaceList", gsonPlaceList);
|
||||
bundle.putString("CurLatLng", gsonCurLatLng);
|
||||
|
||||
// Begin the transaction
|
||||
if (viewMode.isMap()) {
|
||||
setMapFragment();
|
||||
} else {
|
||||
setListFragment();
|
||||
}
|
||||
|
||||
if (progressBar != null) {
|
||||
progressBar.setVisibility(View.GONE);
|
||||
}
|
||||
private void hideProgressBar() {
|
||||
if (progressBar != null) {
|
||||
progressBar.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -334,7 +338,7 @@ public class NearbyActivity extends NavigationBaseActivity {
|
|||
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
|
||||
Fragment fragment = new NearbyMapFragment();
|
||||
fragment.setArguments(bundle);
|
||||
fragmentTransaction.replace(R.id.container, fragment);
|
||||
fragmentTransaction.replace(R.id.container, fragment, fragment.getClass().getSimpleName());
|
||||
fragmentTransaction.commitAllowingStateLoss();
|
||||
}
|
||||
|
||||
|
|
@ -345,12 +349,12 @@ public class NearbyActivity extends NavigationBaseActivity {
|
|||
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
|
||||
Fragment fragment = new NearbyListFragment();
|
||||
fragment.setArguments(bundle);
|
||||
fragmentTransaction.replace(R.id.container, fragment);
|
||||
fragmentTransaction.replace(R.id.container, fragment, fragment.getClass().getSimpleName());
|
||||
fragmentTransaction.commitAllowingStateLoss();
|
||||
}
|
||||
|
||||
public static void startYourself(Context context) {
|
||||
Intent settingsIntent = new Intent(context, NearbyActivity.class);
|
||||
context.startActivity(settingsIntent);
|
||||
@Override
|
||||
public void onLocationChanged(LatLng latLng) {
|
||||
refreshView(false);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package fr.free.nrw.commons.nearby;
|
|||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.graphics.drawable.VectorDrawableCompat;
|
||||
|
||||
import com.mapbox.mapboxsdk.annotations.IconFactory;
|
||||
|
|
@ -15,7 +14,9 @@ import java.util.List;
|
|||
import java.util.Locale;
|
||||
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.location.LatLng;
|
||||
import fr.free.nrw.commons.utils.UiUtils;
|
||||
|
|
@ -24,45 +25,51 @@ import timber.log.Timber;
|
|||
import static fr.free.nrw.commons.utils.LengthUtils.computeDistanceBetween;
|
||||
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
|
||||
|
||||
|
||||
public class NearbyController {
|
||||
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.
|
||||
*
|
||||
* @param curLatLng current location for user
|
||||
* @param context context
|
||||
* @param context context
|
||||
* @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);
|
||||
if (curLatLng == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
NearbyPlaces nearbyPlaces = CommonsApplication.getInstance().getNearbyPlaces();
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
List<Place> places = prefs.getBoolean("useWikidata", true)
|
||||
? nearbyPlaces.getFromWikidataQuery(curLatLng, Locale.getDefault().getLanguage())
|
||||
: nearbyPlaces.getFromWikiNeedsPictures();
|
||||
if (curLatLng != null) {
|
||||
Timber.d("Sorting places by distance...");
|
||||
final Map<Place, Double> distances = new HashMap<>();
|
||||
for (Place place: places) {
|
||||
distances.put(place, computeDistanceBetween(place.location, curLatLng));
|
||||
}
|
||||
Collections.sort(places,
|
||||
(lhs, rhs) -> {
|
||||
double lhsDistance = distances.get(lhs);
|
||||
double rhsDistance = distances.get(rhs);
|
||||
return (int) (lhsDistance - rhsDistance);
|
||||
}
|
||||
);
|
||||
Timber.d("Sorting places by distance...");
|
||||
final Map<Place, Double> distances = new HashMap<>();
|
||||
for (Place place : places) {
|
||||
distances.put(place, computeDistanceBetween(place.location, curLatLng));
|
||||
}
|
||||
Collections.sort(places,
|
||||
(lhs, rhs) -> {
|
||||
double lhsDistance = distances.get(lhs);
|
||||
double rhsDistance = distances.get(rhs);
|
||||
return (int) (lhsDistance - rhsDistance);
|
||||
}
|
||||
);
|
||||
return places;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads attractions from location for list view, we need to return Place data type.
|
||||
*
|
||||
* @param curLatLng users current location
|
||||
* @param placeList list of nearby places in Place data type
|
||||
* @return Place list that holds nearby places
|
||||
|
|
@ -71,7 +78,7 @@ public class NearbyController {
|
|||
LatLng curLatLng,
|
||||
List<Place> placeList) {
|
||||
placeList = placeList.subList(0, Math.min(placeList.size(), MAX_RESULTS));
|
||||
for (Place place: placeList) {
|
||||
for (Place place : placeList) {
|
||||
String distance = formatDistanceBetween(curLatLng, place.location);
|
||||
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 placeList list of nearby places in Place data type
|
||||
* @return BaseMarkerOptions list that holds nearby places
|
||||
|
|
@ -96,26 +104,28 @@ public class NearbyController {
|
|||
|
||||
placeList = placeList.subList(0, Math.min(placeList.size(), MAX_RESULTS));
|
||||
|
||||
Bitmap icon = UiUtils.getBitmap(
|
||||
VectorDrawableCompat.create(
|
||||
context.getResources(), R.drawable.ic_custom_map_marker, context.getTheme()
|
||||
));
|
||||
VectorDrawableCompat vectorDrawable = VectorDrawableCompat.create(
|
||||
context.getResources(), R.drawable.ic_custom_map_marker, context.getTheme()
|
||||
);
|
||||
if (vectorDrawable != null) {
|
||||
Bitmap icon = UiUtils.getBitmap(vectorDrawable);
|
||||
|
||||
for (Place place: placeList) {
|
||||
String distance = formatDistanceBetween(curLatLng, place.location);
|
||||
place.setDistance(distance);
|
||||
for (Place place : placeList) {
|
||||
String distance = formatDistanceBetween(curLatLng, place.location);
|
||||
place.setDistance(distance);
|
||||
|
||||
NearbyBaseMarker nearbyBaseMarker = new NearbyBaseMarker();
|
||||
nearbyBaseMarker.title(place.name);
|
||||
nearbyBaseMarker.position(
|
||||
new com.mapbox.mapboxsdk.geometry.LatLng(
|
||||
place.location.getLatitude(),
|
||||
place.location.getLongitude()));
|
||||
nearbyBaseMarker.place(place);
|
||||
nearbyBaseMarker.icon(IconFactory.getInstance(context)
|
||||
.fromBitmap(icon));
|
||||
NearbyBaseMarker nearbyBaseMarker = new NearbyBaseMarker();
|
||||
nearbyBaseMarker.title(place.name);
|
||||
nearbyBaseMarker.position(
|
||||
new com.mapbox.mapboxsdk.geometry.LatLng(
|
||||
place.location.getLatitude(),
|
||||
place.location.getLongitude()));
|
||||
nearbyBaseMarker.place(place);
|
||||
nearbyBaseMarker.icon(IconFactory.getInstance(context)
|
||||
.fromBitmap(icon));
|
||||
|
||||
baseMarkerOptions.add(nearbyBaseMarker);
|
||||
baseMarkerOptions.add(nearbyBaseMarker);
|
||||
}
|
||||
}
|
||||
return baseMarkerOptions;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ public class NearbyInfoDialog extends OverlayDialog {
|
|||
NearbyInfoDialog mDialog = new NearbyInfoDialog();
|
||||
Bundle bundle = new Bundle();
|
||||
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_LONGITUDE, place.location.getLongitude());
|
||||
bundle.putParcelable(ARG_SITE_LINK, place.siteLinks);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package fr.free.nrw.commons.nearby;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
|
|
@ -17,6 +18,7 @@ import java.lang.reflect.Type;
|
|||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import dagger.android.support.AndroidSupportInjection;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
import fr.free.nrw.commons.utils.UriDeserializer;
|
||||
|
|
@ -40,6 +42,12 @@ public class NearbyListFragment extends Fragment {
|
|||
setRetainInstance(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
AndroidSupportInjection.inject(this);
|
||||
super.onAttach(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater,
|
||||
ViewGroup container,
|
||||
|
|
@ -60,7 +68,7 @@ public class NearbyListFragment extends Fragment {
|
|||
|
||||
Bundle bundle = this.getArguments();
|
||||
if (bundle != null) {
|
||||
String gsonPlaceList = bundle.getString("PlaceList");
|
||||
String gsonPlaceList = bundle.getString("PlaceList", "[]");
|
||||
placeList = gson.fromJson(gsonPlaceList, LIST_TYPE);
|
||||
|
||||
String gsonLatLng = bundle.getString("CurLatLng");
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package fr.free.nrw.commons.nearby;
|
|||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
|
@ -77,6 +76,8 @@ public class NearbyMapFragment extends android.support.v4.app.Fragment {
|
|||
private void setupMapView(Bundle savedInstanceState) {
|
||||
MapboxMapOptions options = new MapboxMapOptions()
|
||||
.styleUrl(Style.OUTDOORS)
|
||||
.logoEnabled(false)
|
||||
.attributionEnabled(false)
|
||||
.camera(new CameraPosition.Builder()
|
||||
.target(new LatLng(curLatLng.getLatitude(), curLatLng.getLongitude()))
|
||||
.zoom(11)
|
||||
|
|
@ -99,11 +100,8 @@ public class NearbyMapFragment extends android.support.v4.app.Fragment {
|
|||
|
||||
addCurrentLocationMarker(mapboxMap);
|
||||
});
|
||||
if (PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean("theme",false)) {
|
||||
mapView.setStyleUrl(getResources().getString(R.string.map_theme_dark));
|
||||
} else {
|
||||
mapView.setStyleUrl(getResources().getString(R.string.map_theme_light));
|
||||
}
|
||||
|
||||
mapView.setStyleUrl("asset://mapstyle.json");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ public class NearbyPlaces {
|
|||
|
||||
public NearbyPlaces() {
|
||||
try {
|
||||
wikidataQuery = FileUtils.readFromResource("/assets/queries/nearby_query.rq");
|
||||
wikidataQuery = FileUtils.readFromResource("/queries/nearby_query.rq");
|
||||
Timber.v(wikidataQuery);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
|
|
@ -46,7 +46,7 @@ public class NearbyPlaces {
|
|||
|
||||
try {
|
||||
// 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);
|
||||
Timber.d("%d results at radius: %f", places.size(), radius);
|
||||
if (places.size() >= MIN_RESULTS) {
|
||||
|
|
@ -62,6 +62,11 @@ public class NearbyPlaces {
|
|||
Timber.d("back to initial radius: %f", 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;
|
||||
}
|
||||
|
||||
|
|
@ -121,7 +126,7 @@ public class NearbyPlaces {
|
|||
|
||||
places.add(new Place(
|
||||
name,
|
||||
Place.Description.fromText(type), // list
|
||||
Place.Label.fromText(type), // list
|
||||
type, // details
|
||||
Uri.parse(icon),
|
||||
new LatLng(latitude, longitude, 0),
|
||||
|
|
@ -183,7 +188,7 @@ public class NearbyPlaces {
|
|||
|
||||
places.add(new Place(
|
||||
name,
|
||||
Place.Description.fromText(type), // list
|
||||
Place.Label.fromText(type), // list
|
||||
type, // details
|
||||
null,
|
||||
new LatLng(latitude, longitude, 0),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package fr.free.nrw.commons.nearby;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.view.LayoutInflater;
|
||||
|
|
@ -7,6 +8,7 @@ import android.view.View;
|
|||
import android.view.ViewGroup;
|
||||
|
||||
import butterknife.ButterKnife;
|
||||
import dagger.android.support.AndroidSupportInjection;
|
||||
import fr.free.nrw.commons.R;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
|
@ -18,6 +20,12 @@ public class NoPermissionsFragment extends Fragment {
|
|||
public NoPermissionsFragment() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
AndroidSupportInjection.inject(this);
|
||||
super.onAttach(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import fr.free.nrw.commons.location.LatLng;
|
|||
public class Place {
|
||||
|
||||
public final String name;
|
||||
private final Description description;
|
||||
private final Label label;
|
||||
private final String longDescription;
|
||||
private final Uri secondaryImageUrl;
|
||||
public final LatLng location;
|
||||
|
|
@ -24,18 +24,22 @@ public class Place {
|
|||
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) {
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.label = label;
|
||||
this.longDescription = longDescription;
|
||||
this.secondaryImageUrl = secondaryImageUrl;
|
||||
this.location = location;
|
||||
this.siteLinks = siteLinks;
|
||||
}
|
||||
|
||||
public Description getDescription() {
|
||||
return description;
|
||||
public Label getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public String getLongDescription() {
|
||||
return longDescription;
|
||||
}
|
||||
|
||||
public void setDistance(String distance) {
|
||||
|
|
@ -67,10 +71,8 @@ public class Place {
|
|||
* Most common types of desc: building, house, cottage, farmhouse,
|
||||
* village, civil parish, church, railway station,
|
||||
* 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),
|
||||
HOUSE("house", R.drawable.round_icon_house),
|
||||
|
|
@ -95,19 +97,19 @@ public class Place {
|
|||
WATERFALL("waterfall", R.drawable.round_icon_waterfall),
|
||||
UNKNOWN("?", R.drawable.round_icon_unknown);
|
||||
|
||||
private static final Map<String, Description> TEXT_TO_DESCRIPTION
|
||||
= new HashMap<>(Description.values().length);
|
||||
private static final Map<String, Label> TEXT_TO_DESCRIPTION
|
||||
= new HashMap<>(Label.values().length);
|
||||
|
||||
static {
|
||||
for (Description description : values()) {
|
||||
TEXT_TO_DESCRIPTION.put(description.text, description);
|
||||
for (Label label : values()) {
|
||||
TEXT_TO_DESCRIPTION.put(label.text, label);
|
||||
}
|
||||
}
|
||||
|
||||
private final String text;
|
||||
@DrawableRes private final int icon;
|
||||
|
||||
Description(String text, @DrawableRes int icon) {
|
||||
Label(String text, @DrawableRes int icon) {
|
||||
this.text = text;
|
||||
this.icon = icon;
|
||||
}
|
||||
|
|
@ -121,9 +123,9 @@ public class Place {
|
|||
return icon;
|
||||
}
|
||||
|
||||
public static Description fromText(String text) {
|
||||
Description description = TEXT_TO_DESCRIPTION.get(text);
|
||||
return description == null ? UNKNOWN : description;
|
||||
public static Label fromText(String text) {
|
||||
Label label = TEXT_TO_DESCRIPTION.get(text);
|
||||
return label == null ? UNKNOWN : label;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,13 +43,13 @@ class PlaceRenderer extends Renderer<Place> {
|
|||
public void render() {
|
||||
Place place = getContent();
|
||||
tvName.setText(place.name);
|
||||
String descriptionText = place.getDescription().getText();
|
||||
String descriptionText = place.getLongDescription();
|
||||
if (descriptionText.equals("?")) {
|
||||
descriptionText = getContext().getString(R.string.no_description_found);
|
||||
}
|
||||
tvDesc.setText(descriptionText);
|
||||
distance.setText(place.distance);
|
||||
icon.setImageResource(place.getDescription().getIcon());
|
||||
icon.setImageResource(place.getLabel().getIcon());
|
||||
}
|
||||
|
||||
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,62 +1,70 @@
|
|||
package fr.free.nrw.commons.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v7.app.AppCompatDelegate;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import butterknife.ButterKnife;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
||||
|
||||
public class SettingsActivity extends NavigationBaseActivity {
|
||||
private AppCompatDelegate settingsDelegate;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
// Check prefs on every activity starts
|
||||
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme",false)) {
|
||||
setTheme(R.style.DarkAppTheme);
|
||||
} else {
|
||||
setTheme(R.style.LightAppTheme);
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_settings);
|
||||
|
||||
ButterKnife.bind(this);
|
||||
initDrawer();
|
||||
}
|
||||
|
||||
// Get an action bar
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
if (settingsDelegate == null) {
|
||||
settingsDelegate = AppCompatDelegate.create(this, null);
|
||||
}
|
||||
settingsDelegate.onPostCreate(savedInstanceState);
|
||||
|
||||
//Get an up button
|
||||
//settingsDelegate.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
//Handle action-bar clicks
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
finish();
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
public static void startYourself(Context context) {
|
||||
Intent settingsIntent = new Intent(context, SettingsActivity.class);
|
||||
context.startActivity(settingsIntent);
|
||||
}
|
||||
package fr.free.nrw.commons.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v7.app.AppCompatDelegate;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import butterknife.ButterKnife;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
||||
|
||||
/**
|
||||
* allows the user to change the settings
|
||||
*/
|
||||
public class SettingsActivity extends NavigationBaseActivity {
|
||||
private AppCompatDelegate settingsDelegate;
|
||||
|
||||
/**
|
||||
* to be called when the activity starts
|
||||
* @param savedInstanceState the previously saved state
|
||||
*/
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
// Check prefs on every activity starts
|
||||
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme",false)) {
|
||||
setTheme(R.style.DarkAppTheme);
|
||||
} else {
|
||||
setTheme(R.style.LightAppTheme);
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_settings);
|
||||
|
||||
ButterKnife.bind(this);
|
||||
initDrawer();
|
||||
}
|
||||
|
||||
// Get an action bar
|
||||
/**
|
||||
* takes care of actions taken after the creation has happened
|
||||
* @param savedInstanceState the saved state
|
||||
*/
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
if (settingsDelegate == null) {
|
||||
settingsDelegate = AppCompatDelegate.create(this, null);
|
||||
}
|
||||
settingsDelegate.onPostCreate(savedInstanceState);
|
||||
|
||||
//Get an up button
|
||||
//settingsDelegate.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle action-bar clicks
|
||||
* @param item the selected item
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
finish();
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +1,50 @@
|
|||
package fr.free.nrw.commons.settings;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.preference.CheckBoxPreference;
|
||||
import android.preference.EditTextPreference;
|
||||
import android.preference.ListPreference;
|
||||
import android.preference.Preference;
|
||||
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.R;
|
||||
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 {
|
||||
|
||||
private static final int REQUEST_CODE_WRITE_EXTERNAL_STORAGE = 100;
|
||||
|
||||
@Inject @Named("default_preferences") SharedPreferences prefs;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
ApplicationlessInjection
|
||||
.getInstance(getActivity().getApplicationContext())
|
||||
.getCommonsApplicationComponent()
|
||||
.inject(this);
|
||||
|
||||
// Load the preferences from an XML resource
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
|
|
@ -38,14 +66,17 @@ public class SettingsFragment extends PreferenceFragment {
|
|||
});
|
||||
|
||||
final EditTextPreference uploadLimit = (EditTextPreference) findPreference("uploads");
|
||||
final SharedPreferences sharedPref = PreferenceManager
|
||||
.getDefaultSharedPreferences(CommonsApplication.getInstance());
|
||||
int uploads = sharedPref.getInt(Prefs.UPLOADS_SHOWING, 100);
|
||||
int uploads = prefs.getInt(Prefs.UPLOADS_SHOWING, 100);
|
||||
uploadLimit.setText(uploads + "");
|
||||
uploadLimit.setSummary(uploads + "");
|
||||
uploadLimit.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
int value = Integer.parseInt(newValue.toString());
|
||||
final SharedPreferences.Editor editor = sharedPref.edit();
|
||||
int value;
|
||||
try {
|
||||
value = Integer.parseInt(newValue.toString());
|
||||
} catch(Exception e) {
|
||||
value = 100; //Default number
|
||||
}
|
||||
final SharedPreferences.Editor editor = prefs.edit();
|
||||
if (value > 500) {
|
||||
new AlertDialog.Builder(getActivity())
|
||||
.setTitle(R.string.maximum_limit)
|
||||
|
|
@ -58,14 +89,71 @@ public class SettingsFragment extends PreferenceFragment {
|
|||
uploadLimit.setSummary(500 + "");
|
||||
uploadLimit.setText(500 + "");
|
||||
} else {
|
||||
editor.putInt(Prefs.UPLOADS_SHOWING, Integer.parseInt(newValue.toString()));
|
||||
editor.putInt(Prefs.UPLOADS_SHOWING, value);
|
||||
editor.putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED,true);
|
||||
uploadLimit.setSummary(newValue.toString());
|
||||
uploadLimit.setSummary(String.valueOf(value));
|
||||
}
|
||||
editor.apply();
|
||||
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.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
|
||||
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;
|
||||
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
if(Utils.isDarkTheme(this)){
|
||||
boolean currentThemeIsDark = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme", false);
|
||||
if (currentThemeIsDark){
|
||||
currentTheme = true;
|
||||
setTheme(R.style.DarkAppTheme);
|
||||
} else {
|
||||
|
|
@ -27,8 +26,8 @@ public class BaseActivity extends AppCompatActivity {
|
|||
@Override
|
||||
protected void onResume() {
|
||||
// Restart activity if theme is changed
|
||||
boolean newTheme = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme",false);
|
||||
if(currentTheme!=newTheme){ //is activity theme changed
|
||||
boolean newTheme = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme", false);
|
||||
if (currentTheme != newTheme) { //is activity theme changed
|
||||
Intent intent = getIntent();
|
||||
finish();
|
||||
startActivity(intent);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
package fr.free.nrw.commons.theme;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountManager;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.support.annotation.NonNull;
|
||||
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.widget.Toolbar;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import butterknife.BindView;
|
||||
|
|
@ -18,9 +23,11 @@ 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.auth.AccountUtil;
|
||||
import fr.free.nrw.commons.auth.LoginActivity;
|
||||
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 timber.log.Timber;
|
||||
|
||||
|
|
@ -47,6 +54,22 @@ public abstract class NavigationBaseActivity extends BaseActivity
|
|||
toggle.setDrawerIndicatorEnabled(true);
|
||||
toggle.syncState();
|
||||
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() {
|
||||
|
|
@ -70,30 +93,25 @@ public abstract class NavigationBaseActivity extends BaseActivity
|
|||
|
||||
@Override
|
||||
public boolean onNavigationItemSelected(@NonNull final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
final int itemId = item.getItemId();
|
||||
switch (itemId) {
|
||||
case R.id.action_home:
|
||||
drawerLayout.closeDrawer(navigationView);
|
||||
if (!(this instanceof ContributionsActivity)) {
|
||||
ContributionsActivity.startYourself(this);
|
||||
}
|
||||
startActivityWithFlags(
|
||||
this, ContributionsActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP,
|
||||
Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
return true;
|
||||
case R.id.action_nearby:
|
||||
drawerLayout.closeDrawer(navigationView);
|
||||
if (!(this instanceof NearbyActivity)) {
|
||||
NearbyActivity.startYourself(this);
|
||||
}
|
||||
startActivityWithFlags(this, NearbyActivity.class, Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
|
||||
return true;
|
||||
case R.id.action_about:
|
||||
drawerLayout.closeDrawer(navigationView);
|
||||
if (!(this instanceof AboutActivity)) {
|
||||
AboutActivity.startYourself(this);
|
||||
}
|
||||
startActivityWithFlags(this, AboutActivity.class, Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
|
||||
return true;
|
||||
case R.id.action_settings:
|
||||
drawerLayout.closeDrawer(navigationView);
|
||||
if (!(this instanceof SettingsActivity)) {
|
||||
SettingsActivity.startYourself(this);
|
||||
}
|
||||
startActivityWithFlags(this, SettingsActivity.class, Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
|
||||
return true;
|
||||
case R.id.action_introduction:
|
||||
drawerLayout.closeDrawer(navigationView);
|
||||
|
|
@ -126,7 +144,12 @@ public abstract class NavigationBaseActivity extends BaseActivity
|
|||
.setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel())
|
||||
.show();
|
||||
return true;
|
||||
case R.id.action_notifications:
|
||||
drawerLayout.closeDrawer(navigationView);
|
||||
NotificationActivity.startYourself(this);
|
||||
return true;
|
||||
default:
|
||||
Timber.e("Unknown option [%s] selected from the navigation menu", itemId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -143,4 +166,12 @@ public abstract class NavigationBaseActivity extends BaseActivity
|
|||
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,73 +1,100 @@
|
|||
package fr.free.nrw.commons.ui.widget;
|
||||
|
||||
/**
|
||||
* Created by mikel on 07/08/2017.
|
||||
*/
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v7.widget.AppCompatDrawableManager;
|
||||
import android.support.v7.widget.AppCompatTextView;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.utils.UiUtils;
|
||||
|
||||
public class CompatTextView extends AppCompatTextView {
|
||||
public CompatTextView(Context context) {
|
||||
super(context);
|
||||
init(null);
|
||||
}
|
||||
|
||||
public CompatTextView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(attrs);
|
||||
}
|
||||
|
||||
public CompatTextView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init(attrs);
|
||||
}
|
||||
|
||||
private void init(@Nullable AttributeSet attrs) {
|
||||
if (attrs != null) {
|
||||
Context context = getContext();
|
||||
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CompatTextView);
|
||||
|
||||
// Obtain DrawableManager used to pull Drawables safely, and check if we're in RTL
|
||||
AppCompatDrawableManager dm = AppCompatDrawableManager.get();
|
||||
boolean rtl = ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL;
|
||||
|
||||
// Grab the compat drawable padding from the XML
|
||||
float drawablePadding = a.getDimension(R.styleable.CompatTextView_drawablePadding, 0);
|
||||
|
||||
// Grab the compat drawable resources from the XML
|
||||
int startDrawableRes = a.getResourceId(R.styleable.CompatTextView_drawableStart, 0);
|
||||
int topDrawableRes = a.getResourceId(R.styleable.CompatTextView_drawableTop, 0);
|
||||
int endDrawableRes = a.getResourceId(R.styleable.CompatTextView_drawableEnd, 0);
|
||||
int bottomDrawableRes = a.getResourceId(R.styleable.CompatTextView_drawableBottom, 0);
|
||||
|
||||
// Load the used drawables, fall back to whatever was set in an "android:"
|
||||
Drawable[] currentDrawables = getCompoundDrawables();
|
||||
Drawable left = startDrawableRes != 0
|
||||
? dm.getDrawable(context, startDrawableRes) : currentDrawables[0];
|
||||
Drawable right = endDrawableRes != 0
|
||||
? dm.getDrawable(context, endDrawableRes) : currentDrawables[1];
|
||||
Drawable top = topDrawableRes != 0
|
||||
? dm.getDrawable(context, topDrawableRes) : currentDrawables[2];
|
||||
Drawable bottom = bottomDrawableRes != 0
|
||||
? dm.getDrawable(context, bottomDrawableRes) : currentDrawables[3];
|
||||
|
||||
// Account for RTL and apply the compound Drawables
|
||||
Drawable start = rtl ? right : left;
|
||||
Drawable end = rtl ? left : right;
|
||||
setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom);
|
||||
setCompoundDrawablePadding((int) UiUtils.convertDpToPixel(drawablePadding, getContext()));
|
||||
|
||||
a.recycle();
|
||||
}
|
||||
}
|
||||
}
|
||||
package fr.free.nrw.commons.ui.widget;
|
||||
|
||||
/*
|
||||
*Created by mikel on 07/08/2017.
|
||||
*/
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v7.widget.AppCompatDrawableManager;
|
||||
import android.support.v7.widget.AppCompatTextView;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.utils.UiUtils;
|
||||
|
||||
/**
|
||||
* a text view compatible with older versions of the platform
|
||||
*/
|
||||
public class CompatTextView extends AppCompatTextView {
|
||||
|
||||
/**
|
||||
* Constructs a new instance of CompatTextView
|
||||
*
|
||||
* @param context the view context
|
||||
*/
|
||||
public CompatTextView(Context context) {
|
||||
super(context);
|
||||
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) {
|
||||
super(context, attrs);
|
||||
init(attrs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new instance of CompatTextView
|
||||
*
|
||||
* @param context
|
||||
* @param attrs
|
||||
* @param defStyleAttr
|
||||
*/
|
||||
public CompatTextView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init(attrs);
|
||||
}
|
||||
|
||||
/**
|
||||
* initializes the view
|
||||
*
|
||||
* @param attrs the attribute set of the view, which can be null
|
||||
*/
|
||||
private void init(@Nullable AttributeSet attrs) {
|
||||
if (attrs != null) {
|
||||
Context context = getContext();
|
||||
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CompatTextView);
|
||||
|
||||
// Obtain DrawableManager used to pull Drawables safely, and check if we're in RTL
|
||||
AppCompatDrawableManager dm = AppCompatDrawableManager.get();
|
||||
boolean rtl = ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL;
|
||||
|
||||
// Grab the compat drawable padding from the XML
|
||||
float drawablePadding = a.getDimension(R.styleable.CompatTextView_drawablePadding, 0);
|
||||
|
||||
// Grab the compat drawable resources from the XML
|
||||
int startDrawableRes = a.getResourceId(R.styleable.CompatTextView_drawableStart, 0);
|
||||
int topDrawableRes = a.getResourceId(R.styleable.CompatTextView_drawableTop, 0);
|
||||
int endDrawableRes = a.getResourceId(R.styleable.CompatTextView_drawableEnd, 0);
|
||||
int bottomDrawableRes = a.getResourceId(R.styleable.CompatTextView_drawableBottom, 0);
|
||||
|
||||
// Load the used drawables, fall back to whatever was set in an "android:"
|
||||
Drawable[] currentDrawables = getCompoundDrawables();
|
||||
Drawable left = startDrawableRes != 0
|
||||
? dm.getDrawable(context, startDrawableRes) : currentDrawables[0];
|
||||
Drawable right = endDrawableRes != 0
|
||||
? dm.getDrawable(context, endDrawableRes) : currentDrawables[1];
|
||||
Drawable top = topDrawableRes != 0
|
||||
? dm.getDrawable(context, topDrawableRes) : currentDrawables[2];
|
||||
Drawable bottom = bottomDrawableRes != 0
|
||||
? dm.getDrawable(context, bottomDrawableRes) : currentDrawables[3];
|
||||
|
||||
// Account for RTL and apply the compound Drawables
|
||||
Drawable start = rtl ? right : left;
|
||||
Drawable end = rtl ? left : right;
|
||||
setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom);
|
||||
setCompoundDrawablePadding((int) UiUtils.convertDpToPixel(drawablePadding, getContext()));
|
||||
|
||||
a.recycle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +1,51 @@
|
|||
package fr.free.nrw.commons.ui.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.v7.widget.AppCompatTextView;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import fr.free.nrw.commons.Utils;
|
||||
|
||||
/**
|
||||
* An {@link AppCompatTextView} which formats the text to HTML displayable text and makes any
|
||||
* links clickable.
|
||||
*/
|
||||
public class HtmlTextView extends AppCompatTextView {
|
||||
|
||||
public HtmlTextView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
setMovementMethod(LinkMovementMethod.getInstance());
|
||||
setText(Utils.fromHtml(getText().toString()));
|
||||
}
|
||||
|
||||
public void setHtmlText(String newText) {
|
||||
setText(Utils.fromHtml(newText));
|
||||
}
|
||||
}
|
||||
package fr.free.nrw.commons.ui.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.support.v7.widget.AppCompatTextView;
|
||||
import android.text.Html;
|
||||
import android.text.Spanned;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
/**
|
||||
* An {@link AppCompatTextView} which formats the text to HTML displayable text and makes any
|
||||
* links clickable.
|
||||
*/
|
||||
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) {
|
||||
super(context, attrs);
|
||||
|
||||
setMovementMethod(LinkMovementMethod.getInstance());
|
||||
setText(fromHtml(getText().toString()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the text to be displayed
|
||||
* @param newText the text to be displayed
|
||||
*/
|
||||
public void setHtmlText(String 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