mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 20:33:53 +01:00
Merge branch 'master' into Bug#954
This commit is contained in:
commit
2eab64eb7a
359 changed files with 9275 additions and 3359 deletions
|
|
@ -35,7 +35,7 @@ before_script:
|
||||||
- android-wait-for-emulator
|
- android-wait-for-emulator
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- ./gradlew clean check connectedCheck jacocoTestReport --stacktrace
|
- ./gradlew clean check connectedCheck jacocoTestReport
|
||||||
|
|
||||||
after_success:
|
after_success:
|
||||||
- bash <(curl -s https://codecov.io/bash)
|
- bash <(curl -s https://codecov.io/bash)
|
||||||
|
|
|
||||||
29
CHANGELOG.md
29
CHANGELOG.md
|
|
@ -1,5 +1,34 @@
|
||||||
# Wikimedia Commons for Android
|
# Wikimedia Commons for Android
|
||||||
|
|
||||||
|
## v2.6.7
|
||||||
|
- Added null checks to prevent frequent crashes in ModificationsSyncAdapter
|
||||||
|
|
||||||
|
## v2.6.6
|
||||||
|
- Refactored Dagger to fix crashes encountered in production
|
||||||
|
- Fixed "?" displaying in description of Nearby places
|
||||||
|
- Database-related cleanup and tests
|
||||||
|
- Optimized dimens.xml
|
||||||
|
- Fixed issue where map opens with incorrect coordinates
|
||||||
|
|
||||||
|
## v2.6.5 beta
|
||||||
|
- Changed "send log" feature to only send logs to private Google group forum
|
||||||
|
- Switched to using Wikimedia maps server instead of Mapbox for privacy reasons
|
||||||
|
- Removed event logging from app for privacy reasons
|
||||||
|
- Fixed crash caused by rapidly switching from Nearby map to list while loading
|
||||||
|
|
||||||
|
## v2.6.4 beta
|
||||||
|
- Excluded httpclient and commons-logging to fix release build errors
|
||||||
|
- Fixed crashes caused by Fresco and Dagger
|
||||||
|
|
||||||
|
## v2.6.3 beta
|
||||||
|
- Same as 2.6.2 except with localizations added for Google Code-In
|
||||||
|
|
||||||
|
## v2.6.2 beta
|
||||||
|
- Reverted temporarily to last stable version while working on crash fix
|
||||||
|
|
||||||
|
## v2.6.1 beta
|
||||||
|
- Failed attempt to fix crashes in release build with the previous beta release
|
||||||
|
|
||||||
## v2.6.0 beta
|
## v2.6.0 beta
|
||||||
- Multiple bugfixes for location updates and list/map loading in Nearby
|
- Multiple bugfixes for location updates and list/map loading in Nearby
|
||||||
- Multiple fixes for various crashes and memory leaks
|
- Multiple fixes for various crashes and memory leaks
|
||||||
|
|
|
||||||
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
|
||||||
950
CREDITS
950
CREDITS
|
|
@ -39,3 +39,953 @@ their contribution to the product.
|
||||||
|
|
||||||
3rd party open source apps from which significant code has been reused:
|
3rd party open source apps from which significant code has been reused:
|
||||||
* Android Wikipedia app https://github.com/wikimedia/apps-android-wikipedia
|
* Android Wikipedia app https://github.com/wikimedia/apps-android-wikipedia
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
The Wikimedia Commons Android app uses portions of MapBox.
|
||||||
|
|
||||||
|
mapbox-gl-native copyright (c) 2014-2018 Mapbox.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in
|
||||||
|
the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
||||||
|
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||||
|
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||||
|
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of Android Gesture Detectors Framework.
|
||||||
|
|
||||||
|
Copyright (c) 2012, Almer Thie
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||||
|
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
|
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of Android Support Library.
|
||||||
|
|
||||||
|
Copyright (c) 2005-2013, The Android Open Source Project
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of Boost.
|
||||||
|
|
||||||
|
Distributed under the Boost Software License, Version 1.0.
|
||||||
|
|
||||||
|
http://www.boost.org/LICENSE_1_0.txt
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of Clipper.
|
||||||
|
|
||||||
|
Author : Angus Johnson
|
||||||
|
Version : 6.1.3a
|
||||||
|
Date : 22 January 2014
|
||||||
|
Website : http://www.angusj.com
|
||||||
|
Copyright : Angus Johnson 2010-2014
|
||||||
|
|
||||||
|
License:
|
||||||
|
Use, modification & distribution is subject to Boost Software License Ver 1.
|
||||||
|
http://www.boost.org/LICENSE_1_0.txt
|
||||||
|
|
||||||
|
Attributions:
|
||||||
|
The code in this library is an extension of Bala Vatti's clipping algorithm:
|
||||||
|
"A generic solution to polygon clipping"
|
||||||
|
Communications of the ACM, Vol 35, Issue 7 (July 1992) pp 56-63.
|
||||||
|
http://portal.acm.org/citation.cfm?id=129906
|
||||||
|
|
||||||
|
Computer graphics and geometric modeling: implementation and algorithms
|
||||||
|
By Max K. Agoston
|
||||||
|
Springer; 1 edition (January 4, 2005)
|
||||||
|
http://books.google.com/books?q=vatti+clipping+agoston
|
||||||
|
|
||||||
|
See also:
|
||||||
|
"Polygon Offsetting by Computing Winding Numbers"
|
||||||
|
Paper no. DETC2005-85513 pp. 565-575
|
||||||
|
ASME 2005 International Design Engineering Technical Conferences
|
||||||
|
and Computers and Information in Engineering Conference (IDETC/CIE2005)
|
||||||
|
September 24-28, 2005 , Long Beach, California, USA
|
||||||
|
http://www.me.berkeley.edu/~mcmains/pubs/DAC05OffsetPolygon.pdf
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of BugshotKit.
|
||||||
|
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 marcoarment
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of CSS Color Parser.
|
||||||
|
|
||||||
|
(c) Dean McNamee <dean@gmail.com>, 2012.
|
||||||
|
C++ port by Konstantin Käfer <mail@kkaefer.com>, 2014.
|
||||||
|
|
||||||
|
https://github.com/deanm/css-color-parser-js
|
||||||
|
https://github.com/kkaefer/css-color-parser-cpp
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to
|
||||||
|
deal in the Software without restriction, including without limitation the
|
||||||
|
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||||
|
sell copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||||
|
IN THE SOFTWARE.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of GLFW.
|
||||||
|
|
||||||
|
Copyright (c) 2002-2006 Marcus Geelnard
|
||||||
|
Copyright (c) 2006-2010 Camilla Berglund <elmindreda@elmindreda.org>
|
||||||
|
|
||||||
|
This software is provided 'as-is', without any express or implied
|
||||||
|
warranty. In no event will the authors be held liable for any damages
|
||||||
|
arising from the use of this software.
|
||||||
|
|
||||||
|
Permission is granted to anyone to use this software for any purpose,
|
||||||
|
including commercial applications, and to alter it and redistribute it
|
||||||
|
freely, subject to the following restrictions:
|
||||||
|
|
||||||
|
1. The origin of this software must not be misrepresented; you must not
|
||||||
|
claim that you wrote the original software. If you use this software
|
||||||
|
in a product, an acknowledgment in the product documentation would
|
||||||
|
be appreciated but is not required.
|
||||||
|
|
||||||
|
2. Altered source versions must be plainly marked as such, and must not
|
||||||
|
be misrepresented as being the original software.
|
||||||
|
|
||||||
|
3. This notice may not be removed or altered from any source
|
||||||
|
distribution.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of libc++.
|
||||||
|
|
||||||
|
The libc++ library is dual licensed under both the University of Illinois
|
||||||
|
"BSD-Like" license and the MIT license. As a user of this code you may choose
|
||||||
|
to use it under either license. As a contributor, you agree to allow your code
|
||||||
|
to be used under both.
|
||||||
|
|
||||||
|
Full text of the relevant licenses is included below.
|
||||||
|
|
||||||
|
====
|
||||||
|
|
||||||
|
University of Illinois/NCSA
|
||||||
|
Open Source License
|
||||||
|
|
||||||
|
Copyright (c) 2009-2015 by the contributors listed in CREDITS.TXT
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Developed by:
|
||||||
|
|
||||||
|
LLVM Team
|
||||||
|
|
||||||
|
University of Illinois at Urbana-Champaign
|
||||||
|
|
||||||
|
http://llvm.org
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal with
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
so, subject to the following conditions:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimers.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimers in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
* Neither the names of the LLVM Team, University of Illinois at
|
||||||
|
Urbana-Champaign, nor the names of its contributors may be used to
|
||||||
|
endorse or promote products derived from this Software without specific
|
||||||
|
prior written permission.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
====
|
||||||
|
|
||||||
|
Copyright (c) 2009-2014 by the contributors listed in CREDITS.TXT
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of libcurl.
|
||||||
|
|
||||||
|
COPYRIGHT AND PERMISSION NOTICE
|
||||||
|
|
||||||
|
Copyright (c) 1996 - 2015, Daniel Stenberg, <daniel@haxx.se>.
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and distribute this software for any purpose
|
||||||
|
with or without fee is hereby granted, provided that the above copyright
|
||||||
|
notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. IN
|
||||||
|
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||||
|
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
||||||
|
OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
Except as contained in this notice, the name of a copyright holder shall not
|
||||||
|
be used in advertising or otherwise to promote the sale, use or other dealings
|
||||||
|
in this Software without prior written authorization of the copyright holder.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of libjpeg-turbo.
|
||||||
|
|
||||||
|
This software is based in part on the work of the Independent JPEG Group.
|
||||||
|
|
||||||
|
Copyright (C)2009-2015 D. R. Commander. All Rights Reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
- Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer.
|
||||||
|
- Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
- Neither the name of the libjpeg-turbo Project nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from this
|
||||||
|
software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS",
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
|
||||||
|
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
TurboJPEG/LJT: this implements the TurboJPEG API using libjpeg or libjpeg-turbo
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of libpng.
|
||||||
|
|
||||||
|
This copy of the libpng notices is provided for your convenience. In case of
|
||||||
|
any discrepancy between this copy and the notices in the file png.h that is
|
||||||
|
included in the libpng distribution, the latter shall prevail.
|
||||||
|
|
||||||
|
COPYRIGHT NOTICE, DISCLAIMER, and LICENSE:
|
||||||
|
|
||||||
|
If you modify libpng you may insert additional notices immediately following
|
||||||
|
this sentence.
|
||||||
|
|
||||||
|
This code is released under the libpng license.
|
||||||
|
|
||||||
|
libpng versions 1.0.7, July 1, 2000, through 1.6.18, July 23, 2015, are
|
||||||
|
Copyright (c) 2000-2002, 2004, 2006-2015 Glenn Randers-Pehrson, and are
|
||||||
|
distributed according to the same disclaimer and license as libpng-1.0.6
|
||||||
|
with the following individuals added to the list of Contributing Authors:
|
||||||
|
|
||||||
|
Simon-Pierre Cadieux
|
||||||
|
Eric S. Raymond
|
||||||
|
Mans Rullgard
|
||||||
|
Cosmin Truta
|
||||||
|
Gilles Vollant
|
||||||
|
James Yu
|
||||||
|
|
||||||
|
and with the following additions to the disclaimer:
|
||||||
|
|
||||||
|
There is no warranty against interference with your enjoyment of the
|
||||||
|
library or against infringement. There is no warranty that our
|
||||||
|
efforts or the library will fulfill any of your particular purposes
|
||||||
|
or needs. This library is provided with all faults, and the entire
|
||||||
|
risk of satisfactory quality, performance, accuracy, and effort is with
|
||||||
|
the user.
|
||||||
|
|
||||||
|
libpng versions 0.97, January 1998, through 1.0.6, March 20, 2000, are
|
||||||
|
Copyright (c) 1998-2000 Glenn Randers-Pehrson, and are distributed according
|
||||||
|
to the same disclaimer and license as libpng-0.96, with the following
|
||||||
|
individuals added to the list of Contributing Authors:
|
||||||
|
|
||||||
|
Tom Lane
|
||||||
|
Glenn Randers-Pehrson
|
||||||
|
Willem van Schaik
|
||||||
|
|
||||||
|
libpng versions 0.89, June 1996, through 0.96, May 1997, are
|
||||||
|
Copyright (c) 1996-1997 Andreas Dilger, and are
|
||||||
|
distributed according to the same disclaimer and license as libpng-0.88,
|
||||||
|
with the following individuals added to the list of Contributing Authors:
|
||||||
|
|
||||||
|
John Bowler
|
||||||
|
Kevin Bracey
|
||||||
|
Sam Bushell
|
||||||
|
Magnus Holmgren
|
||||||
|
Greg Roelofs
|
||||||
|
Tom Tanner
|
||||||
|
|
||||||
|
libpng versions 0.5, May 1995, through 0.88, January 1996, are
|
||||||
|
Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc.
|
||||||
|
|
||||||
|
For the purposes of this copyright and license, "Contributing Authors"
|
||||||
|
is defined as the following set of individuals:
|
||||||
|
|
||||||
|
Andreas Dilger
|
||||||
|
Dave Martindale
|
||||||
|
Guy Eric Schalnat
|
||||||
|
Paul Schmidt
|
||||||
|
Tim Wegner
|
||||||
|
|
||||||
|
The PNG Reference Library is supplied "AS IS". The Contributing Authors
|
||||||
|
and Group 42, Inc. disclaim all warranties, expressed or implied,
|
||||||
|
including, without limitation, the warranties of merchantability and of
|
||||||
|
fitness for any purpose. The Contributing Authors and Group 42, Inc.
|
||||||
|
assume no liability for direct, indirect, incidental, special, exemplary,
|
||||||
|
or consequential damages, which may result from the use of the PNG
|
||||||
|
Reference Library, even if advised of the possibility of such damage.
|
||||||
|
|
||||||
|
Permission is hereby granted to use, copy, modify, and distribute this
|
||||||
|
source code, or portions hereof, for any purpose, without fee, subject
|
||||||
|
to the following restrictions:
|
||||||
|
|
||||||
|
1. The origin of this source code must not be misrepresented.
|
||||||
|
|
||||||
|
2. Altered versions must be plainly marked as such and must not
|
||||||
|
be misrepresented as being the original source.
|
||||||
|
|
||||||
|
3. This Copyright notice may not be removed or altered from any
|
||||||
|
source or altered source distribution.
|
||||||
|
|
||||||
|
The Contributing Authors and Group 42, Inc. specifically permit, without
|
||||||
|
fee, and encourage the use of this source code as a component to
|
||||||
|
supporting the PNG file format in commercial products. If you use this
|
||||||
|
source code in a product, acknowledgment is not required but would be
|
||||||
|
appreciated.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of libuv.
|
||||||
|
|
||||||
|
libuv is part of the Node project: http://nodejs.org/
|
||||||
|
libuv may be distributed alone under Node's license:
|
||||||
|
|
||||||
|
====
|
||||||
|
|
||||||
|
Copyright Joyent, Inc. and other Node contributors. All rights reserved.
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to
|
||||||
|
deal in the Software without restriction, including without limitation the
|
||||||
|
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||||
|
sell copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||||
|
IN THE SOFTWARE.
|
||||||
|
|
||||||
|
====
|
||||||
|
|
||||||
|
This license applies to all parts of libuv that are not externally
|
||||||
|
maintained libraries.
|
||||||
|
|
||||||
|
The externally maintained libraries used by libuv are:
|
||||||
|
|
||||||
|
- tree.h (from FreeBSD), copyright Niels Provos. Two clause BSD license.
|
||||||
|
|
||||||
|
- inet_pton and inet_ntop implementations, contained in src/inet.c, are
|
||||||
|
copyright the Internet Systems Consortium, Inc., and licensed under the ISC
|
||||||
|
license.
|
||||||
|
|
||||||
|
- stdint-msvc2008.h (from msinttypes), copyright Alexander Chemeris. Three
|
||||||
|
clause BSD license.
|
||||||
|
|
||||||
|
- pthread-fixes.h, pthread-fixes.c, copyright Google Inc. and Sony Mobile
|
||||||
|
Communications AB. Three clause BSD license.
|
||||||
|
|
||||||
|
- android-ifaddrs.h, android-ifaddrs.c, copyright Berkeley Software Design
|
||||||
|
Inc, Kenneth MacKay and Emergya (Cloud4all, FP7/2007-2013, grant agreement
|
||||||
|
n° 289016). Three clause BSD license.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of libzip.
|
||||||
|
|
||||||
|
Copyright (C) 1999-2014 Dieter Baron and Thomas Klausner
|
||||||
|
|
||||||
|
The authors can be contacted at <libzip@nih.at>
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in
|
||||||
|
the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
|
||||||
|
3. The names of the authors may not be used to endorse or promote
|
||||||
|
products derived from this software without specific prior
|
||||||
|
written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS
|
||||||
|
OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY
|
||||||
|
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
||||||
|
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||||
|
IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
||||||
|
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
|
||||||
|
IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of LOST.
|
||||||
|
|
||||||
|
Copyright (c) 2014 Mapzen
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of the Mapbox iOS SDK, which was derived from the
|
||||||
|
Route-Me open source project, including the Alpstein fork of it.
|
||||||
|
|
||||||
|
The Route-Me license appears below.
|
||||||
|
|
||||||
|
Copyright (c) 2008-2013, Route-Me Contributors
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||||
|
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of nunicode.
|
||||||
|
|
||||||
|
Copyright (c) 2013 Aleksey Tulinov <aleksey.tulinov@gmail.com>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of OkHTTP.
|
||||||
|
|
||||||
|
Copyright 2014 Square, Inc.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of OpenSSL.
|
||||||
|
|
||||||
|
LICENSE ISSUES
|
||||||
|
==============
|
||||||
|
|
||||||
|
The OpenSSL toolkit stays under a dual license, i.e. both the conditions of
|
||||||
|
the OpenSSL License and the original SSLeay license apply to the toolkit.
|
||||||
|
See below for the actual license texts. Actually both licenses are BSD-style
|
||||||
|
Open Source licenses. In case of any license issues related to OpenSSL
|
||||||
|
please contact openssl-core@openssl.org.
|
||||||
|
|
||||||
|
OpenSSL License
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Copyright (c) 1998-2011 The OpenSSL Project. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in
|
||||||
|
the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
|
||||||
|
3. All advertising materials mentioning features or use of this
|
||||||
|
software must display the following acknowledgment:
|
||||||
|
"This product includes software developed by the OpenSSL Project
|
||||||
|
for use in the OpenSSL Toolkit. (http://www.openssl.org/)"
|
||||||
|
|
||||||
|
4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to
|
||||||
|
endorse or promote products derived from this software without
|
||||||
|
prior written permission. For written permission, please contact
|
||||||
|
openssl-core@openssl.org.
|
||||||
|
|
||||||
|
5. Products derived from this software may not be called "OpenSSL"
|
||||||
|
nor may "OpenSSL" appear in their names without prior written
|
||||||
|
permission of the OpenSSL Project.
|
||||||
|
|
||||||
|
6. Redistributions of any form whatsoever must retain the following
|
||||||
|
acknowledgment:
|
||||||
|
"This product includes software developed by the OpenSSL Project
|
||||||
|
for use in the OpenSSL Toolkit (http://www.openssl.org/)"
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY
|
||||||
|
EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR
|
||||||
|
ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||||
|
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||||
|
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||||
|
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
|
||||||
|
OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
This product includes cryptographic software written by Eric Young
|
||||||
|
(eay@cryptsoft.com). This product includes software written by Tim
|
||||||
|
Hudson (tjh@cryptsoft.com).
|
||||||
|
|
||||||
|
Original SSLeay License
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com)
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This package is an SSL implementation written
|
||||||
|
by Eric Young (eay@cryptsoft.com).
|
||||||
|
The implementation was written so as to conform with Netscapes SSL.
|
||||||
|
|
||||||
|
This library is free for commercial and non-commercial use as long as
|
||||||
|
The following conditions are aheared to. The following conditions
|
||||||
|
apply to all code found in this distribution, be it the RC4, RSA,
|
||||||
|
lhash, DES, etc., code; not just the SSL code. The SSL documentation
|
||||||
|
included with this distribution is covered by the same copyright terms
|
||||||
|
except that the holder is Tim Hudson (tjh@cryptsoft.com).
|
||||||
|
|
||||||
|
Copyright remains Eric Young's, and as such any Copyright notices in
|
||||||
|
the code are not to be removed.
|
||||||
|
If this package is used in a product, Eric Young should be given attribution
|
||||||
|
as the author of the parts of the library used.
|
||||||
|
This can be in the form of a textual message at program startup or
|
||||||
|
in documentation (online or textual) provided with the package.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
1. Redistributions of source code must retain the copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
3. All advertising materials mentioning features or use of this software
|
||||||
|
must display the following acknowledgement:
|
||||||
|
"This product includes cryptographic software written by
|
||||||
|
Eric Young (eay@cryptsoft.com)"
|
||||||
|
The word 'cryptographic' can be left out if the rouines from the library
|
||||||
|
being used are not cryptographic related :-).
|
||||||
|
4. If you include any Windows specific code (or a derivative thereof) from
|
||||||
|
the apps directory (application code) you must include an acknowledgement:
|
||||||
|
"This product includes software written by Tim Hudson (tjh@cryptsoft.com)"
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||||
|
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||||
|
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||||
|
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||||
|
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGE.
|
||||||
|
|
||||||
|
The licence and distribution terms for any publically available version or
|
||||||
|
derivative of this code cannot be changed. i.e. this code cannot simply be
|
||||||
|
copied and put under another distribution licence
|
||||||
|
[including the GNU Public Licence.]
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of RapidJSON.
|
||||||
|
|
||||||
|
Tencent is pleased to support the open source community by making RapidJSON
|
||||||
|
available.
|
||||||
|
|
||||||
|
Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. All rights
|
||||||
|
reserved.
|
||||||
|
|
||||||
|
If you have downloaded a copy of the RapidJSON binary from Tencent, please note
|
||||||
|
that the RapidJSON binary is licensed under the MIT License. If you have
|
||||||
|
downloaded a copy of the RapidJSON source code from Tencent, please note that
|
||||||
|
RapidJSON source code is licensed under the MIT License, except for the third-
|
||||||
|
party components listed below which are subject to different license terms.
|
||||||
|
Your integration of RapidJSON into your own projects may require compliance with
|
||||||
|
the MIT License, as well as the other licenses applicable to the third-party
|
||||||
|
components included within RapidJSON. To avoid the problematic JSON license in
|
||||||
|
your own projects, it's sufficient to exclude the bin/jsonchecker/ directory, as
|
||||||
|
it's the only code under the JSON license. A copy of the MIT License is included
|
||||||
|
in this file.
|
||||||
|
|
||||||
|
Other dependencies and licenses:
|
||||||
|
|
||||||
|
Open Source Software Licensed Under the BSD License:
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
|
||||||
|
The msinttypes r29
|
||||||
|
Copyright (c) 2006-2013 Alexander Chemeris
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
* Neither the name of copyright holder nor the names of its contributors may be
|
||||||
|
used to endorse or promote products derived from this software without
|
||||||
|
specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
|
||||||
|
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
|
||||||
|
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
|
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
Open Source Software Licensed Under the JSON License:
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
|
||||||
|
json.org
|
||||||
|
Copyright (c) 2002 JSON.org
|
||||||
|
All Rights Reserved.
|
||||||
|
|
||||||
|
JSON_checker
|
||||||
|
Copyright (c) 2002 JSON.org
|
||||||
|
All Rights Reserved.
|
||||||
|
|
||||||
|
Terms of the JSON License:
|
||||||
|
---------------------------------------------------
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
The Software shall be used for Good, not Evil.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
Terms of the MIT License:
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of Reachability.
|
||||||
|
|
||||||
|
Copyright (c) 2011, Tony Million.
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of SQLite.
|
||||||
|
|
||||||
|
2001 September 15
|
||||||
|
|
||||||
|
The author disclaims copyright to this source code. In place of
|
||||||
|
a legal notice, here is a blessing:
|
||||||
|
|
||||||
|
May you do good and not evil.
|
||||||
|
May you find forgiveness for yourself and forgive others.
|
||||||
|
May you share freely, never taking more than you give.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of SVPulsingAnnotationView.
|
||||||
|
|
||||||
|
Copyright (c) 2013, Sam Vermette <hello@samvermette.com>
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any purpose
|
||||||
|
with or without fee is hereby granted, provided that the above copyright notice
|
||||||
|
and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||||
|
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
||||||
|
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||||
|
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||||
|
THIS SOFTWARE.
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of zlib.
|
||||||
|
|
||||||
|
Acknowledgments:
|
||||||
|
|
||||||
|
The deflate format used by zlib was defined by Phil Katz. The deflate and
|
||||||
|
zlib specifications were written by L. Peter Deutsch. Thanks to all the
|
||||||
|
people who reported problems and suggested various improvements in zlib; they
|
||||||
|
are too numerous to cite here.
|
||||||
|
|
||||||
|
Copyright notice:
|
||||||
|
|
||||||
|
(C) 1995-2013 Jean-loup Gailly and Mark Adler
|
||||||
|
|
||||||
|
This software is provided 'as-is', without any express or implied
|
||||||
|
warranty. In no event will the authors be held liable for any damages
|
||||||
|
arising from the use of this software.
|
||||||
|
|
||||||
|
Permission is granted to anyone to use this software for any purpose,
|
||||||
|
including commercial applications, and to alter it and redistribute it
|
||||||
|
freely, subject to the following restrictions:
|
||||||
|
|
||||||
|
1. The origin of this software must not be misrepresented; you must not
|
||||||
|
claim that you wrote the original software. If you use this software
|
||||||
|
in a product, an acknowledgment in the product documentation would be
|
||||||
|
appreciated but is not required.
|
||||||
|
2. Altered source versions must be plainly marked as such, and must not be
|
||||||
|
misrepresented as being the original software.
|
||||||
|
3. This notice may not be removed or altered from any source distribution.
|
||||||
|
|
||||||
|
Jean-loup Gailly Mark Adler
|
||||||
|
jloup@gzip.org madler@alumni.caltech.edu
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Mapbox GL uses portions of Realm Objective-C.
|
||||||
|
|
||||||
|
Copyright 2015 Realm Inc.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
|
|
||||||
33
ISSUE_TEMPLATE.md
Normal file
33
ISSUE_TEMPLATE.md
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
**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.
|
||||||
|
|
@ -18,16 +18,16 @@ dependencies {
|
||||||
implementation 'com.google.code.gson:gson:2.8.1'
|
implementation 'com.google.code.gson:gson:2.8.1'
|
||||||
implementation 'com.jakewharton.timber:timber:4.5.1'
|
implementation 'com.jakewharton.timber:timber:4.5.1'
|
||||||
implementation 'info.debatty:java-string-similarity:0.24'
|
implementation 'info.debatty:java-string-similarity:0.24'
|
||||||
implementation ('com.mapbox.mapboxsdk:mapbox-android-sdk:5.1.0@aar'){
|
implementation ('com.mapbox.mapboxsdk:mapbox-android-sdk:5.2.1@aar'){
|
||||||
transitive=true
|
transitive=true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
implementation "com.android.support:support-v4:${project.supportLibVersion}"
|
implementation "com.android.support:support-v4:$SUPPORT_LIB_VERSION"
|
||||||
implementation "com.android.support:appcompat-v7:${project.supportLibVersion}"
|
implementation "com.android.support:appcompat-v7:$SUPPORT_LIB_VERSION"
|
||||||
implementation "com.android.support:design:${project.supportLibVersion}"
|
implementation "com.android.support:design:$SUPPORT_LIB_VERSION"
|
||||||
|
|
||||||
implementation "com.android.support:cardview-v7:${project.supportLibVersion}"
|
implementation "com.android.support:cardview-v7:$SUPPORT_LIB_VERSION"
|
||||||
|
|
||||||
implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION"
|
implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION"
|
||||||
kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
|
kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
|
||||||
|
|
@ -44,7 +44,7 @@ dependencies {
|
||||||
implementation 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.0.0'
|
implementation 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.0.0'
|
||||||
implementation 'com.jakewharton.rxbinding2:rxbinding-design:2.0.0'
|
implementation 'com.jakewharton.rxbinding2:rxbinding-design:2.0.0'
|
||||||
|
|
||||||
implementation 'com.facebook.fresco:fresco:1.3.0'
|
implementation 'com.facebook.fresco:fresco:1.5.0'
|
||||||
implementation 'com.facebook.stetho:stetho:1.5.0'
|
implementation 'com.facebook.stetho:stetho:1.5.0'
|
||||||
|
|
||||||
implementation "com.google.dagger:dagger:$DAGGER_VERSION"
|
implementation "com.google.dagger:dagger:$DAGGER_VERSION"
|
||||||
|
|
@ -62,12 +62,17 @@ dependencies {
|
||||||
|
|
||||||
testImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1'
|
testImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1'
|
||||||
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1'
|
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1'
|
||||||
androidTestImplementation "com.android.support:support-annotations:${project.supportLibVersion}"
|
androidTestImplementation "com.android.support:support-annotations:$SUPPORT_LIB_VERSION"
|
||||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
|
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
|
||||||
|
|
||||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.1'
|
debugImplementation "com.squareup.leakcanary:leakcanary-android:$LEAK_CANARY"
|
||||||
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
|
releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY"
|
||||||
testImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
|
testImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY"
|
||||||
|
|
||||||
|
implementation "com.google.dagger:dagger:$DAGGER_VERSION"
|
||||||
|
implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
|
||||||
|
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
|
||||||
|
kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
@ -78,8 +83,8 @@ android {
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId 'fr.free.nrw.commons'
|
applicationId 'fr.free.nrw.commons'
|
||||||
versionCode 76
|
versionCode 82
|
||||||
versionName '2.6.1'
|
versionName '2.6.7'
|
||||||
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
|
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
|
||||||
|
|
||||||
minSdkVersion project.minSdkVersion
|
minSdkVersion project.minSdkVersion
|
||||||
|
|
@ -89,7 +94,12 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
// use kotlin only in tests (for now)
|
||||||
test.java.srcDirs += 'src/test/kotlin'
|
test.java.srcDirs += 'src/test/kotlin'
|
||||||
|
|
||||||
|
// use main assets and resources in test
|
||||||
|
test.assets.srcDirs += 'src/main/assets'
|
||||||
|
test.resources.srcDirs += 'src/main/resoures'
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import android.support.test.runner.AndroidJUnit4;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.BuildConfig;
|
||||||
|
|
||||||
import static org.hamcrest.CoreMatchers.is;
|
import static org.hamcrest.CoreMatchers.is;
|
||||||
import static org.junit.Assert.assertThat;
|
import static org.junit.Assert.assertThat;
|
||||||
|
|
||||||
|
|
@ -14,7 +16,7 @@ import static org.junit.Assert.assertThat;
|
||||||
public class FileUtilsTest {
|
public class FileUtilsTest {
|
||||||
@Test
|
@Test
|
||||||
public void isSelfOwned() throws Exception {
|
public void isSelfOwned() throws Exception {
|
||||||
Uri uri = Uri.parse("content://fr.free.nrw.commons.provider/document/1");
|
Uri uri = Uri.parse("content://" + BuildConfig.APPLICATION_ID + ".provider/document/1");
|
||||||
boolean selfOwned = FileUtils.isSelfOwned(InstrumentationRegistry.getTargetContext(), uri);
|
boolean selfOwned = FileUtils.isSelfOwned(InstrumentationRegistry.getTargetContext(), uri);
|
||||||
assertThat(selfOwned, is(true));
|
assertThat(selfOwned, is(true));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,18 @@
|
||||||
package="fr.free.nrw.commons">
|
package="fr.free.nrw.commons">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
|
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
||||||
<uses-permission android:name="android.permission.READ_SYNC_STATS"/>
|
<uses-permission android:name="android.permission.READ_SYNC_STATS" />
|
||||||
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/>
|
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
|
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
|
||||||
<uses-permission android:name="android.permission.GET_ACCOUNTS"/>
|
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||||
<uses-permission android:name="android.permission.USE_CREDENTIALS"/>
|
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
|
||||||
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS"/>
|
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
|
||||||
<uses-permission android:name="android.permission.MANAGE_DOCUMENTS"/>
|
<uses-permission android:name="android.permission.MANAGE_DOCUMENTS" />
|
||||||
<uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS"/>
|
<uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" />
|
||||||
<uses-permission android:name="android.permission.READ_LOGS"/>
|
<uses-permission android:name="android.permission.READ_LOGS"/>
|
||||||
|
|
||||||
<!-- Needed only if your app targets Android 5.0 (API level 21) or higher. -->
|
<!-- Needed only if your app targets Android 5.0 (API level 21) or higher. -->
|
||||||
|
|
@ -31,23 +31,19 @@
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:finishOnTaskLaunch="true" />
|
android:finishOnTaskLaunch="true" />
|
||||||
|
|
||||||
<activity
|
<activity android:name=".auth.LoginActivity">
|
||||||
android:name=".auth.LoginActivity"
|
|
||||||
>
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
|
||||||
android:name=".WelcomeActivity"
|
<activity android:name=".WelcomeActivity" />
|
||||||
>
|
|
||||||
</activity>
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".upload.ShareActivity"
|
android:name=".upload.ShareActivity"
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name">
|
||||||
>
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
@ -55,11 +51,11 @@
|
||||||
<data android:mimeType="audio/ogg" />
|
<data android:mimeType="audio/ogg" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".upload.MultipleShareActivity"
|
android:name=".upload.MultipleShareActivity"
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name">
|
||||||
>
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
@ -69,33 +65,38 @@
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".contributions.ContributionsActivity"
|
android:name=".contributions.ContributionsActivity"
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name" />
|
||||||
>
|
|
||||||
</activity>
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".settings.SettingsActivity"
|
android:name=".settings.SettingsActivity"
|
||||||
android:label="@string/title_activity_settings"
|
android:label="@string/title_activity_settings" />
|
||||||
/>
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".AboutActivity"
|
android:name=".AboutActivity"
|
||||||
android:label="@string/title_activity_about"
|
android:label="@string/title_activity_about"
|
||||||
android:parentActivityName=".contributions.ContributionsActivity" />
|
android:parentActivityName=".contributions.ContributionsActivity" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".auth.SignupActivity"
|
android:name=".auth.SignupActivity"
|
||||||
android:label="@string/title_activity_signup"/>
|
android:label="@string/title_activity_signup" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".nearby.NearbyActivity"
|
android:name=".nearby.NearbyActivity"
|
||||||
android:label="@string/title_activity_nearby"
|
android:label="@string/title_activity_nearby"
|
||||||
android:parentActivityName=".contributions.ContributionsActivity" />
|
android:parentActivityName=".contributions.ContributionsActivity" />
|
||||||
|
|
||||||
<service android:name=".upload.UploadService" >
|
<activity
|
||||||
</service>
|
android:name=".notification.NotificationActivity"
|
||||||
|
android:label="@string/navigation_item_notification" />
|
||||||
|
|
||||||
|
<service android:name=".upload.UploadService" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".auth.WikiAccountAuthenticatorService"
|
android:name=".auth.WikiAccountAuthenticatorService"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:process=":auth" >
|
android:process=":auth">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.accounts.AccountAuthenticator" />
|
<action android:name="android.accounts.AccountAuthenticator" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
@ -106,27 +107,25 @@
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".contributions.ContributionsSyncService"
|
android:name=".contributions.ContributionsSyncService"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action
|
<action android:name="android.content.SyncAdapter" />
|
||||||
android:name="android.content.SyncAdapter" />
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.content.SyncAdapter"
|
android:name="android.content.SyncAdapter"
|
||||||
android:resource="@xml/contributions_sync_adapter" />
|
android:resource="@xml/contributions_sync_adapter" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".modifications.ModificationsSyncService"
|
android:name=".modifications.ModificationsSyncService"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action
|
<action android:name="android.content.SyncAdapter" />
|
||||||
android:name="android.content.SyncAdapter" />
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.content.SyncAdapter"
|
android:name="android.content.SyncAdapter"
|
||||||
android:resource="@xml/modifications_sync_adapter" />
|
android:resource="@xml/modifications_sync_adapter" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
|
|
@ -136,31 +135,29 @@
|
||||||
android:grantUriPermissions="true">
|
android:grantUriPermissions="true">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
android:resource="@xml/provider_paths"/>
|
android:resource="@xml/provider_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name=".contributions.ContributionsContentProvider"
|
android:name=".contributions.ContributionsContentProvider"
|
||||||
android:label="@string/provider_contributions"
|
android:authorities="fr.free.nrw.commons.contributions.contentprovider"
|
||||||
android:syncable="true"
|
android:exported="false"
|
||||||
android:authorities="fr.free.nrw.commons.contributions.contentprovider"
|
android:label="@string/provider_contributions"
|
||||||
android:exported="false">
|
android:syncable="true" />
|
||||||
</provider>
|
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name=".modifications.ModificationsContentProvider"
|
android:name=".modifications.ModificationsContentProvider"
|
||||||
android:label="@string/provider_modifications"
|
android:authorities="fr.free.nrw.commons.modifications.contentprovider"
|
||||||
android:syncable="true"
|
android:exported="false"
|
||||||
android:authorities="fr.free.nrw.commons.modifications.contentprovider"
|
android:label="@string/provider_modifications"
|
||||||
android:exported="false">
|
android:syncable="true" />
|
||||||
</provider>
|
|
||||||
<provider
|
<provider
|
||||||
android:name=".category.CategoryContentProvider"
|
android:name=".category.CategoryContentProvider"
|
||||||
android:label="@string/provider_categories"
|
android:authorities="fr.free.nrw.commons.categories.contentprovider"
|
||||||
android:syncable="false"
|
android:exported="false"
|
||||||
android:authorities="fr.free.nrw.commons.categories.contentprovider"
|
android:label="@string/provider_categories"
|
||||||
android:exported="false">
|
android:syncable="false" />
|
||||||
</provider>
|
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
|
|
||||||
26
app/src/main/assets/mapstyle.json
Normal file
26
app/src/main/assets/mapstyle.json
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"version": 8,
|
||||||
|
"sources": {
|
||||||
|
"wikimedia-osm": {
|
||||||
|
"type": "raster",
|
||||||
|
"tiles": [
|
||||||
|
"https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png"
|
||||||
|
],
|
||||||
|
"tileSize": 128
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"id": "background",
|
||||||
|
"type": "background",
|
||||||
|
"paint": {
|
||||||
|
"background-color": "#606060"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "osm",
|
||||||
|
"type": "raster",
|
||||||
|
"source": "wikimedia-osm"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -1,19 +1,29 @@
|
||||||
package fr.free.nrw.commons;
|
package fr.free.nrw.commons;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import butterknife.BindView;
|
import butterknife.BindView;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
|
import butterknife.OnClick;
|
||||||
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
||||||
import fr.free.nrw.commons.ui.widget.HtmlTextView;
|
import fr.free.nrw.commons.ui.widget.HtmlTextView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents about screen of this app
|
||||||
|
*/
|
||||||
public class AboutActivity extends NavigationBaseActivity {
|
public class AboutActivity extends NavigationBaseActivity {
|
||||||
@BindView(R.id.about_version) TextView versionText;
|
@BindView(R.id.about_version) TextView versionText;
|
||||||
@BindView(R.id.about_license) HtmlTextView aboutLicenseText;
|
@BindView(R.id.about_license) HtmlTextView aboutLicenseText;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method helps in the creation About screen
|
||||||
|
*
|
||||||
|
* @param savedInstanceState Data bundle
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
@ -27,4 +37,32 @@ public class AboutActivity extends NavigationBaseActivity {
|
||||||
versionText.setText(BuildConfig.VERSION_NAME);
|
versionText.setText(BuildConfig.VERSION_NAME);
|
||||||
initDrawer();
|
initDrawer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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,19 +1,9 @@
|
||||||
package fr.free.nrw.commons;
|
package fr.free.nrw.commons;
|
||||||
|
|
||||||
import android.accounts.Account;
|
|
||||||
import android.accounts.AccountManager;
|
|
||||||
import android.accounts.AccountManagerCallback;
|
|
||||||
import android.accounts.AccountManagerFuture;
|
|
||||||
import android.accounts.AuthenticatorException;
|
|
||||||
import android.accounts.OperationCanceledException;
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.support.v4.util.LruCache;
|
|
||||||
|
|
||||||
import com.facebook.drawee.backends.pipeline.Fresco;
|
import com.facebook.drawee.backends.pipeline.Fresco;
|
||||||
import com.facebook.stetho.Stetho;
|
import com.facebook.stetho.Stetho;
|
||||||
|
|
@ -25,24 +15,20 @@ import org.acra.ReportingInteractionMode;
|
||||||
import org.acra.annotation.ReportsCrashes;
|
import org.acra.annotation.ReportsCrashes;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
|
||||||
import dagger.android.AndroidInjector;
|
import fr.free.nrw.commons.auth.SessionManager;
|
||||||
import dagger.android.DispatchingAndroidInjector;
|
import fr.free.nrw.commons.category.CategoryDao;
|
||||||
import dagger.android.HasActivityInjector;
|
import fr.free.nrw.commons.contributions.ContributionDao;
|
||||||
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 fr.free.nrw.commons.data.DBOpenHelper;
|
import fr.free.nrw.commons.data.DBOpenHelper;
|
||||||
import fr.free.nrw.commons.di.DaggerAppComponent;
|
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||||
import fr.free.nrw.commons.modifications.ModifierSequence;
|
import fr.free.nrw.commons.di.CommonsApplicationComponent;
|
||||||
import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi;
|
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
|
||||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
|
||||||
import fr.free.nrw.commons.nearby.NearbyPlaces;
|
|
||||||
import fr.free.nrw.commons.utils.FileUtils;
|
import fr.free.nrw.commons.utils.FileUtils;
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.schedulers.Schedulers;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
// TODO: Use ProGuard to rip out reporting when publishing
|
// TODO: Use ProGuard to rip out reporting when publishing
|
||||||
|
|
@ -54,95 +40,45 @@ import timber.log.Timber;
|
||||||
resDialogCommentPrompt = R.string.crash_dialog_comment_prompt,
|
resDialogCommentPrompt = R.string.crash_dialog_comment_prompt,
|
||||||
resDialogOkToast = R.string.crash_dialog_ok_toast
|
resDialogOkToast = R.string.crash_dialog_ok_toast
|
||||||
)
|
)
|
||||||
public class CommonsApplication extends Application implements HasActivityInjector {
|
public class CommonsApplication extends Application {
|
||||||
|
|
||||||
private Account currentAccount = null; // Unlike a savings account...
|
@Inject SessionManager sessionManager;
|
||||||
|
@Inject DBOpenHelper dbOpenHelper;
|
||||||
|
|
||||||
public static final Object[] EVENT_UPLOAD_ATTEMPT = {"MobileAppUploadAttempts", 5334329L};
|
@Inject @Named("default_preferences") SharedPreferences defaultPrefs;
|
||||||
public static final Object[] EVENT_LOGIN_ATTEMPT = {"MobileAppLoginAttempts", 5257721L};
|
@Inject @Named("application_preferences") SharedPreferences applicationPrefs;
|
||||||
public static final Object[] EVENT_SHARE_ATTEMPT = {"MobileAppShareAttempts", 5346170L};
|
@Inject @Named("prefs") SharedPreferences otherPrefs;
|
||||||
public static final Object[] EVENT_CATEGORIZATION_ATTEMPT = {"MobileAppCategorizationAttempts", 5359208L};
|
|
||||||
|
|
||||||
public static final String DEFAULT_EDIT_SUMMARY = "Uploaded using Android Commons app";
|
public static final String DEFAULT_EDIT_SUMMARY = "Uploaded using Android Commons app";
|
||||||
|
|
||||||
public static final String FEEDBACK_EMAIL = "commons-app-android-private@googlegroups.com";
|
public static final String FEEDBACK_EMAIL = "commons-app-android@googlegroups.com";
|
||||||
|
|
||||||
|
public static final String LOGS_PRIVATE_EMAIL = "commons-app-android-private@googlegroups.com";
|
||||||
|
|
||||||
public static final String FEEDBACK_EMAIL_SUBJECT = "Commons Android App (%s) Feedback";
|
public static final String FEEDBACK_EMAIL_SUBJECT = "Commons Android App (%s) Feedback";
|
||||||
|
|
||||||
@Inject DispatchingAndroidInjector<Activity> dispatchingActivityInjector;
|
|
||||||
@Inject MediaWikiApi mediaWikiApi;
|
|
||||||
|
|
||||||
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;
|
private RefWatcher refWatcher;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This should not be called by ANY application code (other than the magic Android glue)
|
* Used to declare and initialize various components and dependencies
|
||||||
* Use CommonsApplication.getInstance() instead to get the singleton.
|
|
||||||
*/
|
*/
|
||||||
public CommonsApplication() {
|
|
||||||
CommonsApplication.instance = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static CommonsApplication getInstance() {
|
|
||||||
if (instance == null) {
|
|
||||||
instance = new CommonsApplication();
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MediaWikiApi getMWApi() {
|
|
||||||
if (api == null) {
|
|
||||||
api = new ApacheHttpClientMediaWikiApi(BuildConfig.WIKIMEDIA_API_HOST);
|
|
||||||
}
|
|
||||||
return api;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CacheController getCacheData() {
|
|
||||||
if (cacheData == null) {
|
|
||||||
cacheData = new CacheController();
|
|
||||||
}
|
|
||||||
return cacheData;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LruCache<String, String> getThumbnailUrlCache() {
|
|
||||||
return thumbnailUrlCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized DBOpenHelper getDBOpenHelper() {
|
|
||||||
if (dbOpenHelper == null) {
|
|
||||||
dbOpenHelper = new DBOpenHelper(this);
|
|
||||||
}
|
|
||||||
return dbOpenHelper;
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized NearbyPlaces getNearbyPlaces() {
|
|
||||||
if (nearbyPlaces == null) {
|
|
||||||
nearbyPlaces = new NearbyPlaces();
|
|
||||||
}
|
|
||||||
return nearbyPlaces;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
|
|
||||||
|
ApplicationlessInjection
|
||||||
|
.getInstance(this)
|
||||||
|
.getCommonsApplicationComponent()
|
||||||
|
.inject(this);
|
||||||
|
|
||||||
|
Fresco.initialize(this);
|
||||||
if (setupLeakCanary() == RefWatcher.DISABLED) {
|
if (setupLeakCanary() == RefWatcher.DISABLED) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Timber.plant(new Timber.DebugTree());
|
Timber.plant(new Timber.DebugTree());
|
||||||
|
|
||||||
DaggerAppComponent
|
|
||||||
.builder()
|
|
||||||
.application(this)
|
|
||||||
.build()
|
|
||||||
.inject(this);
|
|
||||||
|
|
||||||
if (!BuildConfig.DEBUG) {
|
if (!BuildConfig.DEBUG) {
|
||||||
ACRA.init(this);
|
ACRA.init(this);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -151,13 +87,13 @@ public class CommonsApplication extends Application implements HasActivityInject
|
||||||
|
|
||||||
// Fire progress callbacks for every 3% of uploaded content
|
// Fire progress callbacks for every 3% of uploaded content
|
||||||
System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0");
|
System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0");
|
||||||
|
|
||||||
Fresco.initialize(this);
|
|
||||||
|
|
||||||
//For caching area -> categories
|
|
||||||
cacheData = new CacheController();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helps in setting up LeakCanary library
|
||||||
|
* @return instance of LeakCanary
|
||||||
|
*/
|
||||||
protected RefWatcher setupLeakCanary() {
|
protected RefWatcher setupLeakCanary() {
|
||||||
if (LeakCanary.isInAnalyzerProcess(this)) {
|
if (LeakCanary.isInAnalyzerProcess(this)) {
|
||||||
return RefWatcher.DISABLED;
|
return RefWatcher.DISABLED;
|
||||||
|
|
@ -165,50 +101,22 @@ public class CommonsApplication extends Application implements HasActivityInject
|
||||||
return LeakCanary.install(this);
|
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) {
|
public static RefWatcher getRefWatcher(Context context) {
|
||||||
CommonsApplication application = (CommonsApplication) context.getApplicationContext();
|
CommonsApplication application = (CommonsApplication) context.getApplicationContext();
|
||||||
return application.refWatcher;
|
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(), mediaWikiApi.getAuthCookie());
|
|
||||||
try {
|
|
||||||
String authCookie = accountManager.blockingGetAuthToken(curAccount, "", false);
|
|
||||||
mediaWikiApi.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) {
|
public void clearApplicationData(Context context, LogoutListener logoutListener) {
|
||||||
File cacheDirectory = context.getCacheDir();
|
File cacheDirectory = context.getCacheDir();
|
||||||
File applicationDirectory = new File(cacheDirectory.getParent());
|
File applicationDirectory = new File(cacheDirectory.getParent());
|
||||||
|
|
@ -221,75 +129,37 @@ public class CommonsApplication extends Application implements HasActivityInject
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AccountManager accountManager = AccountManager.get(this);
|
sessionManager.clearAllAccounts()
|
||||||
Account[] allAccounts = accountManager.getAccountsByType(AccountUtil.accountType());
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
AccountManagerCallback<Boolean> amCallback = new AccountManagerCallback<Boolean>() {
|
.subscribe(() -> {
|
||||||
|
|
||||||
private int index = 0;
|
|
||||||
|
|
||||||
void setIndex(int index) {
|
|
||||||
this.index = index;
|
|
||||||
}
|
|
||||||
|
|
||||||
int getIndex() {
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run(AccountManagerFuture<Boolean> accountManagerFuture) {
|
|
||||||
setIndex(getIndex() + 1);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (accountManagerFuture != null && accountManagerFuture.getResult()) {
|
|
||||||
Timber.d("Account removed successfully.");
|
|
||||||
}
|
|
||||||
} catch (OperationCanceledException | IOException | AuthenticatorException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getIndex() == allAccounts.length) {
|
|
||||||
Timber.d("All accounts have been removed");
|
Timber.d("All accounts have been removed");
|
||||||
//TODO: fix preference manager
|
//TODO: fix preference manager
|
||||||
PreferenceManager.getDefaultSharedPreferences(getInstance())
|
defaultPrefs.edit().clear().apply();
|
||||||
.edit().clear().commit();
|
applicationPrefs.edit().clear().apply();
|
||||||
SharedPreferences preferences = context
|
applicationPrefs.edit().putBoolean("firstrun", false).apply();
|
||||||
.getSharedPreferences("fr.free.nrw.commons", MODE_PRIVATE);
|
otherPrefs.edit().clear().apply();
|
||||||
preferences.edit().clear().commit();
|
|
||||||
context.getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
|
||||||
.edit().clear().commit();
|
|
||||||
preferences.edit().putBoolean("firstrun", false).apply();
|
|
||||||
updateAllDatabases();
|
updateAllDatabases();
|
||||||
currentAccount = null;
|
|
||||||
|
|
||||||
logoutListener.onLogoutComplete();
|
logoutListener.onLogoutComplete();
|
||||||
}
|
});
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for (Account account : allAccounts) {
|
|
||||||
accountManager.removeAccount(account, amCallback, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public AndroidInjector<Activity> activityInjector() {
|
|
||||||
return dispatchingActivityInjector;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes all tables and re-creates them.
|
* Deletes all tables and re-creates them.
|
||||||
*/
|
*/
|
||||||
public void updateAllDatabases() {
|
private void updateAllDatabases() {
|
||||||
DBOpenHelper dbOpenHelper = CommonsApplication.getInstance().getDBOpenHelper();
|
|
||||||
dbOpenHelper.getReadableDatabase().close();
|
dbOpenHelper.getReadableDatabase().close();
|
||||||
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
|
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
|
||||||
|
|
||||||
ModifierSequence.Table.onDelete(db);
|
ModifierSequenceDao.Table.onDelete(db);
|
||||||
Category.Table.onDelete(db);
|
CategoryDao.Table.onDelete(db);
|
||||||
Contribution.Table.onDelete(db);
|
ContributionDao.Table.onDelete(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface used to get log-out events
|
||||||
|
*/
|
||||||
public interface LogoutListener {
|
public interface LogoutListener {
|
||||||
void onLogoutComplete();
|
void onLogoutComplete();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package fr.free.nrw.commons;
|
package fr.free.nrw.commons;
|
||||||
|
|
||||||
import android.app.Service;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Binder;
|
import android.os.Binder;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
|
@ -9,7 +8,9 @@ import android.os.IBinder;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
|
|
||||||
public abstract class HandlerService<T> extends Service {
|
import fr.free.nrw.commons.di.CommonsDaggerService;
|
||||||
|
|
||||||
|
public abstract class HandlerService<T> extends CommonsDaggerService {
|
||||||
private volatile Looper threadLooper;
|
private volatile Looper threadLooper;
|
||||||
private volatile ServiceHandler threadHandler;
|
private volatile ServiceHandler threadHandler;
|
||||||
private String serviceName;
|
private String serviceName;
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,25 @@ package fr.free.nrw.commons;
|
||||||
|
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* represents Licence object
|
||||||
|
*/
|
||||||
public class License {
|
public class License {
|
||||||
private String key;
|
private String key;
|
||||||
private String template;
|
private String template;
|
||||||
private String url;
|
private String url;
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of License.
|
||||||
|
*
|
||||||
|
* @param key license key
|
||||||
|
* @param template license template
|
||||||
|
* @param url license URL
|
||||||
|
* @param name licence name
|
||||||
|
*
|
||||||
|
* @throws RuntimeException if License.key or Licence.template is null
|
||||||
|
*/
|
||||||
public License(String key, String template, String url, String name) {
|
public License(String key, String template, String url, String name) {
|
||||||
if (key == null) {
|
if (key == null) {
|
||||||
throw new RuntimeException("License.key must not be null");
|
throw new RuntimeException("License.key must not be null");
|
||||||
|
|
@ -21,10 +34,18 @@ public class License {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the license key.
|
||||||
|
* @return license key as a String.
|
||||||
|
*/
|
||||||
public String getKey() {
|
public String getKey() {
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the license template.
|
||||||
|
* @return license template as a String.
|
||||||
|
*/
|
||||||
public String getTemplate() {
|
public String getTemplate() {
|
||||||
return template;
|
return template;
|
||||||
}
|
}
|
||||||
|
|
@ -38,6 +59,12 @@ public class License {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the license URL
|
||||||
|
*
|
||||||
|
* @param language license language
|
||||||
|
* @return URL
|
||||||
|
*/
|
||||||
public @Nullable String getUrl(String language) {
|
public @Nullable String getUrl(String language) {
|
||||||
if (url == null) {
|
if (url == null) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -5,21 +5,31 @@ import android.content.res.Resources;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
import org.xmlpull.v1.XmlPullParser;
|
import org.xmlpull.v1.XmlPullParser;
|
||||||
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a list of Licenses
|
||||||
|
*/
|
||||||
public class LicenseList {
|
public class LicenseList {
|
||||||
private Map<String, License> licenses = new HashMap<>();
|
private Map<String, License> licenses = new HashMap<>();
|
||||||
private Resources res;
|
private Resources res;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs new instance of LicenceList
|
||||||
|
*
|
||||||
|
* @param activity License activity
|
||||||
|
*/
|
||||||
public LicenseList(Activity activity) {
|
public LicenseList(Activity activity) {
|
||||||
res = activity.getResources();
|
res = activity.getResources();
|
||||||
XmlPullParser parser = res.getXml(R.xml.wikimedia_licenses);
|
XmlPullParser parser = res.getXml(R.xml.wikimedia_licenses);
|
||||||
String namespace = "https://www.mediawiki.org/wiki/Extension:UploadWizard/xmlns/licenses";
|
String namespace = "https://www.mediawiki.org/wiki/Extension:UploadWizard/xmlns/licenses";
|
||||||
while (Utils.xmlFastForward(parser, namespace, "license")) {
|
while (xmlFastForward(parser, namespace, "license")) {
|
||||||
String id = parser.getAttributeValue(null, "id");
|
String id = parser.getAttributeValue(null, "id");
|
||||||
String template = parser.getAttributeValue(null, "template");
|
String template = parser.getAttributeValue(null, "template");
|
||||||
String url = parser.getAttributeValue(null, "url");
|
String url = parser.getAttributeValue(null, "url");
|
||||||
|
|
@ -29,14 +39,28 @@ public class LicenseList {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a collection of licenses
|
||||||
|
* @return License values
|
||||||
|
*/
|
||||||
public Collection<License> values() {
|
public Collection<License> values() {
|
||||||
return licenses.values();
|
return licenses.values();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets license
|
||||||
|
* @param key License key
|
||||||
|
* @return License that matches key
|
||||||
|
*/
|
||||||
public License get(String key) {
|
public License get(String key) {
|
||||||
return licenses.get(key);
|
return licenses.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a license from template
|
||||||
|
* @param template License template
|
||||||
|
* @return null
|
||||||
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
License licenseForTemplate(String template) {
|
License licenseForTemplate(String template) {
|
||||||
String ucTemplate = new PageTitle(template).getDisplayText();
|
String ucTemplate = new PageTitle(template).getDisplayText();
|
||||||
|
|
@ -48,6 +72,11 @@ public class LicenseList {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets template name id
|
||||||
|
* @param template License template
|
||||||
|
* @return name id of template
|
||||||
|
*/
|
||||||
private String nameIdForTemplate(String template) {
|
private String nameIdForTemplate(String template) {
|
||||||
// hack :D (converts dashes and periods to underscores)
|
// hack :D (converts dashes and periods to underscores)
|
||||||
// cc-by-sa-3.0 -> cc_by_sa_3_0
|
// cc-by-sa-3.0 -> cc_by_sa_3_0
|
||||||
|
|
@ -55,9 +84,44 @@ public class LicenseList {
|
||||||
"_").replace(".", "_");
|
"_").replace(".", "_");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets name of given template
|
||||||
|
* @param template License template
|
||||||
|
* @return name of template
|
||||||
|
*/
|
||||||
private String nameForTemplate(String template) {
|
private String nameForTemplate(String template) {
|
||||||
int nameId = res.getIdentifier("fr.free.nrw.commons:string/"
|
int nameId = res.getIdentifier("fr.free.nrw.commons:string/"
|
||||||
+ nameIdForTemplate(template), null, null);
|
+ nameIdForTemplate(template), null, null);
|
||||||
return (nameId != 0) ? res.getString(nameId) : template;
|
return (nameId != 0) ? res.getString(nameId) : template;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* Fast-forward an XmlPullParser to the next instance of the given element
|
||||||
|
* in the input stream (namespaced).
|
||||||
|
*
|
||||||
|
* @param parser
|
||||||
|
* @param namespace
|
||||||
|
* @param element
|
||||||
|
* @return true on match, false on failure
|
||||||
|
*/
|
||||||
|
private boolean xmlFastForward(XmlPullParser parser, String namespace, String element) {
|
||||||
|
try {
|
||||||
|
while (parser.next() != XmlPullParser.END_DOCUMENT) {
|
||||||
|
if (parser.getEventType() == XmlPullParser.START_TAG &&
|
||||||
|
parser.getNamespace().equals(namespace) &&
|
||||||
|
parser.getName().equals(element)) {
|
||||||
|
// We found it!
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (XmlPullParserException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,16 +47,35 @@ public class Media implements Parcelable {
|
||||||
private HashMap<String, Object> tags = new HashMap<>();
|
private HashMap<String, Object> tags = new HashMap<>();
|
||||||
private @Nullable LatLng coordinates;
|
private @Nullable LatLng coordinates;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides local constructor
|
||||||
|
*/
|
||||||
protected Media() {
|
protected Media() {
|
||||||
this.categories = new ArrayList<>();
|
this.categories = new ArrayList<>();
|
||||||
this.descriptions = new HashMap<>();
|
this.descriptions = new HashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a minimal constructor
|
||||||
|
*
|
||||||
|
* @param filename Media filename
|
||||||
|
*/
|
||||||
public Media(String filename) {
|
public Media(String filename) {
|
||||||
this();
|
this();
|
||||||
this.filename = filename;
|
this.filename = filename;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide Media constructor
|
||||||
|
* @param localUri Media URI
|
||||||
|
* @param imageUrl Media image URL
|
||||||
|
* @param filename Media filename
|
||||||
|
* @param description Media description
|
||||||
|
* @param dataLength Media date length
|
||||||
|
* @param dateCreated Media creation date
|
||||||
|
* @param dateUploaded Media date uploaded
|
||||||
|
* @param creator Media creator
|
||||||
|
*/
|
||||||
public Media(Uri localUri, String imageUrl, String filename, String description,
|
public Media(Uri localUri, String imageUrl, String filename, String description,
|
||||||
long dataLength, Date dateCreated, @Nullable Date dateUploaded, String creator) {
|
long dataLength, Date dateCreated, @Nullable Date dateUploaded, String creator) {
|
||||||
this();
|
this();
|
||||||
|
|
@ -90,19 +109,33 @@ public class Media implements Parcelable {
|
||||||
descriptions = in.readHashMap(ClassLoader.getSystemClassLoader());
|
descriptions = in.readHashMap(ClassLoader.getSystemClassLoader());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets tag of media
|
||||||
|
* @param key Media key
|
||||||
|
* @return Media tag
|
||||||
|
*/
|
||||||
public Object getTag(String key) {
|
public Object getTag(String key) {
|
||||||
return tags.get(key);
|
return tags.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifies( or creates a) tag of media
|
||||||
|
* @param key Media key
|
||||||
|
* @param value Media value
|
||||||
|
*/
|
||||||
public void setTag(String key, Object value) {
|
public void setTag(String key, Object value) {
|
||||||
tags.put(key, value);
|
tags.put(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets media display title
|
||||||
|
* @return Media title
|
||||||
|
*/
|
||||||
public String getDisplayTitle() {
|
public String getDisplayTitle() {
|
||||||
if (filename == null) {
|
if (filename == null) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
// FIXME: Gross hack bercause my regex skills suck maybe or I am too lazy who knows
|
// FIXME: Gross hack because my regex skills suck maybe or I am too lazy who knows
|
||||||
String title = getFilePageTitle().getDisplayText().replaceFirst("^File:", "");
|
String title = getFilePageTitle().getDisplayText().replaceFirst("^File:", "");
|
||||||
Matcher matcher = displayTitlePattern.matcher(title);
|
Matcher matcher = displayTitlePattern.matcher(title);
|
||||||
if (matcher.matches()) {
|
if (matcher.matches()) {
|
||||||
|
|
@ -112,14 +145,27 @@ public class Media implements Parcelable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets file page title
|
||||||
|
* @return New media page title
|
||||||
|
*/
|
||||||
public PageTitle getFilePageTitle() {
|
public PageTitle getFilePageTitle() {
|
||||||
return new PageTitle("File:" + getFilename().replaceFirst("^File:", ""));
|
return new PageTitle("File:" + getFilename().replaceFirst("^File:", ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets local URI
|
||||||
|
* @return Media local URI
|
||||||
|
*/
|
||||||
public Uri getLocalUri() {
|
public Uri getLocalUri() {
|
||||||
return localUri;
|
return localUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets image URL
|
||||||
|
* can be null.
|
||||||
|
* @return Image URL
|
||||||
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
public String getImageUrl() {
|
public String getImageUrl() {
|
||||||
if (imageUrl == null && this.getFilename() != null) {
|
if (imageUrl == null && this.getFilename() != null) {
|
||||||
|
|
@ -128,94 +174,186 @@ public class Media implements Parcelable {
|
||||||
return imageUrl;
|
return imageUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the name of the file.
|
||||||
|
* @return file name as a string
|
||||||
|
*/
|
||||||
public String getFilename() {
|
public String getFilename() {
|
||||||
return filename;
|
return filename;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the name of the file.
|
||||||
|
* @param filename the new name of the file
|
||||||
|
*/
|
||||||
public void setFilename(String filename) {
|
public void setFilename(String filename) {
|
||||||
this.filename = filename;
|
this.filename = filename;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the file description.
|
||||||
|
* @return file description as a string
|
||||||
|
*/
|
||||||
public String getDescription() {
|
public String getDescription() {
|
||||||
return description;
|
return description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the file description.
|
||||||
|
* @param description the new description of the file
|
||||||
|
*/
|
||||||
public void setDescription(String description) {
|
public void setDescription(String description) {
|
||||||
this.description = description;
|
this.description = description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the datalength of the file.
|
||||||
|
* @return file datalength as a long
|
||||||
|
*/
|
||||||
public long getDataLength() {
|
public long getDataLength() {
|
||||||
return dataLength;
|
return dataLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the datalength of the file.
|
||||||
|
* @param dataLength as a long
|
||||||
|
*/
|
||||||
public void setDataLength(long dataLength) {
|
public void setDataLength(long dataLength) {
|
||||||
this.dataLength = dataLength;
|
this.dataLength = dataLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the creation date of the file.
|
||||||
|
* @return creation date as a Date
|
||||||
|
*/
|
||||||
public Date getDateCreated() {
|
public Date getDateCreated() {
|
||||||
return dateCreated;
|
return dateCreated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the creation date of the file.
|
||||||
|
* @param date creation date as a Date
|
||||||
|
*/
|
||||||
public void setDateCreated(Date date) {
|
public void setDateCreated(Date date) {
|
||||||
this.dateCreated = date;
|
this.dateCreated = date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the upload date of the file.
|
||||||
|
* Can be null.
|
||||||
|
* @return upload date as a Date
|
||||||
|
*/
|
||||||
public @Nullable
|
public @Nullable
|
||||||
Date getDateUploaded() {
|
Date getDateUploaded() {
|
||||||
return dateUploaded;
|
return dateUploaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the name of the creator of the file.
|
||||||
|
* @return creator name as a String
|
||||||
|
*/
|
||||||
public String getCreator() {
|
public String getCreator() {
|
||||||
return creator;
|
return creator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the creator name of the file.
|
||||||
|
* @param creator creator name as a string
|
||||||
|
*/
|
||||||
public void setCreator(String creator) {
|
public void setCreator(String creator) {
|
||||||
this.creator = creator;
|
this.creator = creator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the width of the media.
|
||||||
|
* @return file width as an int
|
||||||
|
*/
|
||||||
public int getWidth() {
|
public int getWidth() {
|
||||||
return width;
|
return width;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the width of the media.
|
||||||
|
* @param width file width as an int
|
||||||
|
*/
|
||||||
public void setWidth(int width) {
|
public void setWidth(int width) {
|
||||||
this.width = width;
|
this.width = width;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the height of the media.
|
||||||
|
* @return file height as an int
|
||||||
|
*/
|
||||||
public int getHeight() {
|
public int getHeight() {
|
||||||
return height;
|
return height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the height of the media.
|
||||||
|
* @param height file height as an int
|
||||||
|
*/
|
||||||
public void setHeight(int height) {
|
public void setHeight(int height) {
|
||||||
this.height = height;
|
this.height = height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the license name of the file.
|
||||||
|
* @return license as a String
|
||||||
|
*/
|
||||||
public String getLicense() {
|
public String getLicense() {
|
||||||
return license;
|
return license;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the license name of the file.
|
||||||
|
* @param license license name as a String
|
||||||
|
*/
|
||||||
public void setLicense(String license) {
|
public void setLicense(String license) {
|
||||||
this.license = license;
|
this.license = license;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the coordinates of where the file was created.
|
||||||
|
* @return file coordinates as a LatLng
|
||||||
|
*/
|
||||||
public @Nullable
|
public @Nullable
|
||||||
LatLng getCoordinates() {
|
LatLng getCoordinates() {
|
||||||
return coordinates;
|
return coordinates;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the coordinates of where the file was created.
|
||||||
|
* @param coordinates file coordinates as a LatLng
|
||||||
|
*/
|
||||||
public void setCoordinates(@Nullable LatLng coordinates) {
|
public void setCoordinates(@Nullable LatLng coordinates) {
|
||||||
this.coordinates = coordinates;
|
this.coordinates = coordinates;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the categories the file falls under.
|
||||||
|
* @return file categories as an ArrayList of Strings
|
||||||
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public ArrayList<String> getCategories() {
|
public ArrayList<String> getCategories() {
|
||||||
return (ArrayList<String>) categories.clone(); // feels dirty
|
return (ArrayList<String>) categories.clone(); // feels dirty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the categories the file falls under.
|
||||||
|
* </p>
|
||||||
|
* Does not append: i.e. will clear the current categories
|
||||||
|
* and then add the specified ones.
|
||||||
|
* @param categories file categories as a list of Strings
|
||||||
|
*/
|
||||||
public void setCategories(List<String> categories) {
|
public void setCategories(List<String> categories) {
|
||||||
this.categories.clear();
|
this.categories.clear();
|
||||||
this.categories.addAll(categories);
|
this.categories.addAll(categories);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifies (or sets) media descriptions
|
||||||
|
* @param descriptions Media descriptions
|
||||||
|
*/
|
||||||
void setDescriptions(Map<String, String> descriptions) {
|
void setDescriptions(Map<String, String> descriptions) {
|
||||||
for (String key : this.descriptions.keySet()) {
|
for (String key : this.descriptions.keySet()) {
|
||||||
this.descriptions.remove(key);
|
this.descriptions.remove(key);
|
||||||
|
|
@ -225,6 +363,11 @@ public class Media implements Parcelable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets media description in preferred language
|
||||||
|
* @param preferredLanguage Language preferred
|
||||||
|
* @return Description in preferred language
|
||||||
|
*/
|
||||||
public String getDescription(String preferredLanguage) {
|
public String getDescription(String preferredLanguage) {
|
||||||
if (descriptions.containsKey(preferredLanguage)) {
|
if (descriptions.containsKey(preferredLanguage)) {
|
||||||
// See if the requested language is there.
|
// See if the requested language is there.
|
||||||
|
|
@ -241,11 +384,21 @@ public class Media implements Parcelable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method of Parcelable interface
|
||||||
|
* @return zero
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public int describeContents() {
|
public int describeContents() {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a way to transfer information between two or more
|
||||||
|
* activities.
|
||||||
|
* @param parcel Instance of Parcel
|
||||||
|
* @param flags Parcel flag
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void writeToParcel(Parcel parcel, int flags) {
|
public void writeToParcel(Parcel parcel, int flags) {
|
||||||
parcel.writeParcelable(localUri, flags);
|
parcel.writeParcelable(localUri, flags);
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,12 @@ import org.xml.sax.SAXException;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
import javax.xml.parsers.DocumentBuilder;
|
import javax.xml.parsers.DocumentBuilder;
|
||||||
import javax.xml.parsers.DocumentBuilderFactory;
|
import javax.xml.parsers.DocumentBuilderFactory;
|
||||||
import javax.xml.parsers.ParserConfigurationException;
|
import javax.xml.parsers.ParserConfigurationException;
|
||||||
|
|
@ -33,45 +33,39 @@ import timber.log.Timber;
|
||||||
* which are not intrinsic to the media and may change due to editing.
|
* which are not intrinsic to the media and may change due to editing.
|
||||||
*/
|
*/
|
||||||
public class MediaDataExtractor {
|
public class MediaDataExtractor {
|
||||||
|
private final MediaWikiApi mediaWikiApi;
|
||||||
private boolean fetched;
|
private boolean fetched;
|
||||||
|
|
||||||
private String filename;
|
|
||||||
private ArrayList<String> categories;
|
private ArrayList<String> categories;
|
||||||
private Map<String, String> descriptions;
|
private Map<String, String> descriptions;
|
||||||
private String license;
|
private String license;
|
||||||
private @Nullable LatLng coordinates;
|
private @Nullable LatLng coordinates;
|
||||||
private LicenseList licenseList;
|
|
||||||
|
|
||||||
/**
|
@Inject
|
||||||
* @param filename of the target media object, should include 'File:' prefix
|
public MediaDataExtractor(MediaWikiApi mwApi) {
|
||||||
*/
|
this.categories = new ArrayList<>();
|
||||||
public MediaDataExtractor(String filename, LicenseList licenseList) {
|
this.descriptions = new HashMap<>();
|
||||||
this.filename = filename;
|
this.fetched = false;
|
||||||
categories = new ArrayList<>();
|
this.mediaWikiApi = mwApi;
|
||||||
descriptions = new HashMap<>();
|
|
||||||
fetched = false;
|
|
||||||
this.licenseList = licenseList;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* Actually fetch the data over the network.
|
* Actually fetch the data over the network.
|
||||||
* todo: use local caching?
|
* todo: use local caching?
|
||||||
*
|
*
|
||||||
* Warning: synchronous i/o, call on a background thread
|
* Warning: synchronous i/o, call on a background thread
|
||||||
*/
|
*/
|
||||||
public void fetch() throws IOException {
|
public void fetch(String filename, LicenseList licenseList) throws IOException {
|
||||||
if (fetched) {
|
if (fetched) {
|
||||||
throw new IllegalStateException("Tried to call MediaDataExtractor.fetch() again.");
|
throw new IllegalStateException("Tried to call MediaDataExtractor.fetch() again.");
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
|
MediaResult result = mediaWikiApi.fetchMediaByFilename(filename);
|
||||||
MediaResult result = api.fetchMediaByFilename(filename);
|
|
||||||
|
|
||||||
// In-page category links are extracted from source, as XML doesn't cover [[links]]
|
// In-page category links are extracted from source, as XML doesn't cover [[links]]
|
||||||
extractCategories(result.getWikiSource());
|
extractCategories(result.getWikiSource());
|
||||||
|
|
||||||
// Description template info is extracted from preprocessor XML
|
// Description template info is extracted from preprocessor XML
|
||||||
processWikiParseTree(result.getParseTreeXmlSource());
|
processWikiParseTree(result.getParseTreeXmlSource(), licenseList);
|
||||||
fetched = true;
|
fetched = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,7 +84,7 @@ public class MediaDataExtractor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processWikiParseTree(String source) throws IOException {
|
private void processWikiParseTree(String source, LicenseList licenseList) throws IOException {
|
||||||
Document doc;
|
Document doc;
|
||||||
try {
|
try {
|
||||||
DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
|
DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,17 @@ import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
|
|
||||||
class MediaThumbnailFetchTask extends AsyncTask<String, String, String> {
|
class MediaThumbnailFetchTask extends AsyncTask<String, String, String> {
|
||||||
protected final Media media;
|
protected final Media media;
|
||||||
|
private MediaWikiApi mediaWikiApi;
|
||||||
|
|
||||||
public MediaThumbnailFetchTask(@NonNull Media media) {
|
public MediaThumbnailFetchTask(@NonNull Media media, MediaWikiApi mwApi) {
|
||||||
this.media = media;
|
this.media = media;
|
||||||
|
this.mediaWikiApi = mwApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String doInBackground(String... params) {
|
protected String doInBackground(String... params) {
|
||||||
try {
|
try {
|
||||||
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
|
return mediaWikiApi.findThumbnailByFilename(params[0]);
|
||||||
return api.findThumbnailByFilename(params[0]);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// Do something better!
|
// Do something better!
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import android.content.Context;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.graphics.drawable.VectorDrawableCompat;
|
import android.support.graphics.drawable.VectorDrawableCompat;
|
||||||
|
import android.support.v4.util.LruCache;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
@ -11,9 +12,16 @@ import android.widget.Toast;
|
||||||
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
|
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
|
||||||
import com.facebook.drawee.view.SimpleDraweeView;
|
import com.facebook.drawee.view.SimpleDraweeView;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||||
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
public class MediaWikiImageView extends SimpleDraweeView {
|
public class MediaWikiImageView extends SimpleDraweeView {
|
||||||
|
@Inject MediaWikiApi mwApi;
|
||||||
|
@Inject LruCache<String, String> thumbnailUrlCache;
|
||||||
|
|
||||||
private ThumbnailFetchTask currentThumbnailTask;
|
private ThumbnailFetchTask currentThumbnailTask;
|
||||||
|
|
||||||
public MediaWikiImageView(Context context) {
|
public MediaWikiImageView(Context context) {
|
||||||
|
|
@ -31,6 +39,10 @@ public class MediaWikiImageView extends SimpleDraweeView {
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the media. Fetches its thumbnail if necessary.
|
||||||
|
* @param media the new media
|
||||||
|
*/
|
||||||
public void setMedia(Media media) {
|
public void setMedia(Media media) {
|
||||||
if (currentThumbnailTask != null) {
|
if (currentThumbnailTask != null) {
|
||||||
currentThumbnailTask.cancel(true);
|
currentThumbnailTask.cancel(true);
|
||||||
|
|
@ -39,11 +51,11 @@ public class MediaWikiImageView extends SimpleDraweeView {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CommonsApplication.getInstance().getThumbnailUrlCache().get(media.getFilename()) != null) {
|
if (thumbnailUrlCache.get(media.getFilename()) != null) {
|
||||||
setImageUrl(CommonsApplication.getInstance().getThumbnailUrlCache().get(media.getFilename()));
|
setImageUrl(thumbnailUrlCache.get(media.getFilename()));
|
||||||
} else {
|
} else {
|
||||||
setImageUrl(null);
|
setImageUrl(null);
|
||||||
currentThumbnailTask = new ThumbnailFetchTask(media);
|
currentThumbnailTask = new ThumbnailFetchTask(media, mwApi);
|
||||||
currentThumbnailTask.execute(media.getFilename());
|
currentThumbnailTask.execute(media.getFilename());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -56,7 +68,15 @@ public class MediaWikiImageView extends SimpleDraweeView {
|
||||||
super.onDetachedFromWindow();
|
super.onDetachedFromWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes MediaWikiImageView.
|
||||||
|
*/
|
||||||
private void init() {
|
private void init() {
|
||||||
|
ApplicationlessInjection
|
||||||
|
.getInstance(getContext()
|
||||||
|
.getApplicationContext())
|
||||||
|
.getCommonsApplicationComponent()
|
||||||
|
.inject(this);
|
||||||
setHierarchy(GenericDraweeHierarchyBuilder
|
setHierarchy(GenericDraweeHierarchyBuilder
|
||||||
.newInstance(getResources())
|
.newInstance(getResources())
|
||||||
.setPlaceholderImage(VectorDrawableCompat.create(getResources(),
|
.setPlaceholderImage(VectorDrawableCompat.create(getResources(),
|
||||||
|
|
@ -66,13 +86,17 @@ public class MediaWikiImageView extends SimpleDraweeView {
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the image from the URL.
|
||||||
|
* @param url the URL of the image
|
||||||
|
*/
|
||||||
private void setImageUrl(@Nullable String url) {
|
private void setImageUrl(@Nullable String url) {
|
||||||
setImageURI(url);
|
setImageURI(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ThumbnailFetchTask extends MediaThumbnailFetchTask {
|
private class ThumbnailFetchTask extends MediaThumbnailFetchTask {
|
||||||
ThumbnailFetchTask(@NonNull Media media) {
|
ThumbnailFetchTask(@NonNull Media media, @NonNull MediaWikiApi mwApi) {
|
||||||
super(media);
|
super(media, mwApi);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -85,7 +109,7 @@ public class MediaWikiImageView extends SimpleDraweeView {
|
||||||
} else {
|
} else {
|
||||||
// only cache meaningful thumbnails received from network.
|
// only cache meaningful thumbnails received from network.
|
||||||
try {
|
try {
|
||||||
CommonsApplication.getInstance().getThumbnailUrlCache().put(media.getFilename(), result);
|
thumbnailUrlCache.put(media.getFilename(), result);
|
||||||
} catch (NullPointerException npe) {
|
} catch (NullPointerException npe) {
|
||||||
Timber.e("error when adding pic to cache " + npe);
|
Timber.e("error when adding pic to cache " + npe);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,12 @@ public class PageTitle {
|
||||||
return titleKey;
|
return titleKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the canonicalized title for displaying (such as "File:My example.jpg").
|
||||||
|
* </p>
|
||||||
|
* Essentially equivalent to getPrefixedText
|
||||||
|
* @return canonical title as a String
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return getPrefixedText();
|
return getPrefixedText();
|
||||||
|
|
|
||||||
|
|
@ -1,102 +1,26 @@
|
||||||
package fr.free.nrw.commons;
|
package fr.free.nrw.commons;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Build;
|
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.text.Html;
|
|
||||||
import android.text.Spanned;
|
|
||||||
|
|
||||||
import org.apache.commons.codec.binary.Hex;
|
import org.apache.commons.codec.binary.Hex;
|
||||||
import org.apache.commons.codec.digest.DigestUtils;
|
import org.apache.commons.codec.digest.DigestUtils;
|
||||||
import org.w3c.dom.Node;
|
|
||||||
import org.xmlpull.v1.XmlPullParser;
|
|
||||||
import org.xmlpull.v1.XmlPullParserException;
|
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.io.StringWriter;
|
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.math.BigInteger;
|
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.text.ParseException;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.TimeZone;
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import javax.xml.transform.Transformer;
|
|
||||||
import javax.xml.transform.TransformerConfigurationException;
|
|
||||||
import javax.xml.transform.TransformerException;
|
|
||||||
import javax.xml.transform.TransformerFactory;
|
|
||||||
import javax.xml.transform.TransformerFactoryConfigurationError;
|
|
||||||
import javax.xml.transform.dom.DOMSource;
|
|
||||||
import javax.xml.transform.stream.StreamResult;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.settings.Prefs;
|
import fr.free.nrw.commons.settings.Prefs;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
public class Utils {
|
public class Utils {
|
||||||
|
|
||||||
// Get SHA1 of file from input stream
|
|
||||||
public static String getSHA1(InputStream is) {
|
|
||||||
|
|
||||||
MessageDigest digest;
|
|
||||||
try {
|
|
||||||
digest = MessageDigest.getInstance("SHA1");
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
Timber.e(e, "Exception while getting Digest");
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] buffer = new byte[8192];
|
|
||||||
int read;
|
|
||||||
try {
|
|
||||||
while ((read = is.read(buffer)) > 0) {
|
|
||||||
digest.update(buffer, 0, read);
|
|
||||||
}
|
|
||||||
byte[] md5sum = digest.digest();
|
|
||||||
BigInteger bigInt = new BigInteger(1, md5sum);
|
|
||||||
String output = bigInt.toString(16);
|
|
||||||
// Fill to 40 chars
|
|
||||||
output = String.format("%40s", output).replace(' ', '0');
|
|
||||||
Timber.i("File SHA1: %s", output);
|
|
||||||
|
|
||||||
return output;
|
|
||||||
} catch (IOException e) {
|
|
||||||
Timber.e(e, "IO Exception");
|
|
||||||
return "";
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
is.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
Timber.e(e, "Exception on closing MD5 input stream");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fix Html.fromHtml is deprecated problem
|
|
||||||
*
|
|
||||||
* @param source provided Html string
|
|
||||||
* @return returned Spanned of appropriate method according to version check
|
|
||||||
*/
|
|
||||||
public static Spanned fromHtml(String source) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
||||||
return Html.fromHtml(source, Html.FROM_HTML_MODE_LEGACY);
|
|
||||||
} else {
|
|
||||||
//noinspection deprecation
|
|
||||||
return Html.fromHtml(source);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Strips localization symbols from a string.
|
* Strips localization symbols from a string.
|
||||||
* Removes the suffix after "@" and quotes.
|
* Removes the suffix after "@" and quotes.
|
||||||
|
|
@ -113,49 +37,23 @@ public class Utils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Date parseMWDate(String mwDate) {
|
/**
|
||||||
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH); // Assuming MW always gives me UTC
|
* Creates an URL for thumbnail
|
||||||
try {
|
*
|
||||||
return isoFormat.parse(mwDate);
|
* @param filename Thumbnail file name
|
||||||
} catch (ParseException e) {
|
* @return URL of thumbnail
|
||||||
throw new RuntimeException(e);
|
*/
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String 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(@NonNull String filename) {
|
public static String makeThumbBaseUrl(@NonNull String filename) {
|
||||||
String name = new PageTitle(filename).getPrefixedText();
|
String name = new PageTitle(filename).getPrefixedText();
|
||||||
String sha = new String(Hex.encodeHex(DigestUtils.md5(name)));
|
String sha = new String(Hex.encodeHex(DigestUtils.md5(name)));
|
||||||
return String.format("%s/%s/%s/%s", BuildConfig.IMAGE_URL_BASE, sha.substring(0, 1), sha.substring(0, 2), urlEncode(name));
|
return String.format("%s/%s/%s/%s", BuildConfig.IMAGE_URL_BASE, sha.substring(0, 1), sha.substring(0, 2), urlEncode(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getStringFromDOM(Node dom) {
|
/**
|
||||||
Transformer transformer = null;
|
* URL Encode an URL in UTF-8 format
|
||||||
try {
|
* @param url Unformatted URL
|
||||||
transformer = TransformerFactory.newInstance().newTransformer();
|
* @return Encoded URL
|
||||||
} catch (TransformerConfigurationException | TransformerFactoryConfigurationError e) {
|
*/
|
||||||
// TODO Auto-generated catch block
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
StringWriter outputStream = new StringWriter();
|
|
||||||
DOMSource domSource = new DOMSource(dom);
|
|
||||||
StreamResult strResult = new StreamResult(outputStream);
|
|
||||||
|
|
||||||
try {
|
|
||||||
transformer.transform(domSource, strResult);
|
|
||||||
} catch (TransformerException e) {
|
|
||||||
// TODO Auto-generated catch block
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
return outputStream.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String urlEncode(String url) {
|
public static String urlEncode(String url) {
|
||||||
try {
|
try {
|
||||||
return URLEncoder.encode(url, "utf-8");
|
return URLEncoder.encode(url, "utf-8");
|
||||||
|
|
@ -164,39 +62,21 @@ public class Utils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static long countBytes(InputStream stream) throws IOException {
|
/**
|
||||||
long count = 0;
|
* Capitalizes the first character of a string.
|
||||||
BufferedInputStream bis = new BufferedInputStream(stream);
|
*
|
||||||
while (bis.read() != -1) {
|
* @param string
|
||||||
count++;
|
* @return string with capitalized first character
|
||||||
}
|
*/
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String capitalize(String string) {
|
public static String capitalize(String string) {
|
||||||
return string.substring(0, 1).toUpperCase(Locale.getDefault()) + string.substring(1);
|
return string.substring(0, 1).toUpperCase(Locale.getDefault()) + string.substring(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String licenseTemplateFor(String license) {
|
/**
|
||||||
switch (license) {
|
* Generates licence name with given ID
|
||||||
case Prefs.Licenses.CC_BY_3:
|
* @param license License ID
|
||||||
return "{{self|cc-by-3.0}}";
|
* @return Name of license
|
||||||
case Prefs.Licenses.CC_BY_4:
|
*/
|
||||||
return "{{self|cc-by-4.0}}";
|
|
||||||
case Prefs.Licenses.CC_BY_SA_3:
|
|
||||||
return "{{self|cc-by-sa-3.0}}";
|
|
||||||
case Prefs.Licenses.CC_BY_SA_4:
|
|
||||||
return "{{self|cc-by-sa-4.0}}";
|
|
||||||
case Prefs.Licenses.CC0:
|
|
||||||
return "{{self|cc-zero}}";
|
|
||||||
case Prefs.Licenses.CC_BY:
|
|
||||||
return "{{self|cc-by-3.0}}";
|
|
||||||
case Prefs.Licenses.CC_BY_SA:
|
|
||||||
return "{{self|cc-by-sa-3.0}}";
|
|
||||||
}
|
|
||||||
throw new RuntimeException("Unrecognized license value: " + license);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int licenseNameFor(String license) {
|
public static int licenseNameFor(String license) {
|
||||||
switch (license) {
|
switch (license) {
|
||||||
case Prefs.Licenses.CC_BY_3:
|
case Prefs.Licenses.CC_BY_3:
|
||||||
|
|
@ -217,51 +97,12 @@ public class Utils {
|
||||||
throw new RuntimeException("Unrecognized license value: " + license);
|
throw new RuntimeException("Unrecognized license value: " + license);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String licenseUrlFor(String license) {
|
|
||||||
switch (license) {
|
|
||||||
case Prefs.Licenses.CC_BY_3:
|
|
||||||
return "https://creativecommons.org/licenses/by/3.0/";
|
|
||||||
case Prefs.Licenses.CC_BY_4:
|
|
||||||
return "https://creativecommons.org/licenses/by/4.0/";
|
|
||||||
case Prefs.Licenses.CC_BY_SA_3:
|
|
||||||
return "https://creativecommons.org/licenses/by-sa/3.0/";
|
|
||||||
case Prefs.Licenses.CC_BY_SA_4:
|
|
||||||
return "https://creativecommons.org/licenses/by-sa/4.0/";
|
|
||||||
case Prefs.Licenses.CC0:
|
|
||||||
return "https://creativecommons.org/publicdomain/zero/1.0/";
|
|
||||||
}
|
|
||||||
throw new RuntimeException("Unrecognized license value: " + license);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fast-forward an XmlPullParser to the next instance of the given element
|
* Fixing incorrect extension
|
||||||
* in the input stream (namespaced).
|
* @param title File name
|
||||||
*
|
* @param extension Correct extension
|
||||||
* @param parser
|
* @return File with correct extension
|
||||||
* @param namespace
|
|
||||||
* @param element
|
|
||||||
* @return true on match, false on failure
|
|
||||||
*/
|
*/
|
||||||
public static boolean xmlFastForward(XmlPullParser parser, String namespace, String element) {
|
|
||||||
try {
|
|
||||||
while (parser.next() != XmlPullParser.END_DOCUMENT) {
|
|
||||||
if (parser.getEventType() == XmlPullParser.START_TAG
|
|
||||||
&& parser.getNamespace().equals(namespace)
|
|
||||||
&& parser.getName().equals(element)) {
|
|
||||||
// We found it!
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} catch (XmlPullParserException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String fixExtension(String title, String extension) {
|
public static String fixExtension(String title, String extension) {
|
||||||
Pattern jpegPattern = Pattern.compile("\\.jpeg$", Pattern.CASE_INSENSITIVE);
|
Pattern jpegPattern = Pattern.compile("\\.jpeg$", Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
|
@ -277,10 +118,11 @@ public class Utils {
|
||||||
return title;
|
return title;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isNullOrWhiteSpace(String value) {
|
/**
|
||||||
return value == null || value.trim().isEmpty();
|
* Tells whether dark theme is active or not
|
||||||
}
|
* @param context Activity context
|
||||||
|
* @return The state of dark theme
|
||||||
|
*/
|
||||||
public static boolean isDarkTheme(Context context) {
|
public static boolean isDarkTheme(Context context) {
|
||||||
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean("theme", false);
|
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean("theme", false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,11 @@ public class WelcomeActivity extends BaseActivity {
|
||||||
|
|
||||||
private WelcomePagerAdapter adapter = new WelcomePagerAdapter();
|
private WelcomePagerAdapter adapter = new WelcomePagerAdapter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialises exiting fields and dependencies
|
||||||
|
*
|
||||||
|
* @param savedInstanceState WelcomeActivity bundled data
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
@ -30,12 +35,20 @@ public class WelcomeActivity extends BaseActivity {
|
||||||
adapter.setCallback(this::finish);
|
adapter.setCallback(this::finish);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* References WelcomePageAdapter to null before the activity is destroyed
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
adapter.setCallback(null);
|
adapter.setCallback(null);
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a way to change current activity to WelcomeActivity
|
||||||
|
*
|
||||||
|
* @param context Activity context
|
||||||
|
*/
|
||||||
public static void startYourself(Context context) {
|
public static void startYourself(Context context) {
|
||||||
Intent welcomeIntent = new Intent(context, WelcomeActivity.class);
|
Intent welcomeIntent = new Intent(context, WelcomeActivity.class);
|
||||||
context.startActivity(welcomeIntent);
|
context.startActivity(welcomeIntent);
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,6 @@ import butterknife.ButterKnife;
|
||||||
import butterknife.OnClick;
|
import butterknife.OnClick;
|
||||||
|
|
||||||
public class WelcomePagerAdapter extends PagerAdapter {
|
public class WelcomePagerAdapter extends PagerAdapter {
|
||||||
private static final int PAGE_FINAL = 4;
|
|
||||||
private Callback callback;
|
|
||||||
|
|
||||||
public interface Callback {
|
|
||||||
void onYesClicked();
|
|
||||||
}
|
|
||||||
|
|
||||||
static final int[] PAGE_LAYOUTS = new int[]{
|
static final int[] PAGE_LAYOUTS = new int[]{
|
||||||
R.layout.welcome_wikipedia,
|
R.layout.welcome_wikipedia,
|
||||||
R.layout.welcome_do_upload,
|
R.layout.welcome_do_upload,
|
||||||
|
|
@ -24,16 +17,34 @@ public class WelcomePagerAdapter extends PagerAdapter {
|
||||||
R.layout.welcome_image_details,
|
R.layout.welcome_image_details,
|
||||||
R.layout.welcome_final
|
R.layout.welcome_final
|
||||||
};
|
};
|
||||||
|
private static final int PAGE_FINAL = 4;
|
||||||
|
private Callback callback;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes callback to provided one
|
||||||
|
*
|
||||||
|
* @param callback New callback
|
||||||
|
* it can be null.
|
||||||
|
*/
|
||||||
public void setCallback(@Nullable Callback callback) {
|
public void setCallback(@Nullable Callback callback) {
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets total number of layouts
|
||||||
|
* @return Number of layouts
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public int getCount() {
|
public int getCount() {
|
||||||
return PAGE_LAYOUTS.length;
|
return PAGE_LAYOUTS.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares given view with provided object
|
||||||
|
* @param view Adapter view
|
||||||
|
* @param object Adapter object
|
||||||
|
* @return Equality between view and object
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean isViewFromObject(View view, Object object) {
|
public boolean isViewFromObject(View view, Object object) {
|
||||||
return (view == object);
|
return (view == object);
|
||||||
|
|
@ -52,16 +63,29 @@ public class WelcomePagerAdapter extends PagerAdapter {
|
||||||
return layout;
|
return layout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a way to remove an item from container
|
||||||
|
* @param container Adapter view group container
|
||||||
|
* @param position Index of item
|
||||||
|
* @param obj Adapter object
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void destroyItem(ViewGroup container, int position, Object obj) {
|
public void destroyItem(ViewGroup container, int position, Object obj) {
|
||||||
container.removeView((View) obj);
|
container.removeView((View) obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interface Callback {
|
||||||
|
void onYesClicked();
|
||||||
|
}
|
||||||
|
|
||||||
class ViewHolder {
|
class ViewHolder {
|
||||||
ViewHolder(View view) {
|
ViewHolder(View view) {
|
||||||
ButterKnife.bind(this, view);
|
ButterKnife.bind(this, view);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers on click callback on button click
|
||||||
|
*/
|
||||||
@OnClick(R.id.welcomeYesButton)
|
@OnClick(R.id.welcomeYesButton)
|
||||||
void onClicked() {
|
void onClicked() {
|
||||||
if (callback != null) {
|
if (callback != null) {
|
||||||
|
|
|
||||||
|
|
@ -4,21 +4,33 @@ import android.accounts.Account;
|
||||||
import android.accounts.AccountAuthenticatorResponse;
|
import android.accounts.AccountAuthenticatorResponse;
|
||||||
import android.accounts.AccountManager;
|
import android.accounts.AccountManager;
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
|
|
||||||
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
|
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
import static android.accounts.AccountManager.ERROR_CODE_REMOTE_EXCEPTION;
|
||||||
|
import static android.accounts.AccountManager.KEY_ACCOUNT_NAME;
|
||||||
|
import static android.accounts.AccountManager.KEY_ACCOUNT_TYPE;
|
||||||
|
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.CONTRIBUTION_AUTHORITY;
|
||||||
|
import static fr.free.nrw.commons.modifications.ModificationsContentProvider.MODIFICATIONS_AUTHORITY;
|
||||||
|
|
||||||
public class AccountUtil {
|
public class AccountUtil {
|
||||||
|
|
||||||
public static void createAccount(@Nullable AccountAuthenticatorResponse response,
|
public static final String ACCOUNT_TYPE = "fr.free.nrw.commons";
|
||||||
String username, String password) {
|
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);
|
boolean created = accountManager().addAccountExplicitly(account, password, null);
|
||||||
|
|
||||||
Timber.d("account creation " + (created ? "successful" : "failure"));
|
Timber.d("account creation " + (created ? "successful" : "failure"));
|
||||||
|
|
@ -26,8 +38,8 @@ public class AccountUtil {
|
||||||
if (created) {
|
if (created) {
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
Bundle bundle = new Bundle();
|
Bundle bundle = new Bundle();
|
||||||
bundle.putString(AccountManager.KEY_ACCOUNT_NAME, username);
|
bundle.putString(KEY_ACCOUNT_NAME, username);
|
||||||
bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, accountType());
|
bundle.putString(KEY_ACCOUNT_TYPE, ACCOUNT_TYPE);
|
||||||
|
|
||||||
|
|
||||||
response.onResult(bundle);
|
response.onResult(bundle);
|
||||||
|
|
@ -35,28 +47,18 @@ public class AccountUtil {
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
response.onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, "");
|
response.onError(ERROR_CODE_REMOTE_EXCEPTION, "");
|
||||||
}
|
}
|
||||||
Timber.d("account creation failure");
|
Timber.d("account creation failure");
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: If the user turns it off, it shouldn't be auto turned back on
|
// FIXME: If the user turns it off, it shouldn't be auto turned back on
|
||||||
ContentResolver.setSyncAutomatically(account, ContributionsContentProvider.AUTHORITY, true); // Enable sync by default!
|
ContentResolver.setSyncAutomatically(account, CONTRIBUTION_AUTHORITY, true); // Enable sync by default!
|
||||||
ContentResolver.setSyncAutomatically(account, ModificationsContentProvider.AUTHORITY, true); // Enable sync by default!
|
ContentResolver.setSyncAutomatically(account, MODIFICATIONS_AUTHORITY, true); // Enable sync by default!
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
private AccountManager accountManager() {
|
||||||
public static String accountType() {
|
return AccountManager.get(context);
|
||||||
return "fr.free.nrw.commons";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static AccountManager accountManager() {
|
|
||||||
return AccountManager.get(app());
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private static CommonsApplication app() {
|
|
||||||
return CommonsApplication.getInstance();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,84 +1,45 @@
|
||||||
package fr.free.nrw.commons.auth;
|
package fr.free.nrw.commons.auth;
|
||||||
|
|
||||||
import android.accounts.Account;
|
|
||||||
import android.accounts.AccountManager;
|
|
||||||
import android.accounts.AccountManagerFuture;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
||||||
import io.reactivex.Single;
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import static fr.free.nrw.commons.auth.AccountUtil.AUTH_COOKIE;
|
||||||
import io.reactivex.schedulers.Schedulers;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
public abstract class AuthenticatedActivity extends NavigationBaseActivity {
|
public abstract class AuthenticatedActivity extends NavigationBaseActivity {
|
||||||
|
|
||||||
private String accountType;
|
@Inject SessionManager sessionManager;
|
||||||
CommonsApplication app;
|
@Inject
|
||||||
|
MediaWikiApi mediaWikiApi;
|
||||||
private String authCookie;
|
private String authCookie;
|
||||||
|
|
||||||
public AuthenticatedActivity() {
|
|
||||||
this.accountType = AccountUtil.accountType();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void getAuthCookie(Account account, AccountManager accountManager) {
|
|
||||||
Single.fromCallable(() -> accountManager.blockingGetAuthToken(account, "", false))
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.doOnError(Timber::e)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(this::onAuthCookieAcquired, throwable -> onAuthFailure());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addAccount(AccountManager accountManager) {
|
|
||||||
Single.just(accountManager.addAccount(accountType, null, null, null, AuthenticatedActivity.this, null, null))
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.map(AccountManagerFuture::getResult)
|
|
||||||
.doOnEvent((bundle, throwable) -> {
|
|
||||||
if (!bundle.containsKey(AccountManager.KEY_ACCOUNT_NAME)) {
|
|
||||||
throw new RuntimeException("Bundle doesn't contain account-name key: "
|
|
||||||
+ AccountManager.KEY_ACCOUNT_NAME);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map(bundle -> bundle.getString(AccountManager.KEY_ACCOUNT_NAME))
|
|
||||||
.doOnError(Timber::e)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(s -> {
|
|
||||||
Account[] allAccounts = accountManager.getAccountsByType(accountType);
|
|
||||||
Account curAccount = allAccounts[0];
|
|
||||||
getAuthCookie(curAccount, accountManager);
|
|
||||||
},
|
|
||||||
throwable -> onAuthFailure());
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void requestAuthToken() {
|
protected void requestAuthToken() {
|
||||||
if (authCookie != null) {
|
if (authCookie != null) {
|
||||||
onAuthCookieAcquired(authCookie);
|
onAuthCookieAcquired(authCookie);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
AccountManager accountManager = AccountManager.get(this);
|
authCookie = sessionManager.getAuthCookie();
|
||||||
Account curAccount = app.getCurrentAccount();
|
if (authCookie != null) {
|
||||||
if (curAccount == null) {
|
onAuthCookieAcquired(authCookie);
|
||||||
addAccount(accountManager);
|
|
||||||
} else {
|
|
||||||
getAuthCookie(curAccount, accountManager);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
app = CommonsApplication.getInstance();
|
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
authCookie = savedInstanceState.getString("authCookie");
|
authCookie = savedInstanceState.getString(AUTH_COOKIE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onSaveInstanceState(Bundle outState) {
|
protected void onSaveInstanceState(Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
outState.putString("authCookie", authCookie);
|
outState.putString(AUTH_COOKIE, authCookie);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract void onAuthCookieAcquired(String authCookie);
|
protected abstract void onAuthCookieAcquired(String authCookie);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
package fr.free.nrw.commons.auth;
|
package fr.free.nrw.commons.auth;
|
||||||
|
|
||||||
|
import android.accounts.Account;
|
||||||
import android.accounts.AccountAuthenticatorActivity;
|
import android.accounts.AccountAuthenticatorActivity;
|
||||||
|
import android.accounts.AccountAuthenticatorResponse;
|
||||||
|
import android.accounts.AccountManager;
|
||||||
import android.app.ProgressDialog;
|
import android.app.ProgressDialog;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
|
@ -8,6 +11,7 @@ import android.os.Bundle;
|
||||||
import android.support.annotation.ColorRes;
|
import android.support.annotation.ColorRes;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.StringRes;
|
import android.support.annotation.StringRes;
|
||||||
|
import android.support.design.widget.TextInputLayout;
|
||||||
import android.support.v4.app.NavUtils;
|
import android.support.v4.app.NavUtils;
|
||||||
import android.support.v4.content.ContextCompat;
|
import android.support.v4.content.ContextCompat;
|
||||||
import android.support.v7.app.AppCompatDelegate;
|
import android.support.v7.app.AppCompatDelegate;
|
||||||
|
|
@ -21,25 +25,44 @@ import android.widget.Button;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
|
||||||
import butterknife.BindView;
|
import butterknife.BindView;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
|
import dagger.android.AndroidInjection;
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
import fr.free.nrw.commons.BuildConfig;
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
import fr.free.nrw.commons.PageTitle;
|
import fr.free.nrw.commons.PageTitle;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.Utils;
|
import fr.free.nrw.commons.Utils;
|
||||||
import fr.free.nrw.commons.WelcomeActivity;
|
import fr.free.nrw.commons.WelcomeActivity;
|
||||||
import fr.free.nrw.commons.contributions.ContributionsActivity;
|
import fr.free.nrw.commons.contributions.ContributionsActivity;
|
||||||
|
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||||
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
||||||
|
import io.reactivex.Observable;
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.schedulers.Schedulers;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
import static android.view.KeyEvent.KEYCODE_ENTER;
|
import static android.view.KeyEvent.KEYCODE_ENTER;
|
||||||
|
import static android.view.View.VISIBLE;
|
||||||
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
|
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
|
||||||
|
import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE;
|
||||||
|
import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE;
|
||||||
|
|
||||||
public class LoginActivity extends AccountAuthenticatorActivity {
|
public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
|
|
||||||
public static final String PARAM_USERNAME = "fr.free.nrw.commons.login.username";
|
public static final String PARAM_USERNAME = "fr.free.nrw.commons.login.username";
|
||||||
|
|
||||||
|
@Inject MediaWikiApi mwApi;
|
||||||
|
@Inject AccountUtil accountUtil;
|
||||||
|
@Inject SessionManager sessionManager;
|
||||||
|
@Inject @Named("application_preferences") SharedPreferences prefs;
|
||||||
|
@Inject @Named("default_preferences") SharedPreferences defaultPrefs;
|
||||||
|
|
||||||
@BindView(R.id.loginButton) Button loginButton;
|
@BindView(R.id.loginButton) Button loginButton;
|
||||||
@BindView(R.id.signupButton) Button signupButton;
|
@BindView(R.id.signupButton) Button signupButton;
|
||||||
@BindView(R.id.loginUsername) EditText usernameEdit;
|
@BindView(R.id.loginUsername) EditText usernameEdit;
|
||||||
|
|
@ -47,11 +70,9 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
@BindView(R.id.loginTwoFactor) EditText twoFactorEdit;
|
@BindView(R.id.loginTwoFactor) EditText twoFactorEdit;
|
||||||
@BindView(R.id.error_message_container) ViewGroup errorMessageContainer;
|
@BindView(R.id.error_message_container) ViewGroup errorMessageContainer;
|
||||||
@BindView(R.id.error_message) TextView errorMessage;
|
@BindView(R.id.error_message) TextView errorMessage;
|
||||||
|
@BindView(R.id.two_factor_container)TextInputLayout twoFactorContainer;
|
||||||
private CommonsApplication app;
|
|
||||||
ProgressDialog progressDialog;
|
ProgressDialog progressDialog;
|
||||||
private AppCompatDelegate delegate;
|
private AppCompatDelegate delegate;
|
||||||
private SharedPreferences prefs = null;
|
|
||||||
private LoginTextWatcher textWatcher = new LoginTextWatcher();
|
private LoginTextWatcher textWatcher = new LoginTextWatcher();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -59,16 +80,17 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
setTheme(Utils.isDarkTheme(this) ? R.style.DarkAppTheme : R.style.LightAppTheme);
|
setTheme(Utils.isDarkTheme(this) ? R.style.DarkAppTheme : R.style.LightAppTheme);
|
||||||
getDelegate().installViewFactory();
|
getDelegate().installViewFactory();
|
||||||
getDelegate().onCreate(savedInstanceState);
|
getDelegate().onCreate(savedInstanceState);
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
app = CommonsApplication.getInstance();
|
super.onCreate(savedInstanceState);
|
||||||
|
ApplicationlessInjection
|
||||||
|
.getInstance(this.getApplicationContext())
|
||||||
|
.getCommonsApplicationComponent()
|
||||||
|
.inject(this);
|
||||||
|
|
||||||
setContentView(R.layout.activity_login);
|
setContentView(R.layout.activity_login);
|
||||||
|
|
||||||
ButterKnife.bind(this);
|
ButterKnife.bind(this);
|
||||||
|
|
||||||
prefs = getSharedPreferences("fr.free.nrw.commons", MODE_PRIVATE);
|
|
||||||
|
|
||||||
usernameEdit.addTextChangedListener(textWatcher);
|
usernameEdit.addTextChangedListener(textWatcher);
|
||||||
passwordEdit.addTextChangedListener(textWatcher);
|
passwordEdit.addTextChangedListener(textWatcher);
|
||||||
twoFactorEdit.addTextChangedListener(textWatcher);
|
twoFactorEdit.addTextChangedListener(textWatcher);
|
||||||
|
|
@ -91,7 +113,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
WelcomeActivity.startYourself(this);
|
WelcomeActivity.startYourself(this);
|
||||||
prefs.edit().putBoolean("firstrun", false).apply();
|
prefs.edit().putBoolean("firstrun", false).apply();
|
||||||
}
|
}
|
||||||
if (app.getCurrentAccount() != null) {
|
if (sessionManager.getCurrentAccount() != null) {
|
||||||
startMainActivity();
|
startMainActivity();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -113,6 +135,120 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void performLogin() {
|
||||||
|
Timber.d("Login to start!");
|
||||||
|
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 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Because Mediawiki is upercase-first-char-then-case-sensitive :)
|
||||||
|
* @param username String
|
||||||
|
* @return String canonicial username
|
||||||
|
*/
|
||||||
|
private String canonicializeUsername(String username) {
|
||||||
|
return new PageTitle(username).getText();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onStart() {
|
protected void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
|
|
@ -153,12 +289,10 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void askUserForTwoFactorAuth() {
|
public void askUserForTwoFactorAuth() {
|
||||||
if (BuildConfig.DEBUG) {
|
progressDialog.dismiss();
|
||||||
twoFactorEdit.setVisibility(View.VISIBLE);
|
twoFactorContainer.setVisibility(VISIBLE);
|
||||||
showMessageAndCancelDialog(R.string.login_failed_2fa_needed);
|
twoFactorEdit.setVisibility(VISIBLE);
|
||||||
} else {
|
showMessageAndCancelDialog(R.string.login_failed_2fa_needed);
|
||||||
showMessageAndCancelDialog(R.string.login_failed_2fa_not_supported);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void showMessageAndCancelDialog(@StringRes int resId) {
|
public void showMessageAndCancelDialog(@StringRes int resId) {
|
||||||
|
|
@ -181,12 +315,6 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void performLogin() {
|
|
||||||
Timber.d("Login to start!");
|
|
||||||
LoginTask task = getLoginTask();
|
|
||||||
task.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void signUp() {
|
private void signUp() {
|
||||||
Intent intent = new Intent(this, SignupActivity.class);
|
Intent intent = new Intent(this, SignupActivity.class);
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
|
|
@ -207,28 +335,10 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private LoginTask getLoginTask() {
|
|
||||||
return new LoginTask(
|
|
||||||
this,
|
|
||||||
canonicializeUsername(usernameEdit.getText().toString()),
|
|
||||||
passwordEdit.getText().toString(),
|
|
||||||
twoFactorEdit.getText().toString()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Because Mediawiki is upercase-first-char-then-case-sensitive :)
|
|
||||||
* @param username String
|
|
||||||
* @return String canonicial username
|
|
||||||
*/
|
|
||||||
private String canonicializeUsername(String username) {
|
|
||||||
return new PageTitle(username).getText();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showMessage(@StringRes int resId, @ColorRes int colorResId) {
|
private void showMessage(@StringRes int resId, @ColorRes int colorResId) {
|
||||||
errorMessage.setText(getString(resId));
|
errorMessage.setText(getString(resId));
|
||||||
errorMessage.setTextColor(ContextCompat.getColor(this, colorResId));
|
errorMessage.setTextColor(ContextCompat.getColor(this, colorResId));
|
||||||
errorMessageContainer.setVisibility(View.VISIBLE);
|
errorMessageContainer.setVisibility(VISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private AppCompatDelegate getDelegate() {
|
private AppCompatDelegate getDelegate() {
|
||||||
|
|
@ -250,7 +360,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
@Override
|
@Override
|
||||||
public void afterTextChanged(Editable editable) {
|
public void afterTextChanged(Editable editable) {
|
||||||
boolean enabled = usernameEdit.getText().length() != 0 && passwordEdit.getText().length() != 0
|
boolean enabled = usernameEdit.getText().length() != 0 && passwordEdit.getText().length() != 0
|
||||||
&& (BuildConfig.DEBUG || twoFactorEdit.getText().length() != 0 || twoFactorEdit.getVisibility() != View.VISIBLE);
|
&& (BuildConfig.DEBUG || twoFactorEdit.getText().length() != 0 || twoFactorEdit.getVisibility() != VISIBLE);
|
||||||
loginButton.setEnabled(enabled);
|
loginButton.setEnabled(enabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,40 @@
|
||||||
package fr.free.nrw.commons.auth;
|
package fr.free.nrw.commons.auth;
|
||||||
|
|
||||||
import android.accounts.AccountAuthenticatorResponse;
|
import android.accounts.AccountAuthenticatorResponse;
|
||||||
import android.accounts.AccountManager;
|
|
||||||
import android.app.ProgressDialog;
|
import android.app.ProgressDialog;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.mwapi.EventLog;
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
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 fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE;
|
||||||
|
|
||||||
class LoginTask extends AsyncTask<String, String, String> {
|
class LoginTask extends AsyncTask<String, String, String> {
|
||||||
|
|
||||||
private LoginActivity loginActivity;
|
private LoginActivity loginActivity;
|
||||||
private String username;
|
private String username;
|
||||||
private String password;
|
private String password;
|
||||||
private String twoFactorCode = "";
|
private String twoFactorCode = "";
|
||||||
private CommonsApplication app;
|
private AccountUtil accountUtil;
|
||||||
|
private MediaWikiApi mwApi;
|
||||||
|
|
||||||
public LoginTask(LoginActivity loginActivity, String username, String password, String twoFactorCode) {
|
public LoginTask(LoginActivity loginActivity, String username, String password,
|
||||||
|
String twoFactorCode, AccountUtil accountUtil,
|
||||||
|
MediaWikiApi mwApi, SharedPreferences prefs) {
|
||||||
this.loginActivity = loginActivity;
|
this.loginActivity = loginActivity;
|
||||||
this.username = username;
|
this.username = username;
|
||||||
this.password = password;
|
this.password = password;
|
||||||
this.twoFactorCode = twoFactorCode;
|
this.twoFactorCode = twoFactorCode;
|
||||||
app = CommonsApplication.getInstance();
|
this.accountUtil = accountUtil;
|
||||||
|
this.mwApi = mwApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -44,9 +52,9 @@ class LoginTask extends AsyncTask<String, String, String> {
|
||||||
protected String doInBackground(String... params) {
|
protected String doInBackground(String... params) {
|
||||||
try {
|
try {
|
||||||
if (twoFactorCode.isEmpty()) {
|
if (twoFactorCode.isEmpty()) {
|
||||||
return app.getMWApi().login(username, password);
|
return mwApi.login(username, password);
|
||||||
} else {
|
} else {
|
||||||
return app.getMWApi().login(username, password, twoFactorCode);
|
return mwApi.login(username, password, twoFactorCode);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// Do something better!
|
// Do something better!
|
||||||
|
|
@ -59,11 +67,6 @@ class LoginTask extends AsyncTask<String, String, String> {
|
||||||
super.onPostExecute(result);
|
super.onPostExecute(result);
|
||||||
Timber.d("Login done!");
|
Timber.d("Login done!");
|
||||||
|
|
||||||
EventLog.schema(CommonsApplication.EVENT_LOGIN_ATTEMPT)
|
|
||||||
.param("username", username)
|
|
||||||
.param("result", result)
|
|
||||||
.log();
|
|
||||||
|
|
||||||
if (result.equals("PASS")) {
|
if (result.equals("PASS")) {
|
||||||
handlePassResult();
|
handlePassResult();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -79,16 +82,16 @@ class LoginTask extends AsyncTask<String, String, String> {
|
||||||
Bundle extras = loginActivity.getIntent().getExtras();
|
Bundle extras = loginActivity.getIntent().getExtras();
|
||||||
if (extras != null) {
|
if (extras != null) {
|
||||||
Timber.d("Bundle of extras: %s", extras);
|
Timber.d("Bundle of extras: %s", extras);
|
||||||
response = extras.getParcelable(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE);
|
response = extras.getParcelable(KEY_ACCOUNT_AUTHENTICATOR_RESPONSE);
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
Bundle authResult = new Bundle();
|
Bundle authResult = new Bundle();
|
||||||
authResult.putString(AccountManager.KEY_ACCOUNT_NAME, username);
|
authResult.putString(KEY_ACCOUNT_NAME, username);
|
||||||
authResult.putString(AccountManager.KEY_ACCOUNT_TYPE, AccountUtil.accountType());
|
authResult.putString(KEY_ACCOUNT_TYPE, ACCOUNT_TYPE);
|
||||||
response.onResult(authResult);
|
response.onResult(authResult);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AccountUtil.createAccount(response, username, password);
|
accountUtil.createAccount(response, username, password);
|
||||||
loginActivity.startMainActivity();
|
loginActivity.startMainActivity();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
package fr.free.nrw.commons.auth;
|
||||||
|
|
||||||
|
import android.accounts.Account;
|
||||||
|
import android.accounts.AccountManager;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
|
import io.reactivex.Completable;
|
||||||
|
import io.reactivex.Observable;
|
||||||
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manage the current logged in user session.
|
||||||
|
*/
|
||||||
|
public class SessionManager {
|
||||||
|
private final Context context;
|
||||||
|
private final MediaWikiApi mediaWikiApi;
|
||||||
|
private Account currentAccount; // Unlike a savings account... ;-)
|
||||||
|
private SharedPreferences sharedPreferences;
|
||||||
|
|
||||||
|
public SessionManager(Context context, MediaWikiApi mediaWikiApi, SharedPreferences sharedPreferences) {
|
||||||
|
this.context = context;
|
||||||
|
this.mediaWikiApi = mediaWikiApi;
|
||||||
|
this.currentAccount = null;
|
||||||
|
this.sharedPreferences = sharedPreferences;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Account|null
|
||||||
|
*/
|
||||||
|
public Account getCurrentAccount() {
|
||||||
|
if (currentAccount == null) {
|
||||||
|
AccountManager accountManager = AccountManager.get(context);
|
||||||
|
Account[] allAccounts = accountManager.getAccountsByType(ACCOUNT_TYPE);
|
||||||
|
if (allAccounts.length != 0) {
|
||||||
|
currentAccount = allAccounts[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return currentAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean revalidateAuthToken() {
|
||||||
|
AccountManager accountManager = AccountManager.get(context);
|
||||||
|
Account curAccount = getCurrentAccount();
|
||||||
|
|
||||||
|
if (curAccount == null) {
|
||||||
|
return false; // This should never happen
|
||||||
|
}
|
||||||
|
|
||||||
|
accountManager.invalidateAuthToken(ACCOUNT_TYPE, mediaWikiApi.getAuthCookie());
|
||||||
|
String authCookie = getAuthCookie();
|
||||||
|
|
||||||
|
if (authCookie == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
mediaWikiApi.setAuthCookie(authCookie);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAuthCookie() {
|
||||||
|
boolean isLoggedIn = sharedPreferences.getBoolean("isUserLoggedIn", false);
|
||||||
|
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
Timber.e("User is not logged in");
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
String authCookie = sharedPreferences.getString("getAuthCookie", null);
|
||||||
|
if (authCookie == null) {
|
||||||
|
Timber.e("Auth cookie is null even after login");
|
||||||
|
}
|
||||||
|
return authCookie;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Completable clearAllAccounts() {
|
||||||
|
AccountManager accountManager = AccountManager.get(context);
|
||||||
|
Account[] allAccounts = accountManager.getAccountsByType(ACCOUNT_TYPE);
|
||||||
|
return Completable.fromObservable(Observable.fromArray(allAccounts)
|
||||||
|
.map(a -> accountManager.removeAccount(a, null, null).getResult()))
|
||||||
|
.doOnComplete(() -> currentAccount = null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,6 @@ import android.webkit.WebViewClient;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
import fr.free.nrw.commons.BuildConfig;
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
import fr.free.nrw.commons.theme.BaseActivity;
|
import fr.free.nrw.commons.theme.BaseActivity;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
|
@ -39,11 +38,8 @@ public class SignupActivity extends BaseActivity {
|
||||||
//Signup success, so clear cookies, notify user, and load LoginActivity again
|
//Signup success, so clear cookies, notify user, and load LoginActivity again
|
||||||
Timber.d("Overriding URL %s", url);
|
Timber.d("Overriding URL %s", url);
|
||||||
|
|
||||||
Toast toast = Toast.makeText(
|
Toast toast = Toast.makeText(SignupActivity.this,
|
||||||
CommonsApplication.getInstance(),
|
"Account created!", Toast.LENGTH_LONG);
|
||||||
"Account created!",
|
|
||||||
Toast.LENGTH_LONG
|
|
||||||
);
|
|
||||||
toast.show();
|
toast.show();
|
||||||
// terminate on task completion.
|
// terminate on task completion.
|
||||||
finish();
|
finish();
|
||||||
|
|
|
||||||
|
|
@ -5,51 +5,37 @@ import android.accounts.Account;
|
||||||
import android.accounts.AccountAuthenticatorResponse;
|
import android.accounts.AccountAuthenticatorResponse;
|
||||||
import android.accounts.AccountManager;
|
import android.accounts.AccountManager;
|
||||||
import android.accounts.NetworkErrorException;
|
import android.accounts.NetworkErrorException;
|
||||||
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
import java.io.IOException;
|
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
|
||||||
|
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE;
|
||||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE;
|
||||||
|
|
||||||
import static android.accounts.AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION;
|
|
||||||
import static android.accounts.AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE;
|
|
||||||
import static android.accounts.AccountManager.KEY_ACCOUNT_NAME;
|
|
||||||
import static android.accounts.AccountManager.KEY_ACCOUNT_TYPE;
|
|
||||||
import static android.accounts.AccountManager.KEY_AUTHTOKEN;
|
|
||||||
import static android.accounts.AccountManager.KEY_BOOLEAN_RESULT;
|
|
||||||
import static android.accounts.AccountManager.KEY_ERROR_CODE;
|
|
||||||
import static android.accounts.AccountManager.KEY_ERROR_MESSAGE;
|
|
||||||
import static android.accounts.AccountManager.KEY_INTENT;
|
|
||||||
import static fr.free.nrw.commons.auth.LoginActivity.PARAM_USERNAME;
|
|
||||||
|
|
||||||
public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
|
public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
|
||||||
|
private static final String[] SYNC_AUTHORITIES = {ContributionsContentProvider.CONTRIBUTION_AUTHORITY, ModificationsContentProvider.MODIFICATIONS_AUTHORITY};
|
||||||
|
|
||||||
private Context context;
|
@NonNull
|
||||||
|
private final Context context;
|
||||||
|
|
||||||
WikiAccountAuthenticator(Context context) {
|
public WikiAccountAuthenticator(@NonNull Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
this.context = context;
|
this.context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Bundle unsupportedOperation() {
|
@Override
|
||||||
|
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
|
||||||
Bundle bundle = new Bundle();
|
Bundle bundle = new Bundle();
|
||||||
bundle.putInt(KEY_ERROR_CODE, ERROR_CODE_UNSUPPORTED_OPERATION);
|
bundle.putString("test", "editProperties");
|
||||||
|
|
||||||
// HACK: the docs indicate that this is a required key bit it's not displayed to the user.
|
|
||||||
bundle.putString(KEY_ERROR_MESSAGE, "");
|
|
||||||
|
|
||||||
return bundle;
|
return bundle;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean supportedAccountType(@Nullable String type) {
|
|
||||||
return AccountUtil.accountType().equals(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Bundle addAccount(@NonNull AccountAuthenticatorResponse response,
|
public Bundle addAccount(@NonNull AccountAuthenticatorResponse response,
|
||||||
@NonNull String accountType, @Nullable String authTokenType,
|
@NonNull String accountType, @Nullable String authTokenType,
|
||||||
|
|
@ -57,87 +43,48 @@ public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
|
||||||
throws NetworkErrorException {
|
throws NetworkErrorException {
|
||||||
|
|
||||||
if (!supportedAccountType(accountType)) {
|
if (!supportedAccountType(accountType)) {
|
||||||
return unsupportedOperation();
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putString("test", "addAccount");
|
||||||
|
return bundle;
|
||||||
}
|
}
|
||||||
|
|
||||||
return addAccount(response);
|
return addAccount(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Bundle addAccount(AccountAuthenticatorResponse response) {
|
|
||||||
Intent Intent = new Intent(context, LoginActivity.class);
|
|
||||||
Intent.putExtra(KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
|
|
||||||
|
|
||||||
Bundle bundle = new Bundle();
|
|
||||||
bundle.putParcelable(KEY_INTENT, Intent);
|
|
||||||
|
|
||||||
return bundle;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Bundle confirmCredentials(@NonNull AccountAuthenticatorResponse response,
|
public Bundle confirmCredentials(@NonNull AccountAuthenticatorResponse response,
|
||||||
@NonNull Account account, @Nullable Bundle options)
|
@NonNull Account account, @Nullable Bundle options)
|
||||||
throws NetworkErrorException {
|
throws NetworkErrorException {
|
||||||
return unsupportedOperation();
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putString("test", "confirmCredentials");
|
||||||
|
return bundle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
|
public Bundle getAuthToken(@NonNull AccountAuthenticatorResponse response,
|
||||||
return unsupportedOperation();
|
@NonNull Account account, @NonNull String authTokenType,
|
||||||
}
|
@Nullable Bundle options)
|
||||||
|
throws NetworkErrorException {
|
||||||
private String getAuthCookie(String username, String password) throws IOException {
|
Bundle bundle = new Bundle();
|
||||||
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
|
bundle.putString("test", "getAuthToken");
|
||||||
//TODO add 2fa support here
|
|
||||||
String result = api.login(username, password);
|
|
||||||
if (result.equals("PASS")) {
|
|
||||||
return api.getAuthCookie();
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account,
|
|
||||||
String authTokenType, Bundle options) throws NetworkErrorException {
|
|
||||||
// Extract the username and password from the Account Manager, and ask
|
|
||||||
// the server for an appropriate AuthToken.
|
|
||||||
final AccountManager am = AccountManager.get(context);
|
|
||||||
final String password = am.getPassword(account);
|
|
||||||
if (password != null) {
|
|
||||||
String authCookie;
|
|
||||||
try {
|
|
||||||
authCookie = getAuthCookie(account.name, password);
|
|
||||||
} catch (IOException e) {
|
|
||||||
// Network error!
|
|
||||||
e.printStackTrace();
|
|
||||||
throw new NetworkErrorException(e);
|
|
||||||
}
|
|
||||||
if (authCookie != null) {
|
|
||||||
final Bundle result = new Bundle();
|
|
||||||
result.putString(KEY_ACCOUNT_NAME, account.name);
|
|
||||||
result.putString(KEY_ACCOUNT_TYPE, AccountUtil.accountType());
|
|
||||||
result.putString(KEY_AUTHTOKEN, authCookie);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we get here, then we couldn't access the user's password - so we
|
|
||||||
// need to re-prompt them for their credentials. We do that by creating
|
|
||||||
// an intent to display our AuthenticatorActivity panel.
|
|
||||||
final Intent intent = new Intent(context, LoginActivity.class);
|
|
||||||
intent.putExtra(PARAM_USERNAME, account.name);
|
|
||||||
intent.putExtra(KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
|
|
||||||
final Bundle bundle = new Bundle();
|
|
||||||
bundle.putParcelable(KEY_INTENT, intent);
|
|
||||||
return bundle;
|
return bundle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public String getAuthTokenLabel(@NonNull String authTokenType) {
|
public String getAuthTokenLabel(@NonNull String authTokenType) {
|
||||||
//Note: the wikipedia app actually returns a string here....
|
return supportedAccountType(authTokenType) ? AUTH_TOKEN_TYPE : null;
|
||||||
//return supportedAccountType(authTokenType) ? context.getString(R.string.wikimedia) : null;
|
}
|
||||||
return null;
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public Bundle updateCredentials(@NonNull AccountAuthenticatorResponse response,
|
||||||
|
@NonNull Account account, @Nullable String authTokenType,
|
||||||
|
@Nullable Bundle options)
|
||||||
|
throws NetworkErrorException {
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putString("test", "updateCredentials");
|
||||||
|
return bundle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
|
@ -146,16 +93,50 @@ public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
|
||||||
@NonNull Account account, @NonNull String[] features)
|
@NonNull Account account, @NonNull String[] features)
|
||||||
throws NetworkErrorException {
|
throws NetworkErrorException {
|
||||||
Bundle bundle = new Bundle();
|
Bundle bundle = new Bundle();
|
||||||
bundle.putBoolean(KEY_BOOLEAN_RESULT, false);
|
bundle.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
|
||||||
return bundle;
|
return bundle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
private boolean supportedAccountType(@Nullable String type) {
|
||||||
@Override
|
return ACCOUNT_TYPE.equals(type);
|
||||||
public Bundle updateCredentials(@NonNull AccountAuthenticatorResponse response,
|
|
||||||
@NonNull Account account, @Nullable String authTokenType,
|
|
||||||
@Nullable Bundle options) throws NetworkErrorException {
|
|
||||||
return unsupportedOperation();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Bundle addAccount(AccountAuthenticatorResponse response) {
|
||||||
|
Intent intent = new Intent(context, LoginActivity.class);
|
||||||
|
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
|
||||||
|
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
|
||||||
|
|
||||||
|
return bundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Bundle unsupportedOperation() {
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putInt(AccountManager.KEY_ERROR_CODE, AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION);
|
||||||
|
|
||||||
|
// HACK: the docs indicate that this is a required key bit it's not displayed to the user.
|
||||||
|
bundle.putString(AccountManager.KEY_ERROR_MESSAGE, "");
|
||||||
|
|
||||||
|
return bundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Bundle getAccountRemovalAllowed(AccountAuthenticatorResponse response,
|
||||||
|
Account account) throws NetworkErrorException {
|
||||||
|
Bundle result = super.getAccountRemovalAllowed(response, account);
|
||||||
|
|
||||||
|
if (result.containsKey(AccountManager.KEY_BOOLEAN_RESULT)
|
||||||
|
&& !result.containsKey(AccountManager.KEY_INTENT)) {
|
||||||
|
boolean allowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT);
|
||||||
|
|
||||||
|
if (allowed) {
|
||||||
|
for (String auth : SYNC_AUTHORITIES) {
|
||||||
|
ContentResolver.cancelSync(account, auth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,26 @@
|
||||||
package fr.free.nrw.commons.auth;
|
package fr.free.nrw.commons.auth;
|
||||||
|
|
||||||
import android.accounts.AccountManager;
|
import android.accounts.AbstractAccountAuthenticator;
|
||||||
import android.app.Service;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
public class WikiAccountAuthenticatorService extends Service {
|
import fr.free.nrw.commons.di.CommonsDaggerService;
|
||||||
|
|
||||||
|
public class WikiAccountAuthenticatorService extends CommonsDaggerService {
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private AbstractAccountAuthenticator authenticator;
|
||||||
|
|
||||||
private static WikiAccountAuthenticator wikiAccountAuthenticator = null;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IBinder onBind(Intent intent) {
|
public void onCreate() {
|
||||||
if (!intent.getAction().equals(AccountManager.ACTION_AUTHENTICATOR_INTENT)) {
|
super.onCreate();
|
||||||
return null;
|
authenticator = new WikiAccountAuthenticator(this);
|
||||||
}
|
|
||||||
|
|
||||||
if (wikiAccountAuthenticator == null) {
|
|
||||||
wikiAccountAuthenticator = new WikiAccountAuthenticator(this);
|
|
||||||
}
|
|
||||||
return wikiAccountAuthenticator.getIBinder();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent intent) {
|
||||||
|
return authenticator == null ? null : authenticator.getIBinder();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
package fr.free.nrw.commons.category;
|
package fr.free.nrw.commons.category;
|
||||||
|
|
||||||
import android.content.ContentProviderClient;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.support.v4.app.Fragment;
|
|
||||||
import android.support.v7.app.AlertDialog;
|
import android.support.v7.app.AlertDialog;
|
||||||
import android.support.v7.widget.LinearLayoutManager;
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
|
@ -31,11 +29,14 @@ import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
|
||||||
import butterknife.BindView;
|
import butterknife.BindView;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.data.Category;
|
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||||
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
import fr.free.nrw.commons.upload.MwVolleyApi;
|
import fr.free.nrw.commons.upload.MwVolleyApi;
|
||||||
import fr.free.nrw.commons.utils.StringSortingUtils;
|
import fr.free.nrw.commons.utils.StringSortingUtils;
|
||||||
import io.reactivex.Observable;
|
import io.reactivex.Observable;
|
||||||
|
|
@ -45,12 +46,11 @@ import timber.log.Timber;
|
||||||
|
|
||||||
import static android.view.KeyEvent.ACTION_UP;
|
import static android.view.KeyEvent.ACTION_UP;
|
||||||
import static android.view.KeyEvent.KEYCODE_BACK;
|
import static android.view.KeyEvent.KEYCODE_BACK;
|
||||||
import static fr.free.nrw.commons.category.CategoryContentProvider.AUTHORITY;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays the category suggestion and selection screen. Category search is initiated here.
|
* Displays the category suggestion and selection screen. Category search is initiated here.
|
||||||
*/
|
*/
|
||||||
public class CategorizationFragment extends Fragment {
|
public class CategorizationFragment extends CommonsDaggerSupportFragment {
|
||||||
|
|
||||||
public static final int SEARCH_CATS_LIMIT = 25;
|
public static final int SEARCH_CATS_LIMIT = 25;
|
||||||
|
|
||||||
|
|
@ -65,16 +65,19 @@ public class CategorizationFragment extends Fragment {
|
||||||
@BindView(R.id.categoriesExplanation)
|
@BindView(R.id.categoriesExplanation)
|
||||||
TextView categoriesSkip;
|
TextView categoriesSkip;
|
||||||
|
|
||||||
|
@Inject MediaWikiApi mwApi;
|
||||||
|
@Inject @Named("default_preferences") SharedPreferences prefs;
|
||||||
|
@Inject CategoryDao categoryDao;
|
||||||
|
|
||||||
private RVRendererAdapter<CategoryItem> categoriesAdapter;
|
private RVRendererAdapter<CategoryItem> categoriesAdapter;
|
||||||
private OnCategoriesSaveHandler onCategoriesSaveHandler;
|
private OnCategoriesSaveHandler onCategoriesSaveHandler;
|
||||||
private HashMap<String, ArrayList<String>> categoriesCache;
|
private HashMap<String, ArrayList<String>> categoriesCache;
|
||||||
private List<CategoryItem> selectedCategories = new ArrayList<>();
|
private List<CategoryItem> selectedCategories = new ArrayList<>();
|
||||||
private ContentProviderClient databaseClient;
|
|
||||||
|
|
||||||
private final CategoriesAdapterFactory adapterFactory = new CategoriesAdapterFactory(item -> {
|
private final CategoriesAdapterFactory adapterFactory = new CategoriesAdapterFactory(item -> {
|
||||||
if (item.isSelected()) {
|
if (item.isSelected()) {
|
||||||
selectedCategories.add(item);
|
selectedCategories.add(item);
|
||||||
updateCategoryCount(item, databaseClient);
|
updateCategoryCount(item);
|
||||||
} else {
|
} else {
|
||||||
selectedCategories.remove(item);
|
selectedCategories.remove(item);
|
||||||
}
|
}
|
||||||
|
|
@ -88,13 +91,6 @@ public class CategorizationFragment extends Fragment {
|
||||||
|
|
||||||
categoriesList.setLayoutManager(new LinearLayoutManager(getContext()));
|
categoriesList.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||||
|
|
||||||
RxView.clicks(categoriesSkip)
|
|
||||||
.takeUntil(RxView.detaches(categoriesSkip))
|
|
||||||
.subscribe(o -> {
|
|
||||||
getActivity().onBackPressed();
|
|
||||||
getActivity().finish();
|
|
||||||
});
|
|
||||||
|
|
||||||
ArrayList<CategoryItem> items = new ArrayList<>();
|
ArrayList<CategoryItem> items = new ArrayList<>();
|
||||||
categoriesCache = new HashMap<>();
|
categoriesCache = new HashMap<>();
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
|
|
@ -139,12 +135,6 @@ public class CategorizationFragment extends Fragment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
databaseClient.release();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(Bundle outState) {
|
public void onSaveInstanceState(Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
|
|
@ -180,7 +170,6 @@ public class CategorizationFragment extends Fragment {
|
||||||
setHasOptionsMenu(true);
|
setHasOptionsMenu(true);
|
||||||
onCategoriesSaveHandler = (OnCategoriesSaveHandler) getActivity();
|
onCategoriesSaveHandler = (OnCategoriesSaveHandler) getActivity();
|
||||||
getActivity().setTitle(R.string.categories_activity_title);
|
getActivity().setTitle(R.string.categories_activity_title);
|
||||||
databaseClient = getActivity().getContentResolver().acquireContentProviderClient(AUTHORITY);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateCategoryList(String filter) {
|
private void updateCategoryList(String filter) {
|
||||||
|
|
@ -205,7 +194,9 @@ public class CategorizationFragment extends Fragment {
|
||||||
.sorted(sortBySimilarity(filter))
|
.sorted(sortBySimilarity(filter))
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
s -> categoriesAdapter.add(s), Timber::e, () -> {
|
s -> categoriesAdapter.add(s),
|
||||||
|
Timber::e,
|
||||||
|
() -> {
|
||||||
categoriesAdapter.notifyDataSetChanged();
|
categoriesAdapter.notifyDataSetChanged();
|
||||||
categoriesSearchInProgress.setVisibility(View.GONE);
|
categoriesSearchInProgress.setVisibility(View.GONE);
|
||||||
|
|
||||||
|
|
@ -253,16 +244,15 @@ public class CategorizationFragment extends Fragment {
|
||||||
|
|
||||||
private Observable<CategoryItem> titleCategories() {
|
private Observable<CategoryItem> titleCategories() {
|
||||||
//Retrieve the title that was saved when user tapped submit icon
|
//Retrieve the title that was saved when user tapped submit icon
|
||||||
SharedPreferences titleDesc = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
String title = prefs.getString("Title", "");
|
||||||
String title = titleDesc.getString("Title", "");
|
|
||||||
|
|
||||||
return CommonsApplication.getInstance().getMWApi()
|
return mwApi
|
||||||
.searchTitles(title, SEARCH_CATS_LIMIT)
|
.searchTitles(title, SEARCH_CATS_LIMIT)
|
||||||
.map(name -> new CategoryItem(name, false));
|
.map(name -> new CategoryItem(name, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Observable<CategoryItem> recentCategories() {
|
private Observable<CategoryItem> recentCategories() {
|
||||||
return Observable.fromIterable(Category.recentCategories(databaseClient, SEARCH_CATS_LIMIT))
|
return Observable.fromIterable(categoryDao.recentCategories(SEARCH_CATS_LIMIT))
|
||||||
.map(s -> new CategoryItem(s, false));
|
.map(s -> new CategoryItem(s, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -279,7 +269,7 @@ public class CategorizationFragment extends Fragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
//otherwise, search API for matching categories
|
//otherwise, search API for matching categories
|
||||||
return CommonsApplication.getInstance().getMWApi()
|
return mwApi
|
||||||
.allCategories(term, SEARCH_CATS_LIMIT)
|
.allCategories(term, SEARCH_CATS_LIMIT)
|
||||||
.map(name -> new CategoryItem(name, false));
|
.map(name -> new CategoryItem(name, false));
|
||||||
}
|
}
|
||||||
|
|
@ -290,7 +280,7 @@ public class CategorizationFragment extends Fragment {
|
||||||
return Observable.empty();
|
return Observable.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
return CommonsApplication.getInstance().getMWApi()
|
return mwApi
|
||||||
.searchCategories(term, SEARCH_CATS_LIMIT)
|
.searchCategories(term, SEARCH_CATS_LIMIT)
|
||||||
.map(s -> new CategoryItem(s, false));
|
.map(s -> new CategoryItem(s, false));
|
||||||
}
|
}
|
||||||
|
|
@ -312,24 +302,16 @@ public class CategorizationFragment extends Fragment {
|
||||||
|| item.matches("(.*)needing(.*)") || item.matches("(.*)taken on(.*)"));
|
|| item.matches("(.*)needing(.*)") || item.matches("(.*)taken on(.*)"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateCategoryCount(CategoryItem item, ContentProviderClient client) {
|
private void updateCategoryCount(CategoryItem item) {
|
||||||
Category cat = lookupCategory(item.getName());
|
Category category = categoryDao.find(item.getName());
|
||||||
cat.incTimesUsed();
|
|
||||||
cat.save(client);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Category lookupCategory(String name) {
|
// Newly used category...
|
||||||
Category cat = Category.find(databaseClient, name);
|
if (category == null) {
|
||||||
|
category = new Category(null, item.getName(), new Date(), 0);
|
||||||
if (cat == null) {
|
|
||||||
// Newly used category...
|
|
||||||
cat = new Category();
|
|
||||||
cat.setName(name);
|
|
||||||
cat.setLastUsed(new Date());
|
|
||||||
cat.setTimesUsed(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return cat;
|
category.incTimesUsed();
|
||||||
|
categoryDao.save(category);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getCurrentSelectedCount() {
|
public int getCurrentSelectedCount() {
|
||||||
|
|
|
||||||
96
app/src/main/java/fr/free/nrw/commons/category/Category.java
Normal file
96
app/src/main/java/fr/free/nrw/commons/category/Category.java
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
package fr.free.nrw.commons.category;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a category
|
||||||
|
*/
|
||||||
|
public class Category {
|
||||||
|
private Uri contentUri;
|
||||||
|
private String name;
|
||||||
|
private Date lastUsed;
|
||||||
|
private int timesUsed;
|
||||||
|
|
||||||
|
public Category() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public Category(Uri contentUri, String name, Date lastUsed, int timesUsed) {
|
||||||
|
this.contentUri = contentUri;
|
||||||
|
this.name = name;
|
||||||
|
this.lastUsed = lastUsed;
|
||||||
|
this.timesUsed = timesUsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets name
|
||||||
|
*
|
||||||
|
* @return name
|
||||||
|
*/
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifies name
|
||||||
|
*
|
||||||
|
* @param name Category name
|
||||||
|
*/
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets last used date
|
||||||
|
*
|
||||||
|
* @return Last used date
|
||||||
|
*/
|
||||||
|
public Date getLastUsed() {
|
||||||
|
// warning: Date objects are mutable.
|
||||||
|
return (Date)lastUsed.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates new last used date
|
||||||
|
*/
|
||||||
|
private void touch() {
|
||||||
|
lastUsed = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets no. of times the category is used
|
||||||
|
*
|
||||||
|
* @return no. of times used
|
||||||
|
*/
|
||||||
|
public int getTimesUsed() {
|
||||||
|
return timesUsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments timesUsed by 1 and sets last used date as now.
|
||||||
|
*/
|
||||||
|
public void incTimesUsed() {
|
||||||
|
timesUsed++;
|
||||||
|
touch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the content URI for this category
|
||||||
|
*
|
||||||
|
* @return content URI
|
||||||
|
*/
|
||||||
|
public Uri getContentUri() {
|
||||||
|
return contentUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifies the content URI - marking this category as already saved in the database
|
||||||
|
*
|
||||||
|
* @param contentUri the content URI
|
||||||
|
*/
|
||||||
|
public void setContentUri(Uri contentUri) {
|
||||||
|
this.contentUri = contentUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package fr.free.nrw.commons.category;
|
package fr.free.nrw.commons.category;
|
||||||
|
|
||||||
import android.content.ContentProvider;
|
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.content.UriMatcher;
|
import android.content.UriMatcher;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
|
|
@ -10,16 +9,18 @@ import android.net.Uri;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import fr.free.nrw.commons.data.DBOpenHelper;
|
import fr.free.nrw.commons.data.DBOpenHelper;
|
||||||
|
import fr.free.nrw.commons.di.CommonsDaggerContentProvider;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
import static android.content.UriMatcher.NO_MATCH;
|
import static android.content.UriMatcher.NO_MATCH;
|
||||||
import static fr.free.nrw.commons.data.Category.Table.ALL_FIELDS;
|
import static fr.free.nrw.commons.category.CategoryDao.Table.ALL_FIELDS;
|
||||||
import static fr.free.nrw.commons.data.Category.Table.COLUMN_ID;
|
import static fr.free.nrw.commons.category.CategoryDao.Table.COLUMN_ID;
|
||||||
import static fr.free.nrw.commons.data.Category.Table.TABLE_NAME;
|
import static fr.free.nrw.commons.category.CategoryDao.Table.TABLE_NAME;
|
||||||
|
|
||||||
public class CategoryContentProvider extends ContentProvider {
|
public class CategoryContentProvider extends CommonsDaggerContentProvider {
|
||||||
|
|
||||||
public static final String AUTHORITY = "fr.free.nrw.commons.categories.contentprovider";
|
public static final String AUTHORITY = "fr.free.nrw.commons.categories.contentprovider";
|
||||||
// For URI matcher
|
// For URI matcher
|
||||||
|
|
@ -36,19 +37,11 @@ public class CategoryContentProvider extends ContentProvider {
|
||||||
uriMatcher.addURI(AUTHORITY, BASE_PATH + "/#", CATEGORIES_ID);
|
uriMatcher.addURI(AUTHORITY, BASE_PATH + "/#", CATEGORIES_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
private DBOpenHelper dbOpenHelper;
|
|
||||||
|
|
||||||
public static Uri uriForId(int id) {
|
public static Uri uriForId(int id) {
|
||||||
return Uri.parse(BASE_URI.toString() + "/" + id);
|
return Uri.parse(BASE_URI.toString() + "/" + id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
@Inject DBOpenHelper dbOpenHelper;
|
||||||
@Override
|
|
||||||
public boolean onCreate() {
|
|
||||||
CommonsApplication app = ((CommonsApplication) getContext().getApplicationContext());
|
|
||||||
dbOpenHelper = app.getDBOpenHelper();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
@SuppressWarnings("ConstantConditions")
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -1,115 +1,64 @@
|
||||||
package fr.free.nrw.commons.data;
|
package fr.free.nrw.commons.category;
|
||||||
|
|
||||||
import android.content.ContentProviderClient;
|
import android.content.ContentProviderClient;
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import fr.free.nrw.commons.category.CategoryContentProvider;
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
import javax.inject.Provider;
|
||||||
|
|
||||||
public class Category {
|
public class CategoryDao {
|
||||||
private Uri contentUri;
|
|
||||||
|
|
||||||
private String name;
|
private final Provider<ContentProviderClient> clientProvider;
|
||||||
private Date lastUsed;
|
|
||||||
private int timesUsed;
|
|
||||||
|
|
||||||
// Getters/setters
|
@Inject
|
||||||
public String getName() {
|
public CategoryDao(@Named("category") Provider<ContentProviderClient> clientProvider) {
|
||||||
return name;
|
this.clientProvider = clientProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setName(String name) {
|
public void save(Category category) {
|
||||||
this.name = name;
|
ContentProviderClient db = clientProvider.get();
|
||||||
}
|
|
||||||
|
|
||||||
private Date getLastUsed() {
|
|
||||||
// warning: Date objects are mutable.
|
|
||||||
return (Date)lastUsed.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setLastUsed(Date lastUsed) {
|
|
||||||
// warning: Date objects are mutable.
|
|
||||||
this.lastUsed = (Date)lastUsed.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void touch() {
|
|
||||||
lastUsed = new Date();
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getTimesUsed() {
|
|
||||||
return timesUsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTimesUsed(int timesUsed) {
|
|
||||||
this.timesUsed = timesUsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void incTimesUsed() {
|
|
||||||
timesUsed++;
|
|
||||||
touch();
|
|
||||||
}
|
|
||||||
|
|
||||||
//region Database/content-provider stuff
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Persist category.
|
|
||||||
* @param client ContentProviderClient to handle DB connection
|
|
||||||
*/
|
|
||||||
public void save(ContentProviderClient client) {
|
|
||||||
try {
|
try {
|
||||||
if (contentUri == null) {
|
if (category.getContentUri() == null) {
|
||||||
contentUri = client.insert(CategoryContentProvider.BASE_URI, this.toContentValues());
|
category.setContentUri(db.insert(CategoryContentProvider.BASE_URI, toContentValues(category)));
|
||||||
} else {
|
} else {
|
||||||
client.update(contentUri, toContentValues(), null, null);
|
db.update(category.getContentUri(), toContentValues(category), null, null);
|
||||||
}
|
}
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
|
} finally {
|
||||||
|
db.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ContentValues toContentValues() {
|
|
||||||
ContentValues cv = new ContentValues();
|
|
||||||
cv.put(Table.COLUMN_NAME, getName());
|
|
||||||
cv.put(Table.COLUMN_LAST_USED, getLastUsed().getTime());
|
|
||||||
cv.put(Table.COLUMN_TIMES_USED, getTimesUsed());
|
|
||||||
return cv;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Category fromCursor(Cursor cursor) {
|
|
||||||
// Hardcoding column positions!
|
|
||||||
Category c = new Category();
|
|
||||||
c.contentUri = CategoryContentProvider.uriForId(cursor.getInt(0));
|
|
||||||
c.name = cursor.getString(1);
|
|
||||||
c.lastUsed = new Date(cursor.getLong(2));
|
|
||||||
c.timesUsed = cursor.getInt(3);
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find persisted category in database, based on its name.
|
* Find persisted category in database, based on its name.
|
||||||
* @param client ContentProviderClient to handle DB connection
|
*
|
||||||
* @param name Category's name
|
* @param name Category's name
|
||||||
* @return category from database, or null if not found
|
* @return category from database, or null if not found
|
||||||
*/
|
*/
|
||||||
public static @Nullable Category find(ContentProviderClient client, String name) {
|
@Nullable
|
||||||
|
Category find(String name) {
|
||||||
Cursor cursor = null;
|
Cursor cursor = null;
|
||||||
|
ContentProviderClient db = clientProvider.get();
|
||||||
try {
|
try {
|
||||||
cursor = client.query(
|
cursor = db.query(
|
||||||
CategoryContentProvider.BASE_URI,
|
CategoryContentProvider.BASE_URI,
|
||||||
Category.Table.ALL_FIELDS,
|
Table.ALL_FIELDS,
|
||||||
Category.Table.COLUMN_NAME + "=?",
|
Table.COLUMN_NAME + "=?",
|
||||||
new String[]{name},
|
new String[]{name},
|
||||||
null);
|
null);
|
||||||
if (cursor != null && cursor.moveToFirst()) {
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
return Category.fromCursor(cursor);
|
return fromCursor(cursor);
|
||||||
}
|
}
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
// This feels lazy, but to hell with checked exceptions. :)
|
// This feels lazy, but to hell with checked exceptions. :)
|
||||||
|
|
@ -118,29 +67,32 @@ public class Category {
|
||||||
if (cursor != null) {
|
if (cursor != null) {
|
||||||
cursor.close();
|
cursor.close();
|
||||||
}
|
}
|
||||||
|
db.release();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve recently-used categories, ordered by descending date.
|
* Retrieve recently-used categories, ordered by descending date.
|
||||||
|
*
|
||||||
* @return a list containing recent categories
|
* @return a list containing recent categories
|
||||||
*/
|
*/
|
||||||
public static @NonNull ArrayList<String> recentCategories(ContentProviderClient client, int limit) {
|
@NonNull
|
||||||
ArrayList<String> items = new ArrayList<>();
|
List<String> recentCategories(int limit) {
|
||||||
|
List<String> items = new ArrayList<>();
|
||||||
Cursor cursor = null;
|
Cursor cursor = null;
|
||||||
|
ContentProviderClient db = clientProvider.get();
|
||||||
try {
|
try {
|
||||||
cursor = client.query(
|
cursor = db.query(
|
||||||
CategoryContentProvider.BASE_URI,
|
CategoryContentProvider.BASE_URI,
|
||||||
Category.Table.ALL_FIELDS,
|
Table.ALL_FIELDS,
|
||||||
null,
|
null,
|
||||||
new String[]{},
|
new String[]{},
|
||||||
Category.Table.COLUMN_LAST_USED + " DESC");
|
Table.COLUMN_LAST_USED + " DESC");
|
||||||
// fixme add a limit on the original query instead of falling out of the loop?
|
// fixme add a limit on the original query instead of falling out of the loop?
|
||||||
while (cursor != null && cursor.moveToNext()
|
while (cursor != null && cursor.moveToNext()
|
||||||
&& cursor.getPosition() < limit) {
|
&& cursor.getPosition() < limit) {
|
||||||
Category cat = Category.fromCursor(cursor);
|
items.add(fromCursor(cursor).getName());
|
||||||
items.add(cat.getName());
|
|
||||||
}
|
}
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
|
|
@ -148,17 +100,36 @@ public class Category {
|
||||||
if (cursor != null) {
|
if (cursor != null) {
|
||||||
cursor.close();
|
cursor.close();
|
||||||
}
|
}
|
||||||
|
db.release();
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Category fromCursor(Cursor cursor) {
|
||||||
|
// Hardcoding column positions!
|
||||||
|
return new Category(
|
||||||
|
CategoryContentProvider.uriForId(cursor.getInt(0)),
|
||||||
|
cursor.getString(1),
|
||||||
|
new Date(cursor.getLong(2)),
|
||||||
|
cursor.getInt(3)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ContentValues toContentValues(Category category) {
|
||||||
|
ContentValues cv = new ContentValues();
|
||||||
|
cv.put(CategoryDao.Table.COLUMN_NAME, category.getName());
|
||||||
|
cv.put(CategoryDao.Table.COLUMN_LAST_USED, category.getLastUsed().getTime());
|
||||||
|
cv.put(CategoryDao.Table.COLUMN_TIMES_USED, category.getTimesUsed());
|
||||||
|
return cv;
|
||||||
|
}
|
||||||
|
|
||||||
public static class Table {
|
public static class Table {
|
||||||
public static final String TABLE_NAME = "categories";
|
public static final String TABLE_NAME = "categories";
|
||||||
|
|
||||||
public static final String COLUMN_ID = "_id";
|
public static final String COLUMN_ID = "_id";
|
||||||
public static final String COLUMN_NAME = "name";
|
static final String COLUMN_NAME = "name";
|
||||||
public static final String COLUMN_LAST_USED = "last_used";
|
static final String COLUMN_LAST_USED = "last_used";
|
||||||
public static final String COLUMN_TIMES_USED = "times_used";
|
static final String COLUMN_TIMES_USED = "times_used";
|
||||||
|
|
||||||
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
|
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
|
||||||
public static final String[] ALL_FIELDS = {
|
public static final String[] ALL_FIELDS = {
|
||||||
|
|
@ -168,7 +139,9 @@ public class Category {
|
||||||
COLUMN_TIMES_USED
|
COLUMN_TIMES_USED
|
||||||
};
|
};
|
||||||
|
|
||||||
private static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
|
static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME;
|
||||||
|
|
||||||
|
static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
|
||||||
+ COLUMN_ID + " INTEGER PRIMARY KEY,"
|
+ COLUMN_ID + " INTEGER PRIMARY KEY,"
|
||||||
+ COLUMN_NAME + " STRING,"
|
+ COLUMN_NAME + " STRING,"
|
||||||
+ COLUMN_LAST_USED + " INTEGER,"
|
+ COLUMN_LAST_USED + " INTEGER,"
|
||||||
|
|
@ -180,7 +153,7 @@ public class Category {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void onDelete(SQLiteDatabase db) {
|
public static void onDelete(SQLiteDatabase db) {
|
||||||
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
|
db.execSQL(DROP_TABLE_STATEMENT);
|
||||||
onCreate(db);
|
onCreate(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -208,5 +181,4 @@ public class Category {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//endregion
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,13 +1,8 @@
|
||||||
package fr.free.nrw.commons.contributions;
|
package fr.free.nrw.commons.contributions;
|
||||||
|
|
||||||
import android.content.ContentProviderClient;
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Parcel;
|
import android.os.Parcel;
|
||||||
import android.os.RemoteException;
|
import android.support.annotation.NonNull;
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
@ -16,7 +11,6 @@ import java.util.Locale;
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
import fr.free.nrw.commons.BuildConfig;
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
import fr.free.nrw.commons.CommonsApplication;
|
||||||
import fr.free.nrw.commons.Media;
|
import fr.free.nrw.commons.Media;
|
||||||
import fr.free.nrw.commons.Utils;
|
|
||||||
import fr.free.nrw.commons.settings.Prefs;
|
import fr.free.nrw.commons.settings.Prefs;
|
||||||
|
|
||||||
public class Contribution extends Media {
|
public class Contribution extends Media {
|
||||||
|
|
@ -43,7 +37,6 @@ public class Contribution extends Media {
|
||||||
public static final String SOURCE_GALLERY = "gallery";
|
public static final String SOURCE_GALLERY = "gallery";
|
||||||
public static final String SOURCE_EXTERNAL = "external";
|
public static final String SOURCE_EXTERNAL = "external";
|
||||||
|
|
||||||
private ContentProviderClient client;
|
|
||||||
private Uri contentUri;
|
private Uri contentUri;
|
||||||
private String source;
|
private String source;
|
||||||
private String editSummary;
|
private String editSummary;
|
||||||
|
|
@ -51,24 +44,42 @@ public class Contribution extends Media {
|
||||||
private int state;
|
private int state;
|
||||||
private long transferred;
|
private long transferred;
|
||||||
private String decimalCoords;
|
private String decimalCoords;
|
||||||
|
|
||||||
private boolean isMultiple;
|
private boolean isMultiple;
|
||||||
|
|
||||||
public boolean getMultiple() {
|
public Contribution(Uri contentUri, String filename, Uri localUri, String imageUrl, Date timestamp,
|
||||||
return isMultiple;
|
int state, long dataLength, Date dateUploaded, long transferred,
|
||||||
|
String source, String description, String creator, boolean isMultiple,
|
||||||
|
int width, int height, String license) {
|
||||||
|
super(localUri, imageUrl, filename, description, dataLength, timestamp, dateUploaded, creator);
|
||||||
|
this.contentUri = contentUri;
|
||||||
|
this.state = state;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.transferred = transferred;
|
||||||
|
this.source = source;
|
||||||
|
this.isMultiple = isMultiple;
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
this.license = license;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setMultiple(boolean multiple) {
|
public Contribution(Uri localUri, String imageUrl, String filename, String description, long dataLength,
|
||||||
isMultiple = multiple;
|
Date dateCreated, Date dateUploaded, String creator, String editSummary, String decimalCoords) {
|
||||||
}
|
super(localUri, imageUrl, filename, description, dataLength, dateCreated, dateUploaded, creator);
|
||||||
|
|
||||||
public Contribution(Uri localUri, String remoteUri, String filename, String description, long dataLength, Date dateCreated, Date dateUploaded, String creator, String editSummary, String decimalCoords) {
|
|
||||||
super(localUri, remoteUri, filename, description, dataLength, dateCreated, dateUploaded, creator);
|
|
||||||
this.decimalCoords = decimalCoords;
|
this.decimalCoords = decimalCoords;
|
||||||
this.editSummary = editSummary;
|
this.editSummary = editSummary;
|
||||||
timestamp = new Date(System.currentTimeMillis());
|
timestamp = new Date(System.currentTimeMillis());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Contribution(Parcel in) {
|
||||||
|
super(in);
|
||||||
|
contentUri = in.readParcelable(Uri.class.getClassLoader());
|
||||||
|
source = in.readString();
|
||||||
|
timestamp = (Date) in.readSerializable();
|
||||||
|
state = in.readInt();
|
||||||
|
transferred = in.readLong();
|
||||||
|
isMultiple = in.readInt() == 1;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void writeToParcel(Parcel parcel, int flags) {
|
public void writeToParcel(Parcel parcel, int flags) {
|
||||||
super.writeToParcel(parcel, flags);
|
super.writeToParcel(parcel, flags);
|
||||||
|
|
@ -80,14 +91,12 @@ public class Contribution extends Media {
|
||||||
parcel.writeInt(isMultiple ? 1 : 0);
|
parcel.writeInt(isMultiple ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Contribution(Parcel in) {
|
public boolean getMultiple() {
|
||||||
super(in);
|
return isMultiple;
|
||||||
contentUri = in.readParcelable(Uri.class.getClassLoader());
|
}
|
||||||
source = in.readString();
|
|
||||||
timestamp = (Date) in.readSerializable();
|
public void setMultiple(boolean multiple) {
|
||||||
state = in.readInt();
|
isMultiple = multiple;
|
||||||
transferred = in.readLong();
|
|
||||||
isMultiple = in.readInt() == 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getTransferred() {
|
public long getTransferred() {
|
||||||
|
|
@ -106,10 +115,18 @@ public class Contribution extends Media {
|
||||||
return contentUri;
|
return contentUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setContentUri(Uri contentUri) {
|
||||||
|
this.contentUri = contentUri;
|
||||||
|
}
|
||||||
|
|
||||||
public Date getTimestamp() {
|
public Date getTimestamp() {
|
||||||
return timestamp;
|
return timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setTimestamp(Date timestamp) {
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
public int getState() {
|
public int getState() {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
@ -149,68 +166,12 @@ public class Contribution extends Media {
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer.append("== {{int:license-header}} ==\n")
|
buffer.append("== {{int:license-header}} ==\n")
|
||||||
.append(Utils.licenseTemplateFor(getLicense())).append("\n\n")
|
.append(licenseTemplateFor(getLicense())).append("\n\n")
|
||||||
.append("{{Uploaded from Mobile|platform=Android|version=").append(BuildConfig.VERSION_NAME).append("}}\n")
|
.append("{{Uploaded from Mobile|platform=Android|version=").append(BuildConfig.VERSION_NAME).append("}}\n")
|
||||||
.append(getTrackingTemplates());
|
.append(getTrackingTemplates());
|
||||||
return buffer.toString();
|
return buffer.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setContentProviderClient(ContentProviderClient client) {
|
|
||||||
this.client = client;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void save() {
|
|
||||||
try {
|
|
||||||
if (contentUri == null) {
|
|
||||||
contentUri = client.insert(ContributionsContentProvider.BASE_URI, this.toContentValues());
|
|
||||||
} else {
|
|
||||||
client.update(contentUri, toContentValues(), null, null);
|
|
||||||
}
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void delete() {
|
|
||||||
try {
|
|
||||||
if (contentUri == null) {
|
|
||||||
// noooo
|
|
||||||
throw new RuntimeException("tried to delete item with no content URI");
|
|
||||||
} else {
|
|
||||||
client.delete(contentUri, null, null);
|
|
||||||
}
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public ContentValues toContentValues() {
|
|
||||||
ContentValues cv = new ContentValues();
|
|
||||||
cv.put(Table.COLUMN_FILENAME, getFilename());
|
|
||||||
if (getLocalUri() != null) {
|
|
||||||
cv.put(Table.COLUMN_LOCAL_URI, getLocalUri().toString());
|
|
||||||
}
|
|
||||||
if (getImageUrl() != null) {
|
|
||||||
cv.put(Table.COLUMN_IMAGE_URL, getImageUrl());
|
|
||||||
}
|
|
||||||
if (getDateUploaded() != null) {
|
|
||||||
cv.put(Table.COLUMN_UPLOADED, getDateUploaded().getTime());
|
|
||||||
}
|
|
||||||
cv.put(Table.COLUMN_LENGTH, getDataLength());
|
|
||||||
cv.put(Table.COLUMN_TIMESTAMP, getTimestamp().getTime());
|
|
||||||
cv.put(Table.COLUMN_STATE, getState());
|
|
||||||
cv.put(Table.COLUMN_TRANSFERRED, transferred);
|
|
||||||
cv.put(Table.COLUMN_SOURCE, source);
|
|
||||||
cv.put(Table.COLUMN_DESCRIPTION, description);
|
|
||||||
cv.put(Table.COLUMN_CREATOR, creator);
|
|
||||||
cv.put(Table.COLUMN_MULTIPLE, isMultiple ? 1 : 0);
|
|
||||||
cv.put(Table.COLUMN_WIDTH, width);
|
|
||||||
cv.put(Table.COLUMN_HEIGHT, height);
|
|
||||||
cv.put(Table.COLUMN_LICENSE, license);
|
|
||||||
return cv;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setFilename(String filename) {
|
public void setFilename(String filename) {
|
||||||
this.filename = filename;
|
this.filename = filename;
|
||||||
|
|
@ -224,33 +185,6 @@ public class Contribution extends Media {
|
||||||
timestamp = new Date(System.currentTimeMillis());
|
timestamp = new Date(System.currentTimeMillis());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Contribution fromCursor(Cursor cursor) {
|
|
||||||
// Hardcoding column positions!
|
|
||||||
Contribution c = new Contribution();
|
|
||||||
|
|
||||||
//Check that cursor has a value to avoid CursorIndexOutOfBoundsException
|
|
||||||
if (cursor.getCount() > 0) {
|
|
||||||
c.contentUri = ContributionsContentProvider.uriForId(cursor.getInt(0));
|
|
||||||
c.filename = cursor.getString(1);
|
|
||||||
c.localUri = TextUtils.isEmpty(cursor.getString(2)) ? null : Uri.parse(cursor.getString(2));
|
|
||||||
c.imageUrl = cursor.getString(3);
|
|
||||||
c.timestamp = cursor.getLong(4) == 0 ? null : new Date(cursor.getLong(4));
|
|
||||||
c.state = cursor.getInt(5);
|
|
||||||
c.dataLength = cursor.getLong(6);
|
|
||||||
c.dateUploaded = cursor.getLong(7) == 0 ? null : new Date(cursor.getLong(7));
|
|
||||||
c.transferred = cursor.getLong(8);
|
|
||||||
c.source = cursor.getString(9);
|
|
||||||
c.description = cursor.getString(10);
|
|
||||||
c.creator = cursor.getString(11);
|
|
||||||
c.isMultiple = cursor.getInt(12) == 1;
|
|
||||||
c.width = cursor.getInt(13);
|
|
||||||
c.height = cursor.getInt(14);
|
|
||||||
c.license = cursor.getString(15);
|
|
||||||
}
|
|
||||||
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSource() {
|
public String getSource() {
|
||||||
return source;
|
return source;
|
||||||
}
|
}
|
||||||
|
|
@ -263,118 +197,24 @@ public class Contribution extends Media {
|
||||||
this.localUri = localUri;
|
this.localUri = localUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Table {
|
@NonNull
|
||||||
public static final String TABLE_NAME = "contributions";
|
private String licenseTemplateFor(String license) {
|
||||||
|
switch (license) {
|
||||||
public static final String COLUMN_ID = "_id";
|
case Prefs.Licenses.CC_BY_3:
|
||||||
public static final String COLUMN_FILENAME = "filename";
|
return "{{self|cc-by-3.0}}";
|
||||||
public static final String COLUMN_LOCAL_URI = "local_uri";
|
case Prefs.Licenses.CC_BY_4:
|
||||||
public static final String COLUMN_IMAGE_URL = "image_url";
|
return "{{self|cc-by-4.0}}";
|
||||||
public static final String COLUMN_TIMESTAMP = "timestamp";
|
case Prefs.Licenses.CC_BY_SA_3:
|
||||||
public static final String COLUMN_STATE = "state";
|
return "{{self|cc-by-sa-3.0}}";
|
||||||
public static final String COLUMN_LENGTH = "length";
|
case Prefs.Licenses.CC_BY_SA_4:
|
||||||
public static final String COLUMN_UPLOADED = "uploaded";
|
return "{{self|cc-by-sa-4.0}}";
|
||||||
public static final String COLUMN_TRANSFERRED = "transferred"; // Currently transferred number of bytes
|
case Prefs.Licenses.CC0:
|
||||||
public static final String COLUMN_SOURCE = "source";
|
return "{{self|cc-zero}}";
|
||||||
public static final String COLUMN_DESCRIPTION = "description";
|
case Prefs.Licenses.CC_BY:
|
||||||
public static final String COLUMN_CREATOR = "creator"; // Initial uploader
|
return "{{self|cc-by-3.0}}";
|
||||||
public static final String COLUMN_MULTIPLE = "multiple";
|
case Prefs.Licenses.CC_BY_SA:
|
||||||
public static final String COLUMN_WIDTH = "width";
|
return "{{self|cc-by-sa-3.0}}";
|
||||||
public static final String COLUMN_HEIGHT = "height";
|
|
||||||
public static final String COLUMN_LICENSE = "license";
|
|
||||||
|
|
||||||
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
|
|
||||||
public static final String[] ALL_FIELDS = {
|
|
||||||
COLUMN_ID,
|
|
||||||
COLUMN_FILENAME,
|
|
||||||
COLUMN_LOCAL_URI,
|
|
||||||
COLUMN_IMAGE_URL,
|
|
||||||
COLUMN_TIMESTAMP,
|
|
||||||
COLUMN_STATE,
|
|
||||||
COLUMN_LENGTH,
|
|
||||||
COLUMN_UPLOADED,
|
|
||||||
COLUMN_TRANSFERRED,
|
|
||||||
COLUMN_SOURCE,
|
|
||||||
COLUMN_DESCRIPTION,
|
|
||||||
COLUMN_CREATOR,
|
|
||||||
COLUMN_MULTIPLE,
|
|
||||||
COLUMN_WIDTH,
|
|
||||||
COLUMN_HEIGHT,
|
|
||||||
COLUMN_LICENSE
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
private static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
|
|
||||||
+ "_id INTEGER PRIMARY KEY,"
|
|
||||||
+ "filename STRING,"
|
|
||||||
+ "local_uri STRING,"
|
|
||||||
+ "image_url STRING,"
|
|
||||||
+ "uploaded INTEGER,"
|
|
||||||
+ "timestamp INTEGER,"
|
|
||||||
+ "state INTEGER,"
|
|
||||||
+ "length INTEGER,"
|
|
||||||
+ "transferred INTEGER,"
|
|
||||||
+ "source STRING,"
|
|
||||||
+ "description STRING,"
|
|
||||||
+ "creator STRING,"
|
|
||||||
+ "multiple INTEGER,"
|
|
||||||
+ "width INTEGER,"
|
|
||||||
+ "height INTEGER,"
|
|
||||||
+ "LICENSE STRING"
|
|
||||||
+ ");";
|
|
||||||
|
|
||||||
|
|
||||||
public static void onCreate(SQLiteDatabase db) {
|
|
||||||
db.execSQL(CREATE_TABLE_STATEMENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void onDelete(SQLiteDatabase db) {
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,281 @@
|
||||||
|
package fr.free.nrw.commons.contributions;
|
||||||
|
|
||||||
|
import android.content.ContentProviderClient;
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
import javax.inject.Provider;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.settings.Prefs;
|
||||||
|
|
||||||
|
import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS;
|
||||||
|
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI;
|
||||||
|
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.uriForId;
|
||||||
|
|
||||||
|
public class ContributionDao {
|
||||||
|
/*
|
||||||
|
This sorts in the following order:
|
||||||
|
Currently Uploading
|
||||||
|
Failed (Sorted in ascending order of time added - FIFO)
|
||||||
|
Queued to Upload (Sorted in ascending order of time added - FIFO)
|
||||||
|
Completed (Sorted in descending order of time added)
|
||||||
|
|
||||||
|
This is why Contribution.STATE_COMPLETED is -1.
|
||||||
|
*/
|
||||||
|
static final String CONTRIBUTION_SORT = Table.COLUMN_STATE + " DESC, "
|
||||||
|
+ Table.COLUMN_UPLOADED + " DESC , ("
|
||||||
|
+ Table.COLUMN_TIMESTAMP + " * "
|
||||||
|
+ Table.COLUMN_STATE + ")";
|
||||||
|
|
||||||
|
private final Provider<ContentProviderClient> clientProvider;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public ContributionDao(@Named("contribution") Provider<ContentProviderClient> clientProvider) {
|
||||||
|
this.clientProvider = clientProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
Cursor loadAllContributions() {
|
||||||
|
ContentProviderClient db = clientProvider.get();
|
||||||
|
try {
|
||||||
|
return db.query(BASE_URI, ALL_FIELDS, "", null, CONTRIBUTION_SORT);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
db.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void save(Contribution contribution) {
|
||||||
|
ContentProviderClient db = clientProvider.get();
|
||||||
|
try {
|
||||||
|
if (contribution.getContentUri() == null) {
|
||||||
|
contribution.setContentUri(db.insert(BASE_URI, toContentValues(contribution)));
|
||||||
|
} else {
|
||||||
|
db.update(contribution.getContentUri(), toContentValues(contribution), null, null);
|
||||||
|
}
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
} finally {
|
||||||
|
db.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(Contribution contribution) {
|
||||||
|
ContentProviderClient db = clientProvider.get();
|
||||||
|
try {
|
||||||
|
if (contribution.getContentUri() == null) {
|
||||||
|
// noooo
|
||||||
|
throw new RuntimeException("tried to delete item with no content URI");
|
||||||
|
} else {
|
||||||
|
db.delete(contribution.getContentUri(), null, null);
|
||||||
|
}
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
} finally {
|
||||||
|
db.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentValues toContentValues(Contribution contribution) {
|
||||||
|
ContentValues cv = new ContentValues();
|
||||||
|
cv.put(Table.COLUMN_FILENAME, contribution.getFilename());
|
||||||
|
if (contribution.getLocalUri() != null) {
|
||||||
|
cv.put(Table.COLUMN_LOCAL_URI, contribution.getLocalUri().toString());
|
||||||
|
}
|
||||||
|
if (contribution.getImageUrl() != null) {
|
||||||
|
cv.put(Table.COLUMN_IMAGE_URL, contribution.getImageUrl());
|
||||||
|
}
|
||||||
|
if (contribution.getDateUploaded() != null) {
|
||||||
|
cv.put(Table.COLUMN_UPLOADED, contribution.getDateUploaded().getTime());
|
||||||
|
}
|
||||||
|
cv.put(Table.COLUMN_LENGTH, contribution.getDataLength());
|
||||||
|
cv.put(Table.COLUMN_TIMESTAMP, contribution.getTimestamp().getTime());
|
||||||
|
cv.put(Table.COLUMN_STATE, contribution.getState());
|
||||||
|
cv.put(Table.COLUMN_TRANSFERRED, contribution.getTransferred());
|
||||||
|
cv.put(Table.COLUMN_SOURCE, contribution.getSource());
|
||||||
|
cv.put(Table.COLUMN_DESCRIPTION, contribution.getDescription());
|
||||||
|
cv.put(Table.COLUMN_CREATOR, contribution.getCreator());
|
||||||
|
cv.put(Table.COLUMN_MULTIPLE, contribution.getMultiple() ? 1 : 0);
|
||||||
|
cv.put(Table.COLUMN_WIDTH, contribution.getWidth());
|
||||||
|
cv.put(Table.COLUMN_HEIGHT, contribution.getHeight());
|
||||||
|
cv.put(Table.COLUMN_LICENSE, contribution.getLicense());
|
||||||
|
return cv;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Contribution fromCursor(Cursor cursor) {
|
||||||
|
// Hardcoding column positions!
|
||||||
|
//Check that cursor has a value to avoid CursorIndexOutOfBoundsException
|
||||||
|
if (cursor.getCount() > 0) {
|
||||||
|
return new Contribution(
|
||||||
|
uriForId(cursor.getInt(0)),
|
||||||
|
cursor.getString(1),
|
||||||
|
parseUri(cursor.getString(2)),
|
||||||
|
cursor.getString(3),
|
||||||
|
parseTimestamp(cursor.getLong(4)),
|
||||||
|
cursor.getInt(5),
|
||||||
|
cursor.getLong(6),
|
||||||
|
parseTimestamp(cursor.getLong(7)),
|
||||||
|
cursor.getLong(8),
|
||||||
|
cursor.getString(9),
|
||||||
|
cursor.getString(10),
|
||||||
|
cursor.getString(11),
|
||||||
|
cursor.getInt(12) == 1,
|
||||||
|
cursor.getInt(13),
|
||||||
|
cursor.getInt(14),
|
||||||
|
cursor.getString(15));
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static Date parseTimestamp(long timestamp) {
|
||||||
|
return timestamp == 0 ? null : new Date(timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static Uri parseUri(String uriString) {
|
||||||
|
return TextUtils.isEmpty(uriString) ? null : Uri.parse(uriString);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Table {
|
||||||
|
public static final String TABLE_NAME = "contributions";
|
||||||
|
|
||||||
|
public static final String COLUMN_ID = "_id";
|
||||||
|
public static final String COLUMN_FILENAME = "filename";
|
||||||
|
public static final String COLUMN_LOCAL_URI = "local_uri";
|
||||||
|
public static final String COLUMN_IMAGE_URL = "image_url";
|
||||||
|
public static final String COLUMN_TIMESTAMP = "timestamp";
|
||||||
|
public static final String COLUMN_STATE = "state";
|
||||||
|
public static final String COLUMN_LENGTH = "length";
|
||||||
|
public static final String COLUMN_UPLOADED = "uploaded";
|
||||||
|
public static final String COLUMN_TRANSFERRED = "transferred"; // Currently transferred number of bytes
|
||||||
|
public static final String COLUMN_SOURCE = "source";
|
||||||
|
public static final String COLUMN_DESCRIPTION = "description";
|
||||||
|
public static final String COLUMN_CREATOR = "creator"; // Initial uploader
|
||||||
|
public static final String COLUMN_MULTIPLE = "multiple";
|
||||||
|
public static final String COLUMN_WIDTH = "width";
|
||||||
|
public static final String COLUMN_HEIGHT = "height";
|
||||||
|
public static final String COLUMN_LICENSE = "license";
|
||||||
|
|
||||||
|
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
|
||||||
|
public static final String[] ALL_FIELDS = {
|
||||||
|
COLUMN_ID,
|
||||||
|
COLUMN_FILENAME,
|
||||||
|
COLUMN_LOCAL_URI,
|
||||||
|
COLUMN_IMAGE_URL,
|
||||||
|
COLUMN_TIMESTAMP,
|
||||||
|
COLUMN_STATE,
|
||||||
|
COLUMN_LENGTH,
|
||||||
|
COLUMN_UPLOADED,
|
||||||
|
COLUMN_TRANSFERRED,
|
||||||
|
COLUMN_SOURCE,
|
||||||
|
COLUMN_DESCRIPTION,
|
||||||
|
COLUMN_CREATOR,
|
||||||
|
COLUMN_MULTIPLE,
|
||||||
|
COLUMN_WIDTH,
|
||||||
|
COLUMN_HEIGHT,
|
||||||
|
COLUMN_LICENSE
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME;
|
||||||
|
|
||||||
|
public static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
|
||||||
|
+ "_id INTEGER PRIMARY KEY,"
|
||||||
|
+ "filename STRING,"
|
||||||
|
+ "local_uri STRING,"
|
||||||
|
+ "image_url STRING,"
|
||||||
|
+ "uploaded INTEGER,"
|
||||||
|
+ "timestamp INTEGER,"
|
||||||
|
+ "state INTEGER,"
|
||||||
|
+ "length INTEGER,"
|
||||||
|
+ "transferred INTEGER,"
|
||||||
|
+ "source STRING,"
|
||||||
|
+ "description STRING,"
|
||||||
|
+ "creator STRING,"
|
||||||
|
+ "multiple INTEGER,"
|
||||||
|
+ "width INTEGER,"
|
||||||
|
+ "height INTEGER,"
|
||||||
|
+ "LICENSE STRING"
|
||||||
|
+ ");";
|
||||||
|
|
||||||
|
// Upgrade from version 1 ->
|
||||||
|
static final String ADD_CREATOR_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN creator STRING;";
|
||||||
|
static final String ADD_DESCRIPTION_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN description STRING;";
|
||||||
|
|
||||||
|
// Upgrade from version 2 ->
|
||||||
|
static final String ADD_MULTIPLE_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN multiple INTEGER;";
|
||||||
|
static final String SET_DEFAULT_MULTIPLE = "UPDATE " + TABLE_NAME + " SET multiple = 0";
|
||||||
|
|
||||||
|
// Upgrade from version 5 ->
|
||||||
|
static final String ADD_WIDTH_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN width INTEGER;";
|
||||||
|
static final String SET_DEFAULT_WIDTH = "UPDATE " + TABLE_NAME + " SET width = 0";
|
||||||
|
static final String ADD_HEIGHT_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN height INTEGER;";
|
||||||
|
static final String SET_DEFAULT_HEIGHT = "UPDATE " + TABLE_NAME + " SET height = 0";
|
||||||
|
static final String ADD_LICENSE_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN license STRING;";
|
||||||
|
static final String SET_DEFAULT_LICENSE = "UPDATE " + TABLE_NAME + " SET license='" + Prefs.Licenses.CC_BY_SA_3 + "';";
|
||||||
|
|
||||||
|
|
||||||
|
public static void onCreate(SQLiteDatabase db) {
|
||||||
|
db.execSQL(CREATE_TABLE_STATEMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void onDelete(SQLiteDatabase db) {
|
||||||
|
db.execSQL(DROP_TABLE_STATEMENT);
|
||||||
|
onCreate(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void onUpdate(SQLiteDatabase db, int from, int to) {
|
||||||
|
if (from == to) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (from == 1) {
|
||||||
|
db.execSQL(ADD_DESCRIPTION_FIELD);
|
||||||
|
db.execSQL(ADD_CREATOR_FIELD);
|
||||||
|
from++;
|
||||||
|
onUpdate(db, from, to);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (from == 2) {
|
||||||
|
db.execSQL(ADD_MULTIPLE_FIELD);
|
||||||
|
db.execSQL(SET_DEFAULT_MULTIPLE);
|
||||||
|
from++;
|
||||||
|
onUpdate(db, from, to);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (from == 3) {
|
||||||
|
// Do nothing
|
||||||
|
from++;
|
||||||
|
onUpdate(db, from, to);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (from == 4) {
|
||||||
|
// Do nothing -- added Category
|
||||||
|
from++;
|
||||||
|
onUpdate(db, from, to);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (from == 5) {
|
||||||
|
// Added width and height fields
|
||||||
|
db.execSQL(ADD_WIDTH_FIELD);
|
||||||
|
db.execSQL(SET_DEFAULT_WIDTH);
|
||||||
|
db.execSQL(ADD_HEIGHT_FIELD);
|
||||||
|
db.execSQL(SET_DEFAULT_HEIGHT);
|
||||||
|
db.execSQL(ADD_LICENSE_FIELD);
|
||||||
|
db.execSQL(SET_DEFAULT_LICENSE);
|
||||||
|
from++;
|
||||||
|
onUpdate(db, from, to);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,7 +9,6 @@ import android.database.Cursor;
|
||||||
import android.database.DataSetObserver;
|
import android.database.DataSetObserver;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.support.v4.app.FragmentManager;
|
import android.support.v4.app.FragmentManager;
|
||||||
import android.support.v4.app.LoaderManager;
|
import android.support.v4.app.LoaderManager;
|
||||||
import android.support.v4.content.CursorLoader;
|
import android.support.v4.content.CursorLoader;
|
||||||
|
|
@ -24,35 +23,41 @@ import android.widget.AdapterView;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
import dagger.android.AndroidInjection;
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
import fr.free.nrw.commons.HandlerService;
|
import fr.free.nrw.commons.HandlerService;
|
||||||
import fr.free.nrw.commons.Media;
|
import fr.free.nrw.commons.Media;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.auth.AuthenticatedActivity;
|
import fr.free.nrw.commons.auth.AuthenticatedActivity;
|
||||||
|
import fr.free.nrw.commons.auth.SessionManager;
|
||||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
||||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
import fr.free.nrw.commons.settings.Prefs;
|
import fr.free.nrw.commons.settings.Prefs;
|
||||||
import fr.free.nrw.commons.upload.UploadService;
|
import fr.free.nrw.commons.upload.UploadService;
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.disposables.CompositeDisposable;
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
import io.reactivex.disposables.Disposable;
|
|
||||||
import io.reactivex.schedulers.Schedulers;
|
import io.reactivex.schedulers.Schedulers;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
import static android.content.ContentResolver.requestSync;
|
import static android.content.ContentResolver.requestSync;
|
||||||
import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED;
|
import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED;
|
||||||
import static fr.free.nrw.commons.contributions.Contribution.Table.ALL_FIELDS;
|
import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS;
|
||||||
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.AUTHORITY;
|
|
||||||
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI;
|
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI;
|
||||||
import static fr.free.nrw.commons.settings.Prefs.UPLOADS_SHOWING;
|
import static fr.free.nrw.commons.settings.Prefs.UPLOADS_SHOWING;
|
||||||
|
|
||||||
public class ContributionsActivity extends AuthenticatedActivity
|
public class ContributionsActivity
|
||||||
implements LoaderManager.LoaderCallbacks<Cursor>, AdapterView.OnItemClickListener,
|
extends AuthenticatedActivity
|
||||||
MediaDetailPagerFragment.MediaDetailProvider, FragmentManager.OnBackStackChangedListener,
|
implements LoaderManager.LoaderCallbacks<Cursor>,
|
||||||
ContributionsListFragment.SourceRefresher {
|
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 Cursor allContributions;
|
||||||
private ContributionsListFragment contributionsList;
|
private ContributionsListFragment contributionsList;
|
||||||
|
|
@ -60,24 +65,6 @@ public class ContributionsActivity extends AuthenticatedActivity
|
||||||
private UploadService uploadService;
|
private UploadService uploadService;
|
||||||
private boolean isUploadServiceConnected;
|
private boolean isUploadServiceConnected;
|
||||||
private ArrayList<DataSetObserver> observersWaitingForLoad = new ArrayList<>();
|
private ArrayList<DataSetObserver> observersWaitingForLoad = new ArrayList<>();
|
||||||
private String CONTRIBUTION_SELECTION = "";
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
MediaWikiApi mediaWikiApi;
|
|
||||||
|
|
||||||
/*
|
|
||||||
This sorts in the following order:
|
|
||||||
Currently Uploading
|
|
||||||
Failed (Sorted in ascending order of time added - FIFO)
|
|
||||||
Queued to Upload (Sorted in ascending order of time added - FIFO)
|
|
||||||
Completed (Sorted in descending order of time added)
|
|
||||||
|
|
||||||
This is why Contribution.STATE_COMPLETED is -1.
|
|
||||||
*/
|
|
||||||
private String CONTRIBUTION_SORT = Contribution.Table.COLUMN_STATE + " DESC, "
|
|
||||||
+ Contribution.Table.COLUMN_UPLOADED + " DESC , ("
|
|
||||||
+ Contribution.Table.COLUMN_TIMESTAMP + " * "
|
|
||||||
+ Contribution.Table.COLUMN_STATE + ")";
|
|
||||||
|
|
||||||
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||||
|
|
||||||
|
|
@ -92,7 +79,7 @@ public class ContributionsActivity extends AuthenticatedActivity
|
||||||
@Override
|
@Override
|
||||||
public void onServiceDisconnected(ComponentName componentName) {
|
public void onServiceDisconnected(ComponentName componentName) {
|
||||||
// this should never happen
|
// this should never happen
|
||||||
throw new RuntimeException("UploadService died but the rest of the process did not!");
|
Timber.e(new RuntimeException("UploadService died but the rest of the process did not!"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -109,12 +96,8 @@ public class ContributionsActivity extends AuthenticatedActivity
|
||||||
@Override
|
@Override
|
||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
|
boolean isSettingsChanged = prefs.getBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false);
|
||||||
boolean isSettingsChanged =
|
prefs.edit().putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false).apply();
|
||||||
sharedPreferences.getBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false);
|
|
||||||
SharedPreferences.Editor editor = sharedPreferences.edit();
|
|
||||||
editor.putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false);
|
|
||||||
editor.apply();
|
|
||||||
if (isSettingsChanged) {
|
if (isSettingsChanged) {
|
||||||
refreshSource();
|
refreshSource();
|
||||||
}
|
}
|
||||||
|
|
@ -122,16 +105,14 @@ public class ContributionsActivity extends AuthenticatedActivity
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onAuthCookieAcquired(String authCookie) {
|
protected void onAuthCookieAcquired(String authCookie) {
|
||||||
// Do a sync every time we get here!
|
// Do a sync everytime we get here!
|
||||||
CommonsApplication app = ((CommonsApplication) getApplication());
|
requestSync(sessionManager.getCurrentAccount(), ContributionsContentProvider.CONTRIBUTION_AUTHORITY, new Bundle());
|
||||||
requestSync(app.getCurrentAccount(), AUTHORITY, new Bundle());
|
|
||||||
Intent uploadServiceIntent = new Intent(this, UploadService.class);
|
Intent uploadServiceIntent = new Intent(this, UploadService.class);
|
||||||
uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE);
|
uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE);
|
||||||
startService(uploadServiceIntent);
|
startService(uploadServiceIntent);
|
||||||
bindService(uploadServiceIntent, uploadServiceConnection, Context.BIND_AUTO_CREATE);
|
bindService(uploadServiceIntent, uploadServiceConnection, Context.BIND_AUTO_CREATE);
|
||||||
|
|
||||||
allContributions = getContentResolver().query(BASE_URI, ALL_FIELDS,
|
allContributions = contributionDao.loadAllContributions();
|
||||||
CONTRIBUTION_SELECTION, null, CONTRIBUTION_SORT);
|
|
||||||
|
|
||||||
getSupportLoaderManager().initLoader(0, null, this);
|
getSupportLoaderManager().initLoader(0, null, this);
|
||||||
}
|
}
|
||||||
|
|
@ -139,19 +120,18 @@ public class ContributionsActivity extends AuthenticatedActivity
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
AndroidInjection.inject(this);
|
|
||||||
setContentView(R.layout.activity_contributions);
|
setContentView(R.layout.activity_contributions);
|
||||||
ButterKnife.bind(this);
|
ButterKnife.bind(this);
|
||||||
|
|
||||||
// Activity can call methods in the fragment by acquiring a
|
// Activity can call methods in the fragment by acquiring a
|
||||||
// reference to the Fragment from FragmentManager, using findFragmentById()
|
// reference to the Fragment from FragmentManager, using findFragmentById()
|
||||||
FragmentManager supportFragmentManager = getSupportFragmentManager();
|
FragmentManager supportFragmentManager = getSupportFragmentManager();
|
||||||
contributionsList = (ContributionsListFragment) supportFragmentManager
|
contributionsList = (ContributionsListFragment)supportFragmentManager
|
||||||
.findFragmentById(R.id.contributionsListFragment);
|
.findFragmentById(R.id.contributionsListFragment);
|
||||||
|
|
||||||
supportFragmentManager.addOnBackStackChangedListener(this);
|
supportFragmentManager.addOnBackStackChangedListener(this);
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
mediaDetails = (MediaDetailPagerFragment) supportFragmentManager
|
mediaDetails = (MediaDetailPagerFragment)supportFragmentManager
|
||||||
.findFragmentById(R.id.contributionsFragmentContainer);
|
.findFragmentById(R.id.contributionsFragmentContainer);
|
||||||
|
|
||||||
getSupportLoaderManager().initLoader(0, null, this);
|
getSupportLoaderManager().initLoader(0, null, this);
|
||||||
|
|
@ -190,24 +170,23 @@ public class ContributionsActivity extends AuthenticatedActivity
|
||||||
|
|
||||||
public void retryUpload(int i) {
|
public void retryUpload(int i) {
|
||||||
allContributions.moveToPosition(i);
|
allContributions.moveToPosition(i);
|
||||||
Contribution c = Contribution.fromCursor(allContributions);
|
Contribution c = contributionDao.fromCursor(allContributions);
|
||||||
if (c.getState() == STATE_FAILED) {
|
if (c.getState() == STATE_FAILED) {
|
||||||
uploadService.queue(UploadService.ACTION_UPLOAD_FILE, c);
|
uploadService.queue(UploadService.ACTION_UPLOAD_FILE, c);
|
||||||
Timber.d("Restarting for %s", c.toContentValues());
|
Timber.d("Restarting for %s", c.toString());
|
||||||
} else {
|
} else {
|
||||||
Timber.d("Skipping re-upload for non-failed %s", c.toContentValues());
|
Timber.d("Skipping re-upload for non-failed %s", c.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteUpload(int i) {
|
public void deleteUpload(int i) {
|
||||||
allContributions.moveToPosition(i);
|
allContributions.moveToPosition(i);
|
||||||
Contribution c = Contribution.fromCursor(allContributions);
|
Contribution c = contributionDao.fromCursor(allContributions);
|
||||||
if (c.getState() == STATE_FAILED) {
|
if (c.getState() == STATE_FAILED) {
|
||||||
Timber.d("Deleting failed contrib %s", c.toContentValues());
|
Timber.d("Deleting failed contrib %s", c.toString());
|
||||||
c.setContentProviderClient(getContentResolver().acquireContentProviderClient(AUTHORITY));
|
contributionDao.delete(c);
|
||||||
c.delete();
|
|
||||||
} else {
|
} else {
|
||||||
Timber.d("Skipping deletion for non-failed contrib %s", c.toContentValues());
|
Timber.d("Skipping deletion for non-failed contrib %s", c.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -241,11 +220,10 @@ public class ContributionsActivity extends AuthenticatedActivity
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
|
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
|
||||||
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
|
int uploads = prefs.getInt(UPLOADS_SHOWING, 100);
|
||||||
int uploads = sharedPref.getInt(UPLOADS_SHOWING, 100);
|
|
||||||
return new CursorLoader(this, BASE_URI,
|
return new CursorLoader(this, BASE_URI,
|
||||||
ALL_FIELDS, CONTRIBUTION_SELECTION, null,
|
ALL_FIELDS, "", null,
|
||||||
CONTRIBUTION_SORT + "LIMIT " + uploads);
|
ContributionDao.CONTRIBUTION_SORT + "LIMIT " + uploads);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -254,7 +232,7 @@ public class ContributionsActivity extends AuthenticatedActivity
|
||||||
|
|
||||||
if (contributionsList.getAdapter() == null) {
|
if (contributionsList.getAdapter() == null) {
|
||||||
contributionsList.setAdapter(new ContributionsListAdapter(getApplicationContext(),
|
contributionsList.setAdapter(new ContributionsListAdapter(getApplicationContext(),
|
||||||
cursor, 0));
|
cursor, 0, contributionDao));
|
||||||
} else {
|
} else {
|
||||||
((CursorAdapter) contributionsList.getAdapter()).swapCursor(cursor);
|
((CursorAdapter) contributionsList.getAdapter()).swapCursor(cursor);
|
||||||
}
|
}
|
||||||
|
|
@ -275,7 +253,7 @@ public class ContributionsActivity extends AuthenticatedActivity
|
||||||
// not yet ready to return data
|
// not yet ready to return data
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
return Contribution.fromCursor((Cursor) contributionsList.getAdapter().getItem(i));
|
return contributionDao.fromCursor((Cursor) contributionsList.getAdapter().getItem(i));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -289,9 +267,8 @@ public class ContributionsActivity extends AuthenticatedActivity
|
||||||
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
@SuppressWarnings("ConstantConditions")
|
||||||
private void setUploadCount() {
|
private void setUploadCount() {
|
||||||
CommonsApplication app = ((CommonsApplication) getApplication());
|
compositeDisposable.add(mediaWikiApi
|
||||||
Disposable uploadCountDisposable = mediaWikiApi
|
.getUploadCount(sessionManager.getCurrentAccount().name)
|
||||||
.getUploadCount(app.getCurrentAccount().name)
|
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
|
|
@ -299,8 +276,7 @@ public class ContributionsActivity extends AuthenticatedActivity
|
||||||
.getQuantityString(R.plurals.contributions_subtitle,
|
.getQuantityString(R.plurals.contributions_subtitle,
|
||||||
uploadCount, uploadCount)),
|
uploadCount, uploadCount)),
|
||||||
t -> Timber.e(t, "Fetching upload count failed")
|
t -> Timber.e(t, "Fetching upload count failed")
|
||||||
);
|
));
|
||||||
compositeDisposable.add(uploadCountDisposable);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package fr.free.nrw.commons.contributions;
|
package fr.free.nrw.commons.contributions;
|
||||||
|
|
||||||
import android.content.ContentProvider;
|
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.content.UriMatcher;
|
import android.content.UriMatcher;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
|
|
@ -10,36 +9,36 @@ import android.net.Uri;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.data.DBOpenHelper;
|
||||||
|
import fr.free.nrw.commons.di.CommonsDaggerContentProvider;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
import static android.content.UriMatcher.NO_MATCH;
|
import static android.content.UriMatcher.NO_MATCH;
|
||||||
import static fr.free.nrw.commons.contributions.Contribution.Table.ALL_FIELDS;
|
import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS;
|
||||||
import static fr.free.nrw.commons.contributions.Contribution.Table.TABLE_NAME;
|
import static fr.free.nrw.commons.contributions.ContributionDao.Table.TABLE_NAME;
|
||||||
|
|
||||||
public class ContributionsContentProvider extends ContentProvider {
|
public class ContributionsContentProvider extends CommonsDaggerContentProvider {
|
||||||
|
|
||||||
private static final int CONTRIBUTIONS = 1;
|
private static final int CONTRIBUTIONS = 1;
|
||||||
private static final int CONTRIBUTIONS_ID = 2;
|
private static final int CONTRIBUTIONS_ID = 2;
|
||||||
private static final String BASE_PATH = "contributions";
|
private static final String BASE_PATH = "contributions";
|
||||||
private static final UriMatcher uriMatcher = new UriMatcher(NO_MATCH);
|
private static final UriMatcher uriMatcher = new UriMatcher(NO_MATCH);
|
||||||
public static final String AUTHORITY = "fr.free.nrw.commons.contributions.contentprovider";
|
public static final String CONTRIBUTION_AUTHORITY = "fr.free.nrw.commons.contributions.contentprovider";
|
||||||
|
|
||||||
public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY + "/" + BASE_PATH);
|
public static final Uri BASE_URI = Uri.parse("content://" + CONTRIBUTION_AUTHORITY + "/" + BASE_PATH);
|
||||||
|
|
||||||
static {
|
static {
|
||||||
uriMatcher.addURI(AUTHORITY, BASE_PATH, CONTRIBUTIONS);
|
uriMatcher.addURI(CONTRIBUTION_AUTHORITY, BASE_PATH, CONTRIBUTIONS);
|
||||||
uriMatcher.addURI(AUTHORITY, BASE_PATH + "/#", CONTRIBUTIONS_ID);
|
uriMatcher.addURI(CONTRIBUTION_AUTHORITY, BASE_PATH + "/#", CONTRIBUTIONS_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Uri uriForId(int id) {
|
public static Uri uriForId(int id) {
|
||||||
return Uri.parse(BASE_URI.toString() + "/" + id);
|
return Uri.parse(BASE_URI.toString() + "/" + id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Inject DBOpenHelper dbOpenHelper;
|
||||||
public boolean onCreate() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
@SuppressWarnings("ConstantConditions")
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -50,8 +49,7 @@ public class ContributionsContentProvider extends ContentProvider {
|
||||||
|
|
||||||
int uriType = uriMatcher.match(uri);
|
int uriType = uriMatcher.match(uri);
|
||||||
|
|
||||||
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext();
|
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
|
||||||
SQLiteDatabase db = app.getDBOpenHelper().getReadableDatabase();
|
|
||||||
Cursor cursor;
|
Cursor cursor;
|
||||||
|
|
||||||
switch (uriType) {
|
switch (uriType) {
|
||||||
|
|
@ -87,9 +85,8 @@ public class ContributionsContentProvider extends ContentProvider {
|
||||||
@Override
|
@Override
|
||||||
public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
|
public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
|
||||||
int uriType = uriMatcher.match(uri);
|
int uriType = uriMatcher.match(uri);
|
||||||
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext();
|
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||||
SQLiteDatabase sqlDB = app.getDBOpenHelper().getWritableDatabase();
|
long id = 0;
|
||||||
long id;
|
|
||||||
switch (uriType) {
|
switch (uriType) {
|
||||||
case CONTRIBUTIONS:
|
case CONTRIBUTIONS:
|
||||||
id = sqlDB.insert(TABLE_NAME, null, contentValues);
|
id = sqlDB.insert(TABLE_NAME, null, contentValues);
|
||||||
|
|
@ -107,13 +104,12 @@ public class ContributionsContentProvider extends ContentProvider {
|
||||||
int rows;
|
int rows;
|
||||||
int uriType = uriMatcher.match(uri);
|
int uriType = uriMatcher.match(uri);
|
||||||
|
|
||||||
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext();
|
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
|
||||||
SQLiteDatabase sqlDB = app.getDBOpenHelper().getWritableDatabase();
|
|
||||||
|
|
||||||
switch (uriType) {
|
switch (uriType) {
|
||||||
case CONTRIBUTIONS_ID:
|
case CONTRIBUTIONS_ID:
|
||||||
Timber.d("Deleting contribution id %s", uri.getLastPathSegment());
|
Timber.d("Deleting contribution id %s", uri.getLastPathSegment());
|
||||||
rows = sqlDB.delete(TABLE_NAME,
|
rows = db.delete(TABLE_NAME,
|
||||||
"_id = ?",
|
"_id = ?",
|
||||||
new String[]{uri.getLastPathSegment()}
|
new String[]{uri.getLastPathSegment()}
|
||||||
);
|
);
|
||||||
|
|
@ -130,8 +126,7 @@ public class ContributionsContentProvider extends ContentProvider {
|
||||||
public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
|
public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
|
||||||
Timber.d("Hello, bulk insert! (ContributionsContentProvider)");
|
Timber.d("Hello, bulk insert! (ContributionsContentProvider)");
|
||||||
int uriType = uriMatcher.match(uri);
|
int uriType = uriMatcher.match(uri);
|
||||||
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext();
|
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||||
SQLiteDatabase sqlDB = app.getDBOpenHelper().getWritableDatabase();
|
|
||||||
sqlDB.beginTransaction();
|
sqlDB.beginTransaction();
|
||||||
switch (uriType) {
|
switch (uriType) {
|
||||||
case CONTRIBUTIONS:
|
case CONTRIBUTIONS:
|
||||||
|
|
@ -162,9 +157,8 @@ public class ContributionsContentProvider extends ContentProvider {
|
||||||
error out otherwise.
|
error out otherwise.
|
||||||
*/
|
*/
|
||||||
int uriType = uriMatcher.match(uri);
|
int uriType = uriMatcher.match(uri);
|
||||||
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext();
|
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||||
SQLiteDatabase sqlDB = app.getDBOpenHelper().getWritableDatabase();
|
int rowsUpdated = 0;
|
||||||
int rowsUpdated;
|
|
||||||
switch (uriType) {
|
switch (uriType) {
|
||||||
case CONTRIBUTIONS:
|
case CONTRIBUTIONS:
|
||||||
rowsUpdated = sqlDB.update(TABLE_NAME, contentValues, selection, selectionArgs);
|
rowsUpdated = sqlDB.update(TABLE_NAME, contentValues, selection, selectionArgs);
|
||||||
|
|
@ -175,7 +169,7 @@ public class ContributionsContentProvider extends ContentProvider {
|
||||||
if (TextUtils.isEmpty(selection)) {
|
if (TextUtils.isEmpty(selection)) {
|
||||||
rowsUpdated = sqlDB.update(TABLE_NAME,
|
rowsUpdated = sqlDB.update(TABLE_NAME,
|
||||||
contentValues,
|
contentValues,
|
||||||
Contribution.Table.COLUMN_ID + " = ?",
|
ContributionDao.Table.COLUMN_ID + " = ?",
|
||||||
new String[]{String.valueOf(id)});
|
new String[]{String.valueOf(id)});
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,11 @@ import fr.free.nrw.commons.R;
|
||||||
|
|
||||||
class ContributionsListAdapter extends CursorAdapter {
|
class ContributionsListAdapter extends CursorAdapter {
|
||||||
|
|
||||||
public ContributionsListAdapter(Context context, Cursor c, int flags) {
|
private final ContributionDao contributionDao;
|
||||||
|
|
||||||
|
public ContributionsListAdapter(Context context, Cursor c, int flags, ContributionDao contributionDao) {
|
||||||
super(context, c, flags);
|
super(context, c, flags);
|
||||||
|
this.contributionDao = contributionDao;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -26,7 +29,7 @@ class ContributionsListAdapter extends CursorAdapter {
|
||||||
@Override
|
@Override
|
||||||
public void bindView(View view, Context context, Cursor cursor) {
|
public void bindView(View view, Context context, Cursor cursor) {
|
||||||
final ContributionViewHolder views = (ContributionViewHolder)view.getTag();
|
final ContributionViewHolder views = (ContributionViewHolder)view.getTag();
|
||||||
final Contribution contribution = Contribution.fromCursor(cursor);
|
final Contribution contribution = contributionDao.fromCursor(cursor);
|
||||||
|
|
||||||
views.imageView.setMedia(contribution);
|
views.imageView.setMedia(contribution);
|
||||||
views.titleView.setText(contribution.getDisplayTitle());
|
views.titleView.setText(contribution.getDisplayTitle());
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,7 @@ import android.content.SharedPreferences;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.v4.app.Fragment;
|
|
||||||
import android.support.v4.content.ContextCompat;
|
import android.support.v4.content.ContextCompat;
|
||||||
import android.support.v7.app.AlertDialog;
|
import android.support.v7.app.AlertDialog;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
|
|
@ -22,21 +20,23 @@ import android.widget.ListAdapter;
|
||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
|
||||||
import butterknife.BindView;
|
import butterknife.BindView;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
|
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||||
import fr.free.nrw.commons.nearby.NearbyActivity;
|
import fr.free.nrw.commons.nearby.NearbyActivity;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
|
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
|
||||||
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
|
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
|
||||||
import static android.app.Activity.RESULT_OK;
|
import static android.app.Activity.RESULT_OK;
|
||||||
import static android.content.Context.MODE_PRIVATE;
|
|
||||||
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
|
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
|
||||||
import static android.view.View.GONE;
|
import static android.view.View.GONE;
|
||||||
|
|
||||||
public class ContributionsListFragment extends Fragment {
|
public class ContributionsListFragment extends CommonsDaggerSupportFragment {
|
||||||
|
|
||||||
@BindView(R.id.contributionsList)
|
@BindView(R.id.contributionsList)
|
||||||
GridView contributionsList;
|
GridView contributionsList;
|
||||||
|
|
@ -45,6 +45,9 @@ public class ContributionsListFragment extends Fragment {
|
||||||
@BindView(R.id.loadingContributionsProgressBar)
|
@BindView(R.id.loadingContributionsProgressBar)
|
||||||
ProgressBar progressBar;
|
ProgressBar progressBar;
|
||||||
|
|
||||||
|
@Inject @Named("prefs") SharedPreferences prefs;
|
||||||
|
@Inject @Named("default_preferences") SharedPreferences defaultPrefs;
|
||||||
|
|
||||||
private ContributionController controller;
|
private ContributionController controller;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -59,7 +62,6 @@ public class ContributionsListFragment extends Fragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: Should this be in onResume?
|
//TODO: Should this be in onResume?
|
||||||
SharedPreferences prefs = getActivity().getSharedPreferences("prefs", MODE_PRIVATE);
|
|
||||||
String lastModified = prefs.getString("lastSyncTimestamp", "");
|
String lastModified = prefs.getString("lastSyncTimestamp", "");
|
||||||
Timber.d("Last Sync Timestamp: %s", lastModified);
|
Timber.d("Last Sync Timestamp: %s", lastModified);
|
||||||
|
|
||||||
|
|
@ -162,9 +164,7 @@ public class ContributionsListFragment extends Fragment {
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
case R.id.menu_from_camera:
|
case R.id.menu_from_camera:
|
||||||
SharedPreferences sharedPref = PreferenceManager
|
boolean useExtStorage = defaultPrefs.getBoolean("useExternalStorage", true);
|
||||||
.getDefaultSharedPreferences(CommonsApplication.getInstance());
|
|
||||||
boolean useExtStorage = sharedPref.getBoolean("useExternalStorage", true);
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && useExtStorage) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && useExtStorage) {
|
||||||
// Here, thisActivity is the current activity
|
// Here, thisActivity is the current activity
|
||||||
if (ContextCompat.checkSelfPermission(getActivity(), WRITE_EXTERNAL_STORAGE)
|
if (ContextCompat.checkSelfPermission(getActivity(), WRITE_EXTERNAL_STORAGE)
|
||||||
|
|
@ -242,12 +242,17 @@ public class ContributionsListFragment extends Fragment {
|
||||||
menu.clear(); // See http://stackoverflow.com/a/8495697/17865
|
menu.clear(); // See http://stackoverflow.com/a/8495697/17865
|
||||||
inflater.inflate(R.menu.fragment_contributions_list, menu);
|
inflater.inflate(R.menu.fragment_contributions_list, menu);
|
||||||
|
|
||||||
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext();
|
if (!deviceHasCamera()) {
|
||||||
if (!app.deviceHasCamera()) {
|
|
||||||
menu.findItem(R.id.menu_from_camera).setEnabled(false);
|
menu.findItem(R.id.menu_from_camera).setEnabled(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean deviceHasCamera() {
|
||||||
|
PackageManager pm = getContext().getPackageManager();
|
||||||
|
return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA) ||
|
||||||
|
pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
|
||||||
|
|
@ -13,21 +13,28 @@ import android.os.RemoteException;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
import fr.free.nrw.commons.CommonsApplication;
|
||||||
import fr.free.nrw.commons.Utils;
|
import fr.free.nrw.commons.Utils;
|
||||||
|
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||||
import fr.free.nrw.commons.mwapi.LogEventResult;
|
import fr.free.nrw.commons.mwapi.LogEventResult;
|
||||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
import static android.content.Context.MODE_PRIVATE;
|
|
||||||
import static fr.free.nrw.commons.contributions.Contribution.STATE_COMPLETED;
|
import static fr.free.nrw.commons.contributions.Contribution.STATE_COMPLETED;
|
||||||
import static fr.free.nrw.commons.contributions.Contribution.Table.COLUMN_FILENAME;
|
import static fr.free.nrw.commons.contributions.ContributionDao.Table.COLUMN_FILENAME;
|
||||||
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI;
|
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI;
|
||||||
|
|
||||||
|
@SuppressWarnings("WeakerAccess")
|
||||||
public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
|
public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||||
|
|
||||||
private static final String[] existsQuery = {COLUMN_FILENAME};
|
private static final String[] existsQuery = {COLUMN_FILENAME};
|
||||||
|
|
@ -35,6 +42,10 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||||
private static final ContentValues[] EMPTY = {};
|
private static final ContentValues[] EMPTY = {};
|
||||||
private static int COMMIT_THRESHOLD = 10;
|
private static int COMMIT_THRESHOLD = 10;
|
||||||
|
|
||||||
|
@SuppressWarnings("WeakerAccess")
|
||||||
|
@Inject MediaWikiApi mwApi;
|
||||||
|
@Inject @Named("prefs") SharedPreferences prefs;
|
||||||
|
|
||||||
public ContributionsSyncAdapter(Context context, boolean autoInitialize) {
|
public ContributionsSyncAdapter(Context context, boolean autoInitialize) {
|
||||||
super(context, autoInitialize);
|
super(context, autoInitialize);
|
||||||
}
|
}
|
||||||
|
|
@ -71,19 +82,23 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||||
@Override
|
@Override
|
||||||
public void onPerformSync(Account account, Bundle bundle, String authority,
|
public void onPerformSync(Account account, Bundle bundle, String authority,
|
||||||
ContentProviderClient contentProviderClient, SyncResult syncResult) {
|
ContentProviderClient contentProviderClient, SyncResult syncResult) {
|
||||||
|
ApplicationlessInjection
|
||||||
|
.getInstance(getContext()
|
||||||
|
.getApplicationContext())
|
||||||
|
.getCommonsApplicationComponent()
|
||||||
|
.inject(this);
|
||||||
// This code is fraught with possibilities of race conditions, but lalalalala I can't hear you!
|
// This code is fraught with possibilities of race conditions, but lalalalala I can't hear you!
|
||||||
String user = account.name;
|
String user = account.name;
|
||||||
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
|
|
||||||
SharedPreferences prefs = getContext().getSharedPreferences("prefs", MODE_PRIVATE);
|
|
||||||
String lastModified = prefs.getString("lastSyncTimestamp", "");
|
String lastModified = prefs.getString("lastSyncTimestamp", "");
|
||||||
Date curTime = new Date();
|
Date curTime = new Date();
|
||||||
LogEventResult result;
|
LogEventResult result;
|
||||||
Boolean done = false;
|
Boolean done = false;
|
||||||
String queryContinue = null;
|
String queryContinue = null;
|
||||||
|
ContributionDao contributionDao = new ContributionDao(() -> contentProviderClient);
|
||||||
while (!done) {
|
while (!done) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
result = api.logEvents(user, lastModified, queryContinue, getLimit());
|
result = mwApi.logEvents(user, lastModified, queryContinue, getLimit());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// There isn't really much we can do, eh?
|
// There isn't really much we can do, eh?
|
||||||
// FIXME: Perhaps add EventLogging?
|
// FIXME: Perhaps add EventLogging?
|
||||||
|
|
@ -112,7 +127,7 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||||
"", -1, dateUpdated, dateUpdated, user,
|
"", -1, dateUpdated, dateUpdated, user,
|
||||||
"", "");
|
"", "");
|
||||||
contrib.setState(STATE_COMPLETED);
|
contrib.setState(STATE_COMPLETED);
|
||||||
imageValues.add(contrib.toContentValues());
|
imageValues.add(contributionDao.toContentValues(contrib));
|
||||||
|
|
||||||
if (imageValues.size() % COMMIT_THRESHOLD == 0) {
|
if (imageValues.size() % COMMIT_THRESHOLD == 0) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -137,8 +152,13 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||||
done = true;
|
done = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
prefs.edit().putString("lastSyncTimestamp", Utils.toMWDate(curTime)).apply();
|
prefs.edit().putString("lastSyncTimestamp", toMWDate(curTime)).apply();
|
||||||
Timber.d("Oh hai, everyone! Look, a kitty!");
|
Timber.d("Oh hai, everyone! Look, a kitty!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String toMWDate(Date date) {
|
||||||
|
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH); // Assuming MW always gives me UTC
|
||||||
|
isoFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||||
|
return isoFormat.format(date);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,9 @@ import android.content.Context;
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
import android.database.sqlite.SQLiteOpenHelper;
|
import android.database.sqlite.SQLiteOpenHelper;
|
||||||
|
|
||||||
import fr.free.nrw.commons.contributions.Contribution;
|
import fr.free.nrw.commons.category.CategoryDao;
|
||||||
import fr.free.nrw.commons.modifications.ModifierSequence;
|
import fr.free.nrw.commons.contributions.ContributionDao;
|
||||||
|
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
|
||||||
|
|
||||||
public class DBOpenHelper extends SQLiteOpenHelper {
|
public class DBOpenHelper extends SQLiteOpenHelper {
|
||||||
|
|
||||||
|
|
@ -13,7 +14,8 @@ public class DBOpenHelper extends SQLiteOpenHelper {
|
||||||
private static final int DATABASE_VERSION = 6;
|
private static final int DATABASE_VERSION = 6;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Do not use, please call CommonsApplication.getDBOpenHelper()
|
* Do not use directly - @Inject an instance where it's needed and let
|
||||||
|
* dependency injection take care of managing this as a singleton.
|
||||||
*/
|
*/
|
||||||
public DBOpenHelper(Context context) {
|
public DBOpenHelper(Context context) {
|
||||||
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
||||||
|
|
@ -21,15 +23,15 @@ public class DBOpenHelper extends SQLiteOpenHelper {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(SQLiteDatabase sqLiteDatabase) {
|
public void onCreate(SQLiteDatabase sqLiteDatabase) {
|
||||||
Contribution.Table.onCreate(sqLiteDatabase);
|
ContributionDao.Table.onCreate(sqLiteDatabase);
|
||||||
ModifierSequence.Table.onCreate(sqLiteDatabase);
|
ModifierSequenceDao.Table.onCreate(sqLiteDatabase);
|
||||||
Category.Table.onCreate(sqLiteDatabase);
|
CategoryDao.Table.onCreate(sqLiteDatabase);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int from, int to) {
|
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int from, int to) {
|
||||||
Contribution.Table.onUpdate(sqLiteDatabase, from, to);
|
ContributionDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||||
ModifierSequence.Table.onUpdate(sqLiteDatabase, from, to);
|
ModifierSequenceDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||||
Category.Table.onUpdate(sqLiteDatabase, from, to);
|
CategoryDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
package fr.free.nrw.commons.di;
|
|
||||||
|
|
||||||
import dagger.Module;
|
|
||||||
import dagger.android.ContributesAndroidInjector;
|
|
||||||
import fr.free.nrw.commons.contributions.ContributionsActivity;
|
|
||||||
import fr.free.nrw.commons.nearby.NearbyActivity;
|
|
||||||
|
|
||||||
@Module
|
|
||||||
public abstract class ActivityBuilder {
|
|
||||||
|
|
||||||
@ContributesAndroidInjector()
|
|
||||||
abstract ContributionsActivity bindSplashScreenActivity();
|
|
||||||
|
|
||||||
@ContributesAndroidInjector()
|
|
||||||
abstract NearbyActivity bindNearbyActivity();
|
|
||||||
}
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
package fr.free.nrw.commons.di;
|
|
||||||
|
|
||||||
import android.app.Application;
|
|
||||||
|
|
||||||
import javax.inject.Singleton;
|
|
||||||
|
|
||||||
import dagger.BindsInstance;
|
|
||||||
import dagger.Component;
|
|
||||||
import dagger.android.support.AndroidSupportInjectionModule;
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
@Component(modules = {
|
|
||||||
AndroidSupportInjectionModule.class,
|
|
||||||
AppModule.class,
|
|
||||||
ActivityBuilder.class
|
|
||||||
})
|
|
||||||
public interface AppComponent {
|
|
||||||
|
|
||||||
@Component.Builder
|
|
||||||
interface Builder {
|
|
||||||
@BindsInstance
|
|
||||||
Builder application(Application application);
|
|
||||||
|
|
||||||
AppComponent build();
|
|
||||||
}
|
|
||||||
|
|
||||||
void inject(CommonsApplication application);
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
package fr.free.nrw.commons.di;
|
|
||||||
|
|
||||||
import android.app.Application;
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import javax.inject.Singleton;
|
|
||||||
|
|
||||||
import dagger.Module;
|
|
||||||
import dagger.Provides;
|
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
|
||||||
import fr.free.nrw.commons.location.LocationServiceManager;
|
|
||||||
import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi;
|
|
||||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
|
||||||
|
|
||||||
@Module
|
|
||||||
public class AppModule {
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
Context provideContext(Application application) {
|
|
||||||
return application;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
public MediaWikiApi getMWApi() {
|
|
||||||
return new ApacheHttpClientMediaWikiApi(BuildConfig.WIKIMEDIA_API_HOST);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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,54 @@
|
||||||
|
package fr.free.nrw.commons.di;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
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.category.CategoryContentProvider;
|
||||||
|
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
|
||||||
|
import fr.free.nrw.commons.contributions.ContributionsSyncAdapter;
|
||||||
|
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
|
||||||
|
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,132 +1,156 @@
|
||||||
package fr.free.nrw.commons.location;
|
package fr.free.nrw.commons.location;
|
||||||
|
|
||||||
import android.location.Location;
|
import android.location.Location;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
public class LatLng {
|
/**
|
||||||
|
* a latitude and longitude point with accuracy information, often of a picture
|
||||||
private final double latitude;
|
*/
|
||||||
private final double longitude;
|
public class LatLng {
|
||||||
private final float accuracy;
|
|
||||||
|
private final double latitude;
|
||||||
/** Accepts latitude and longitude.
|
private final double longitude;
|
||||||
* North and South values are cut off at 90°
|
private final float accuracy;
|
||||||
*
|
|
||||||
* @param latitude double value
|
/**
|
||||||
* @param longitude double value
|
* Accepts latitude and longitude.
|
||||||
*/
|
* North and South values are cut off at 90°
|
||||||
public LatLng(double latitude, double longitude, float accuracy) {
|
*
|
||||||
if (-180.0D <= longitude && longitude < 180.0D) {
|
* @param latitude the latitude
|
||||||
this.longitude = longitude;
|
* @param longitude the longitude
|
||||||
} else {
|
* @param accuracy the accuracy
|
||||||
this.longitude = ((longitude - 180.0D) % 360.0D + 360.0D) % 360.0D - 180.0D;
|
*
|
||||||
}
|
* Examples:
|
||||||
this.latitude = Math.max(-90.0D, Math.min(90.0D, latitude));
|
* the Statue of Liberty is located at 40.69° N, 74.04° W
|
||||||
this.accuracy = accuracy;
|
* 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 static LatLng from(@NonNull Location location) {
|
public LatLng(double latitude, double longitude, float accuracy) {
|
||||||
return new LatLng(location.getLatitude(), location.getLongitude(), location.getAccuracy());
|
if (-180.0D <= longitude && longitude < 180.0D) {
|
||||||
}
|
this.longitude = longitude;
|
||||||
|
} else {
|
||||||
public int hashCode() {
|
this.longitude = ((longitude - 180.0D) % 360.0D + 360.0D) % 360.0D - 180.0D;
|
||||||
boolean var1 = true;
|
}
|
||||||
byte var2 = 1;
|
this.latitude = Math.max(-90.0D, Math.min(90.0D, latitude));
|
||||||
long var3 = Double.doubleToLongBits(this.latitude);
|
this.accuracy = accuracy;
|
||||||
int var5 = 31 * var2 + (int)(var3 ^ var3 >>> 32);
|
}
|
||||||
var3 = Double.doubleToLongBits(this.longitude);
|
|
||||||
var5 = 31 * var5 + (int)(var3 ^ var3 >>> 32);
|
/**
|
||||||
return var5;
|
* 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 boolean equals(Object o) {
|
*/
|
||||||
if (this == o) {
|
public static LatLng from(@NonNull Location location) {
|
||||||
return true;
|
return new LatLng(location.getLatitude(), location.getLongitude(), location.getAccuracy());
|
||||||
} else if (!(o instanceof LatLng)) {
|
}
|
||||||
return false;
|
|
||||||
} else {
|
/**
|
||||||
LatLng var2 = (LatLng)o;
|
* creates a hash code for the longitude and longitude
|
||||||
return Double.doubleToLongBits(this.latitude) == Double.doubleToLongBits(var2.latitude) && Double.doubleToLongBits(this.longitude) == Double.doubleToLongBits(var2.longitude);
|
*/
|
||||||
}
|
public int hashCode() {
|
||||||
}
|
byte var1 = 1;
|
||||||
|
long var2 = Double.doubleToLongBits(this.latitude);
|
||||||
public String toString() {
|
int var3 = 31 * var1 + (int)(var2 ^ var2 >>> 32);
|
||||||
return "lat/lng: (" + this.latitude + "," + this.longitude + ")";
|
var2 = Double.doubleToLongBits(this.longitude);
|
||||||
}
|
var3 = 31 * var3 + (int)(var2 ^ var2 >>> 32);
|
||||||
|
return var3;
|
||||||
/**
|
}
|
||||||
* Rounds the float to 4 digits and returns absolute value.
|
|
||||||
*
|
/**
|
||||||
* @param coordinate A coordinate value as string.
|
* checks for equality of two LatLng objects
|
||||||
* @return String of the rounded number.
|
* @param o the second LatLng object
|
||||||
*/
|
*/
|
||||||
private String formatCoordinate(double coordinate) {
|
public boolean equals(Object o) {
|
||||||
double roundedNumber = Math.round(coordinate * 10000d) / 10000d;
|
if (this == o) {
|
||||||
double absoluteNumber = Math.abs(roundedNumber);
|
return true;
|
||||||
return String.valueOf(absoluteNumber);
|
} else if (!(o instanceof LatLng)) {
|
||||||
}
|
return false;
|
||||||
|
} else {
|
||||||
/**
|
LatLng var2 = (LatLng)o;
|
||||||
* Returns "N" or "S" depending on the latitude.
|
return Double.doubleToLongBits(this.latitude) == Double.doubleToLongBits(var2.latitude) && Double.doubleToLongBits(this.longitude) == Double.doubleToLongBits(var2.longitude);
|
||||||
*
|
}
|
||||||
* @return "N" or "S".
|
}
|
||||||
*/
|
|
||||||
private String getNorthSouth() {
|
/**
|
||||||
if (this.latitude < 0) {
|
* returns a string representation of the latitude and longitude
|
||||||
return "S";
|
*/
|
||||||
}
|
public String toString() {
|
||||||
|
return "lat/lng: (" + this.latitude + "," + this.longitude + ")";
|
||||||
return "N";
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
/**
|
* Rounds the float to 4 digits and returns absolute value.
|
||||||
* Returns "E" or "W" depending on the longitude.
|
*
|
||||||
*
|
* @param coordinate A coordinate value as string.
|
||||||
* @return "E" or "W".
|
* @return String of the rounded number.
|
||||||
*/
|
*/
|
||||||
private String getEastWest() {
|
private String formatCoordinate(double coordinate) {
|
||||||
if (this.longitude >= 0 && this.longitude < 180) {
|
double roundedNumber = Math.round(coordinate * 10000d) / 10000d;
|
||||||
return "E";
|
double absoluteNumber = Math.abs(roundedNumber);
|
||||||
}
|
return String.valueOf(absoluteNumber);
|
||||||
|
}
|
||||||
return "W";
|
|
||||||
}
|
/**
|
||||||
|
* Returns "N" or "S" depending on the latitude.
|
||||||
/**
|
*
|
||||||
* Returns a nicely formatted coordinate string. Used e.g. in
|
* @return "N" or "S".
|
||||||
* the detail view.
|
*/
|
||||||
*
|
private String getNorthSouth() {
|
||||||
* @return The formatted string.
|
if (this.latitude < 0) {
|
||||||
*/
|
return "S";
|
||||||
public String getPrettyCoordinateString() {
|
}
|
||||||
return formatCoordinate(this.latitude) + " " + this.getNorthSouth() + ", "
|
|
||||||
+ formatCoordinate(this.longitude) + " " + this.getEastWest();
|
return "N";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the location accuracy in meter.
|
* Returns "E" or "W" depending on the longitude.
|
||||||
*
|
*
|
||||||
* @return float
|
* @return "E" or "W".
|
||||||
*/
|
*/
|
||||||
public float getAccuracy() {
|
private String getEastWest() {
|
||||||
return accuracy;
|
if (this.longitude >= 0 && this.longitude < 180) {
|
||||||
}
|
return "E";
|
||||||
|
}
|
||||||
/**
|
|
||||||
* Return the longitude in degrees.
|
return "W";
|
||||||
*
|
}
|
||||||
* @return double
|
|
||||||
*/
|
/**
|
||||||
public double getLongitude() {
|
* Returns a nicely formatted coordinate string. Used e.g. in
|
||||||
return longitude;
|
* the detail view.
|
||||||
}
|
*
|
||||||
|
* @return The formatted string.
|
||||||
/**
|
*/
|
||||||
* Return the latitude in degrees.
|
public String getPrettyCoordinateString() {
|
||||||
*
|
return formatCoordinate(this.latitude) + " " + this.getNorthSouth() + ", "
|
||||||
* @return double
|
+ formatCoordinate(this.longitude) + " " + this.getEastWest();
|
||||||
*/
|
}
|
||||||
public double getLatitude() {
|
|
||||||
return latitude;
|
/**
|
||||||
}
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ import javax.inject.Singleton;
|
||||||
|
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
@Singleton
|
|
||||||
public class LocationServiceManager implements LocationListener {
|
public class LocationServiceManager implements LocationListener {
|
||||||
public static final int LOCATION_REQUEST = 1;
|
public static final int LOCATION_REQUEST = 1;
|
||||||
|
|
||||||
|
|
@ -32,21 +31,36 @@ public class LocationServiceManager implements LocationListener {
|
||||||
private final List<LocationUpdateListener> locationListeners = new CopyOnWriteArrayList<>();
|
private final List<LocationUpdateListener> locationListeners = new CopyOnWriteArrayList<>();
|
||||||
private boolean isLocationManagerRegistered = false;
|
private boolean isLocationManagerRegistered = false;
|
||||||
|
|
||||||
@Inject
|
/**
|
||||||
|
* Constructs a new instance of LocationServiceManager.
|
||||||
|
* @param context the context
|
||||||
|
*/
|
||||||
public LocationServiceManager(Context context) {
|
public LocationServiceManager(Context context) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
|
this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current status of the GPS provider.
|
||||||
|
* @return true if the GPS provider is enabled
|
||||||
|
*/
|
||||||
public boolean isProviderEnabled() {
|
public boolean isProviderEnabled() {
|
||||||
return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
|
return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the location permission is granted.
|
||||||
|
* @return true if the location permission is granted
|
||||||
|
*/
|
||||||
public boolean isLocationPermissionGranted() {
|
public boolean isLocationPermissionGranted() {
|
||||||
return ContextCompat.checkSelfPermission(context,
|
return ContextCompat.checkSelfPermission(context,
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED;
|
Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests the location permission to be granted.
|
||||||
|
* @param activity the activity
|
||||||
|
*/
|
||||||
public void requestPermissions(Activity activity) {
|
public void requestPermissions(Activity activity) {
|
||||||
if (activity.isFinishing()) {
|
if (activity.isFinishing()) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -79,6 +93,11 @@ public class LocationServiceManager implements LocationListener {
|
||||||
&& requestLocationUpdatesFromProvider(LocationManager.GPS_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) {
|
private boolean requestLocationUpdatesFromProvider(String locationProvider) {
|
||||||
try {
|
try {
|
||||||
locationManager.requestLocationUpdates(locationProvider,
|
locationManager.requestLocationUpdates(locationProvider,
|
||||||
|
|
@ -95,6 +114,12 @@ public class LocationServiceManager implements LocationListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
protected boolean isBetterLocation(Location location, Location currentBestLocation) {
|
||||||
if (currentBestLocation == null) {
|
if (currentBestLocation == null) {
|
||||||
// A new location is always better than no location
|
// A new location is always better than no location
|
||||||
|
|
@ -158,12 +183,20 @@ 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) {
|
public void addLocationListener(LocationUpdateListener listener) {
|
||||||
if (!locationListeners.contains(listener)) {
|
if (!locationListeners.contains(listener)) {
|
||||||
locationListeners.add(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) {
|
public void removeLocationListener(LocationUpdateListener listener) {
|
||||||
locationListeners.remove(listener);
|
locationListeners.remove(listener);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v4.app.Fragment;
|
|
||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
@ -22,6 +21,9 @@ import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Provider;
|
||||||
|
|
||||||
import fr.free.nrw.commons.License;
|
import fr.free.nrw.commons.License;
|
||||||
import fr.free.nrw.commons.LicenseList;
|
import fr.free.nrw.commons.LicenseList;
|
||||||
import fr.free.nrw.commons.Media;
|
import fr.free.nrw.commons.Media;
|
||||||
|
|
@ -29,11 +31,12 @@ import fr.free.nrw.commons.MediaDataExtractor;
|
||||||
import fr.free.nrw.commons.MediaWikiImageView;
|
import fr.free.nrw.commons.MediaWikiImageView;
|
||||||
import fr.free.nrw.commons.PageTitle;
|
import fr.free.nrw.commons.PageTitle;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
|
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||||
import fr.free.nrw.commons.location.LatLng;
|
import fr.free.nrw.commons.location.LatLng;
|
||||||
import fr.free.nrw.commons.ui.widget.CompatTextView;
|
import fr.free.nrw.commons.ui.widget.CompatTextView;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
public class MediaDetailFragment extends Fragment {
|
public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
||||||
|
|
||||||
private boolean editable;
|
private boolean editable;
|
||||||
private MediaDetailPagerFragment.MediaDetailProvider detailProvider;
|
private MediaDetailPagerFragment.MediaDetailProvider detailProvider;
|
||||||
|
|
@ -53,6 +56,9 @@ public class MediaDetailFragment extends Fragment {
|
||||||
return mf;
|
return mf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
Provider<MediaDataExtractor> mediaDataExtractorProvider;
|
||||||
|
|
||||||
private MediaWikiImageView image;
|
private MediaWikiImageView image;
|
||||||
private MediaDetailSpacer spacer;
|
private MediaDetailSpacer spacer;
|
||||||
private int initialListTop = 0;
|
private int initialListTop = 0;
|
||||||
|
|
@ -69,7 +75,7 @@ public class MediaDetailFragment extends Fragment {
|
||||||
private boolean categoriesPresent = false;
|
private boolean categoriesPresent = false;
|
||||||
private ViewTreeObserver.OnGlobalLayoutListener layoutListener; // for layout stuff, only used once!
|
private ViewTreeObserver.OnGlobalLayoutListener layoutListener; // for layout stuff, only used once!
|
||||||
private ViewTreeObserver.OnScrollChangedListener scrollListener;
|
private ViewTreeObserver.OnScrollChangedListener scrollListener;
|
||||||
DataSetObserver dataObserver;
|
private DataSetObserver dataObserver;
|
||||||
private AsyncTask<Void,Void,Boolean> detailFetchTask;
|
private AsyncTask<Void,Void,Boolean> detailFetchTask;
|
||||||
private LicenseList licenseList;
|
private LicenseList licenseList;
|
||||||
|
|
||||||
|
|
@ -188,13 +194,13 @@ public class MediaDetailFragment extends Fragment {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPreExecute() {
|
protected void onPreExecute() {
|
||||||
extractor = new MediaDataExtractor(media.getFilename(), licenseList);
|
extractor = mediaDataExtractorProvider.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Boolean doInBackground(Void... voids) {
|
protected Boolean doInBackground(Void... voids) {
|
||||||
try {
|
try {
|
||||||
extractor.fetch();
|
extractor.fetch(media.getFilename(), licenseList);
|
||||||
return Boolean.TRUE;
|
return Boolean.TRUE;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Timber.d(e);
|
Timber.d(e);
|
||||||
|
|
@ -377,7 +383,7 @@ public class MediaDetailFragment extends Fragment {
|
||||||
private void openMap(LatLng coordinates) {
|
private void openMap(LatLng coordinates) {
|
||||||
//Open map app at given position
|
//Open map app at given position
|
||||||
Uri gmmIntentUri = Uri.parse(
|
Uri gmmIntentUri = Uri.parse(
|
||||||
"geo:0,0?q=" + coordinates.getLatitude() + "," + coordinates.getLatitude());
|
"geo:0,0?q=" + coordinates.getLatitude() + "," + coordinates.getLongitude());
|
||||||
Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri);
|
Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri);
|
||||||
|
|
||||||
if (mapIntent.resolveActivity(getActivity().getPackageManager()) != null) {
|
if (mapIntent.resolveActivity(getActivity().getPackageManager()) != null) {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package fr.free.nrw.commons.media;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.DownloadManager;
|
import android.app.DownloadManager;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
import android.database.DataSetObserver;
|
import android.database.DataSetObserver;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
|
@ -24,20 +25,27 @@ import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
|
||||||
import fr.free.nrw.commons.Media;
|
import fr.free.nrw.commons.Media;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
|
import fr.free.nrw.commons.auth.SessionManager;
|
||||||
import fr.free.nrw.commons.contributions.Contribution;
|
import fr.free.nrw.commons.contributions.Contribution;
|
||||||
import fr.free.nrw.commons.contributions.ContributionsActivity;
|
import fr.free.nrw.commons.contributions.ContributionsActivity;
|
||||||
import fr.free.nrw.commons.mwapi.EventLog;
|
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||||
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
|
|
||||||
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
|
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
|
||||||
import static android.content.Context.DOWNLOAD_SERVICE;
|
import static android.content.Context.DOWNLOAD_SERVICE;
|
||||||
import static android.content.Intent.ACTION_VIEW;
|
import static android.content.Intent.ACTION_VIEW;
|
||||||
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
|
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
|
||||||
import static fr.free.nrw.commons.CommonsApplication.EVENT_SHARE_ATTEMPT;
|
|
||||||
|
|
||||||
public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPageChangeListener {
|
public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment implements ViewPager.OnPageChangeListener {
|
||||||
|
|
||||||
|
@Inject MediaWikiApi mwApi;
|
||||||
|
@Inject SessionManager sessionManager;
|
||||||
|
@Inject @Named("default_preferences") SharedPreferences prefs;
|
||||||
|
|
||||||
private ViewPager pager;
|
private ViewPager pager;
|
||||||
private Boolean editable;
|
private Boolean editable;
|
||||||
|
|
@ -99,12 +107,7 @@ public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPa
|
||||||
Media m = provider.getMediaAtPosition(pager.getCurrentItem());
|
Media m = provider.getMediaAtPosition(pager.getCurrentItem());
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
case R.id.menu_share_current_image:
|
case R.id.menu_share_current_image:
|
||||||
// Share - this is just logs it, intent set in onCreateOptionsMenu, around line 252
|
// Share - intent set in onCreateOptionsMenu, around line 252
|
||||||
CommonsApplication app = (CommonsApplication) getActivity().getApplication();
|
|
||||||
EventLog.schema(EVENT_SHARE_ATTEMPT)
|
|
||||||
.param("username", app.getCurrentAccount().name)
|
|
||||||
.param("filename", m.getFilename())
|
|
||||||
.log();
|
|
||||||
return true;
|
return true;
|
||||||
case R.id.menu_browser_current_image:
|
case R.id.menu_browser_current_image:
|
||||||
// View in browser
|
// View in browser
|
||||||
|
|
@ -161,9 +164,7 @@ public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPa
|
||||||
req.allowScanningByMediaScanner();
|
req.allowScanningByMediaScanner();
|
||||||
req.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
|
req.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !(ContextCompat.checkSelfPermission(getContext(), READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED)) {
|
||||||
&& !(ContextCompat.checkSelfPermission(getContext(),
|
|
||||||
READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED)) {
|
|
||||||
Snackbar.make(getView(), R.string.read_storage_permission_rationale,
|
Snackbar.make(getView(), R.string.read_storage_permission_rationale,
|
||||||
Snackbar.LENGTH_INDEFINITE).setAction(R.string.ok,
|
Snackbar.LENGTH_INDEFINITE).setAction(R.string.ok,
|
||||||
view -> ActivityCompat.requestPermissions(getActivity(),
|
view -> ActivityCompat.requestPermissions(getActivity(),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package fr.free.nrw.commons.modifications;
|
package fr.free.nrw.commons.modifications;
|
||||||
|
|
||||||
import android.content.ContentProvider;
|
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.content.UriMatcher;
|
import android.content.UriMatcher;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
|
|
@ -10,39 +9,40 @@ import android.net.Uri;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.data.DBOpenHelper;
|
||||||
|
import fr.free.nrw.commons.di.CommonsDaggerContentProvider;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
public class ModificationsContentProvider extends ContentProvider {
|
import static fr.free.nrw.commons.modifications.ModifierSequenceDao.Table.TABLE_NAME;
|
||||||
|
|
||||||
|
public class ModificationsContentProvider extends CommonsDaggerContentProvider {
|
||||||
|
|
||||||
private static final int MODIFICATIONS = 1;
|
private static final int MODIFICATIONS = 1;
|
||||||
private static final int MODIFICATIONS_ID = 2;
|
private static final int MODIFICATIONS_ID = 2;
|
||||||
|
|
||||||
public static final String AUTHORITY = "fr.free.nrw.commons.modifications.contentprovider";
|
public static final String MODIFICATIONS_AUTHORITY = "fr.free.nrw.commons.modifications.contentprovider";
|
||||||
private static final String BASE_PATH = "modifications";
|
public static final String BASE_PATH = "modifications";
|
||||||
|
|
||||||
public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY + "/" + BASE_PATH);
|
public static final Uri BASE_URI = Uri.parse("content://" + MODIFICATIONS_AUTHORITY + "/" + BASE_PATH);
|
||||||
|
|
||||||
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||||
static {
|
static {
|
||||||
uriMatcher.addURI(AUTHORITY, BASE_PATH, MODIFICATIONS);
|
uriMatcher.addURI(MODIFICATIONS_AUTHORITY, BASE_PATH, MODIFICATIONS);
|
||||||
uriMatcher.addURI(AUTHORITY, BASE_PATH + "/#", MODIFICATIONS_ID);
|
uriMatcher.addURI(MODIFICATIONS_AUTHORITY, BASE_PATH + "/#", MODIFICATIONS_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Uri uriForId(int id) {
|
public static Uri uriForId(int id) {
|
||||||
return Uri.parse(BASE_URI.toString() + "/" + id);
|
return Uri.parse(BASE_URI.toString() + "/" + id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Inject DBOpenHelper dbOpenHelper;
|
||||||
@Override
|
|
||||||
public boolean onCreate() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
||||||
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
|
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
|
||||||
queryBuilder.setTables(ModifierSequence.Table.TABLE_NAME);
|
queryBuilder.setTables(TABLE_NAME);
|
||||||
|
|
||||||
int uriType = uriMatcher.match(uri);
|
int uriType = uriMatcher.match(uri);
|
||||||
|
|
||||||
|
|
@ -53,7 +53,7 @@ public class ModificationsContentProvider extends ContentProvider {
|
||||||
throw new IllegalArgumentException("Unknown URI" + uri);
|
throw new IllegalArgumentException("Unknown URI" + uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
SQLiteDatabase db = CommonsApplication.getInstance().getDBOpenHelper().getReadableDatabase();
|
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
|
||||||
|
|
||||||
Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder);
|
Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder);
|
||||||
cursor.setNotificationUri(getContext().getContentResolver(), uri);
|
cursor.setNotificationUri(getContext().getContentResolver(), uri);
|
||||||
|
|
@ -69,11 +69,11 @@ public class ModificationsContentProvider extends ContentProvider {
|
||||||
@Override
|
@Override
|
||||||
public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
|
public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
|
||||||
int uriType = uriMatcher.match(uri);
|
int uriType = uriMatcher.match(uri);
|
||||||
SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase();
|
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||||
long id = 0;
|
long id = 0;
|
||||||
switch (uriType) {
|
switch (uriType) {
|
||||||
case MODIFICATIONS:
|
case MODIFICATIONS:
|
||||||
id = sqlDB.insert(ModifierSequence.Table.TABLE_NAME, null, contentValues);
|
id = sqlDB.insert(TABLE_NAME, null, contentValues);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Unknown URI: " + uri);
|
throw new IllegalArgumentException("Unknown URI: " + uri);
|
||||||
|
|
@ -85,11 +85,11 @@ public class ModificationsContentProvider extends ContentProvider {
|
||||||
@Override
|
@Override
|
||||||
public int delete(@NonNull Uri uri, String s, String[] strings) {
|
public int delete(@NonNull Uri uri, String s, String[] strings) {
|
||||||
int uriType = uriMatcher.match(uri);
|
int uriType = uriMatcher.match(uri);
|
||||||
SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase();
|
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||||
switch (uriType) {
|
switch (uriType) {
|
||||||
case MODIFICATIONS_ID:
|
case MODIFICATIONS_ID:
|
||||||
String id = uri.getLastPathSegment();
|
String id = uri.getLastPathSegment();
|
||||||
sqlDB.delete(ModifierSequence.Table.TABLE_NAME,
|
sqlDB.delete(TABLE_NAME,
|
||||||
"_id = ?",
|
"_id = ?",
|
||||||
new String[] { id }
|
new String[] { id }
|
||||||
);
|
);
|
||||||
|
|
@ -103,13 +103,13 @@ public class ModificationsContentProvider extends ContentProvider {
|
||||||
public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
|
public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
|
||||||
Timber.d("Hello, bulk insert! (ModificationsContentProvider)");
|
Timber.d("Hello, bulk insert! (ModificationsContentProvider)");
|
||||||
int uriType = uriMatcher.match(uri);
|
int uriType = uriMatcher.match(uri);
|
||||||
SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase();
|
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||||
sqlDB.beginTransaction();
|
sqlDB.beginTransaction();
|
||||||
switch (uriType) {
|
switch (uriType) {
|
||||||
case MODIFICATIONS:
|
case MODIFICATIONS:
|
||||||
for (ContentValues value: values) {
|
for (ContentValues value: values) {
|
||||||
Timber.d("Inserting! %s", value);
|
Timber.d("Inserting! %s", value);
|
||||||
sqlDB.insert(ModifierSequence.Table.TABLE_NAME, null, value);
|
sqlDB.insert(TABLE_NAME, null, value);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|
@ -131,11 +131,11 @@ public class ModificationsContentProvider extends ContentProvider {
|
||||||
In here, the only concat created argument is for id. It is cast to an int, and will error out otherwise.
|
In here, the only concat created argument is for id. It is cast to an int, and will error out otherwise.
|
||||||
*/
|
*/
|
||||||
int uriType = uriMatcher.match(uri);
|
int uriType = uriMatcher.match(uri);
|
||||||
SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase();
|
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||||
int rowsUpdated = 0;
|
int rowsUpdated = 0;
|
||||||
switch (uriType) {
|
switch (uriType) {
|
||||||
case MODIFICATIONS:
|
case MODIFICATIONS:
|
||||||
rowsUpdated = sqlDB.update(ModifierSequence.Table.TABLE_NAME,
|
rowsUpdated = sqlDB.update(TABLE_NAME,
|
||||||
contentValues,
|
contentValues,
|
||||||
selection,
|
selection,
|
||||||
selectionArgs);
|
selectionArgs);
|
||||||
|
|
@ -144,9 +144,9 @@ public class ModificationsContentProvider extends ContentProvider {
|
||||||
int id = Integer.valueOf(uri.getLastPathSegment());
|
int id = Integer.valueOf(uri.getLastPathSegment());
|
||||||
|
|
||||||
if (TextUtils.isEmpty(selection)) {
|
if (TextUtils.isEmpty(selection)) {
|
||||||
rowsUpdated = sqlDB.update(ModifierSequence.Table.TABLE_NAME,
|
rowsUpdated = sqlDB.update(TABLE_NAME,
|
||||||
contentValues,
|
contentValues,
|
||||||
ModifierSequence.Table.COLUMN_ID + " = ?",
|
ModifierSequenceDao.Table.COLUMN_ID + " = ?",
|
||||||
new String[] { String.valueOf(id) } );
|
new String[] { String.valueOf(id) } );
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException("Parameter `selection` should be empty when updating an ID");
|
throw new IllegalArgumentException("Parameter `selection` should be empty when updating an ID");
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
package fr.free.nrw.commons.modifications;
|
package fr.free.nrw.commons.modifications;
|
||||||
|
|
||||||
import android.accounts.Account;
|
import android.accounts.Account;
|
||||||
import android.accounts.AccountManager;
|
|
||||||
import android.accounts.AuthenticatorException;
|
|
||||||
import android.accounts.OperationCanceledException;
|
|
||||||
import android.content.AbstractThreadedSyncAdapter;
|
import android.content.AbstractThreadedSyncAdapter;
|
||||||
import android.content.ContentProviderClient;
|
import android.content.ContentProviderClient;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
@ -14,15 +11,24 @@ import android.os.RemoteException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
import javax.inject.Inject;
|
||||||
import fr.free.nrw.commons.Utils;
|
|
||||||
|
import fr.free.nrw.commons.auth.SessionManager;
|
||||||
import fr.free.nrw.commons.contributions.Contribution;
|
import fr.free.nrw.commons.contributions.Contribution;
|
||||||
|
import fr.free.nrw.commons.contributions.ContributionDao;
|
||||||
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
|
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
|
||||||
|
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
|
public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||||
|
|
||||||
|
@Inject MediaWikiApi mwApi;
|
||||||
|
@Inject ContributionDao contributionDao;
|
||||||
|
@Inject ModifierSequenceDao modifierSequenceDao;
|
||||||
|
@Inject
|
||||||
|
SessionManager sessionManager;
|
||||||
|
|
||||||
public ModificationsSyncAdapter(Context context, boolean autoInitialize) {
|
public ModificationsSyncAdapter(Context context, boolean autoInitialize) {
|
||||||
super(context, autoInitialize);
|
super(context, autoInitialize);
|
||||||
}
|
}
|
||||||
|
|
@ -30,6 +36,11 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||||
@Override
|
@Override
|
||||||
public void onPerformSync(Account account, Bundle bundle, String s, ContentProviderClient contentProviderClient, SyncResult syncResult) {
|
public void onPerformSync(Account account, Bundle bundle, String s, ContentProviderClient contentProviderClient, SyncResult syncResult) {
|
||||||
// This code is fraught with possibilities of race conditions, but lalalalala I can't hear you!
|
// This code is fraught with possibilities of race conditions, but lalalalala I can't hear you!
|
||||||
|
ApplicationlessInjection
|
||||||
|
.getInstance(getContext()
|
||||||
|
.getApplicationContext())
|
||||||
|
.getCommonsApplicationComponent()
|
||||||
|
.inject(this);
|
||||||
|
|
||||||
Cursor allModifications;
|
Cursor allModifications;
|
||||||
try {
|
try {
|
||||||
|
|
@ -44,27 +55,17 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String authCookie;
|
String authCookie = sessionManager.getAuthCookie();
|
||||||
try {
|
if (isNullOrWhiteSpace(authCookie)) {
|
||||||
authCookie = AccountManager.get(getContext()).blockingGetAuthToken(account, "", false);
|
|
||||||
} catch (OperationCanceledException | AuthenticatorException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
} catch (IOException e) {
|
|
||||||
Timber.d("Could not authenticate :(");
|
Timber.d("Could not authenticate :(");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Utils.isNullOrWhiteSpace(authCookie)) {
|
mwApi.setAuthCookie(authCookie);
|
||||||
Timber.d("Could not authenticate :(");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
|
|
||||||
api.setAuthCookie(authCookie);
|
|
||||||
String editToken;
|
String editToken;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
editToken = api.getEditToken();
|
editToken = mwApi.getEditToken();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Timber.d("Can not retreive edit token!");
|
Timber.d("Can not retreive edit token!");
|
||||||
return;
|
return;
|
||||||
|
|
@ -76,28 +77,36 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||||
|
|
||||||
ContentProviderClient contributionsClient = null;
|
ContentProviderClient contributionsClient = null;
|
||||||
try {
|
try {
|
||||||
contributionsClient = getContext().getContentResolver().acquireContentProviderClient(ContributionsContentProvider.AUTHORITY);
|
contributionsClient = getContext().getContentResolver().acquireContentProviderClient(ContributionsContentProvider.CONTRIBUTION_AUTHORITY);
|
||||||
|
|
||||||
while (!allModifications.isAfterLast()) {
|
while (!allModifications.isAfterLast()) {
|
||||||
ModifierSequence sequence = ModifierSequence.fromCursor(allModifications);
|
ModifierSequence sequence = modifierSequenceDao.fromCursor(allModifications);
|
||||||
sequence.setContentProviderClient(contentProviderClient);
|
|
||||||
Contribution contrib;
|
Contribution contrib;
|
||||||
|
|
||||||
Cursor contributionCursor;
|
Cursor contributionCursor;
|
||||||
|
|
||||||
|
if (contributionsClient == null) {
|
||||||
|
Timber.e("ContributionsClient is null. This should not happen!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
contributionCursor = contributionsClient.query(sequence.getMediaUri(), null, null, null, null);
|
contributionCursor = contributionsClient.query(sequence.getMediaUri(), null, null, null, null);
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
contributionCursor.moveToFirst();
|
|
||||||
contrib = Contribution.fromCursor(contributionCursor);
|
|
||||||
|
|
||||||
if (contrib.getState() == Contribution.STATE_COMPLETED) {
|
if (contributionCursor != null) {
|
||||||
|
contributionCursor.moveToFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
contrib = contributionDao.fromCursor(contributionCursor);
|
||||||
|
|
||||||
|
if (contrib != null && contrib.getState() == Contribution.STATE_COMPLETED) {
|
||||||
String pageContent;
|
String pageContent;
|
||||||
try {
|
try {
|
||||||
pageContent = api.revisionsByFilename(contrib.getFilename());
|
pageContent = mwApi.revisionsByFilename(contrib.getFilename());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Timber.d("Network fuckup on modifications sync!");
|
Timber.d("Network messed up on modifications sync!");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,19 +115,19 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||||
|
|
||||||
String editResult;
|
String editResult;
|
||||||
try {
|
try {
|
||||||
editResult = api.edit(editToken, processedPageContent, contrib.getFilename(), sequence.getEditSummary());
|
editResult = mwApi.edit(editToken, processedPageContent, contrib.getFilename(), sequence.getEditSummary());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Timber.d("Network fuckup on modifications sync!");
|
Timber.d("Network messed up on modifications sync!");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
Timber.d("Response is %s", editResult);
|
Timber.d("Response is %s", editResult);
|
||||||
|
|
||||||
if (!editResult.equals("Success")) {
|
if (!"Success".equals(editResult)) {
|
||||||
// FIXME: Log this somewhere else
|
// FIXME: Log this somewhere else
|
||||||
Timber.d("Non success result! %s", editResult);
|
Timber.d("Non success result! %s", editResult);
|
||||||
} else {
|
} else {
|
||||||
sequence.delete();
|
modifierSequenceDao.delete(sequence);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
allModifications.moveToNext();
|
allModifications.moveToNext();
|
||||||
|
|
@ -129,4 +138,8 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isNullOrWhiteSpace(String value) {
|
||||||
|
return value == null || value.trim().isEmpty();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,8 @@
|
||||||
package fr.free.nrw.commons.modifications;
|
package fr.free.nrw.commons.modifications;
|
||||||
|
|
||||||
import android.content.ContentProviderClient;
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.RemoteException;
|
|
||||||
|
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
@ -17,14 +11,13 @@ public class ModifierSequence {
|
||||||
private Uri mediaUri;
|
private Uri mediaUri;
|
||||||
private ArrayList<PageModifier> modifiers;
|
private ArrayList<PageModifier> modifiers;
|
||||||
private Uri contentUri;
|
private Uri contentUri;
|
||||||
private ContentProviderClient client;
|
|
||||||
|
|
||||||
public ModifierSequence(Uri mediaUri) {
|
public ModifierSequence(Uri mediaUri) {
|
||||||
this.mediaUri = mediaUri;
|
this.mediaUri = mediaUri;
|
||||||
modifiers = new ArrayList<>();
|
modifiers = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ModifierSequence(Uri mediaUri, JSONObject data) {
|
ModifierSequence(Uri mediaUri, JSONObject data) {
|
||||||
this(mediaUri);
|
this(mediaUri);
|
||||||
JSONArray modifiersJSON = data.optJSONArray("modifiers");
|
JSONArray modifiersJSON = data.optJSONArray("modifiers");
|
||||||
for (int i = 0; i < modifiersJSON.length(); i++) {
|
for (int i = 0; i < modifiersJSON.length(); i++) {
|
||||||
|
|
@ -32,7 +25,7 @@ public class ModifierSequence {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Uri getMediaUri() {
|
Uri getMediaUri() {
|
||||||
return mediaUri;
|
return mediaUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,14 +33,14 @@ public class ModifierSequence {
|
||||||
modifiers.add(modifier);
|
modifiers.add(modifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String executeModifications(String pageName, String pageContents) {
|
String executeModifications(String pageName, String pageContents) {
|
||||||
for (PageModifier modifier: modifiers) {
|
for (PageModifier modifier: modifiers) {
|
||||||
pageContents = modifier.doModification(pageName, pageContents);
|
pageContents = modifier.doModification(pageName, pageContents);
|
||||||
}
|
}
|
||||||
return pageContents;
|
return pageContents;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getEditSummary() {
|
String getEditSummary() {
|
||||||
StringBuilder editSummary = new StringBuilder();
|
StringBuilder editSummary = new StringBuilder();
|
||||||
for (PageModifier modifier: modifiers) {
|
for (PageModifier modifier: modifiers) {
|
||||||
editSummary.append(modifier.getEditSumary()).append(" ");
|
editSummary.append(modifier.getEditSumary()).append(" ");
|
||||||
|
|
@ -56,97 +49,16 @@ public class ModifierSequence {
|
||||||
return editSummary.toString();
|
return editSummary.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public JSONObject toJSON() {
|
ArrayList<PageModifier> getModifiers() {
|
||||||
JSONObject data = new JSONObject();
|
return modifiers;
|
||||||
try {
|
|
||||||
JSONArray modifiersJSON = new JSONArray();
|
|
||||||
for (PageModifier modifier: modifiers) {
|
|
||||||
modifiersJSON.put(modifier.toJSON());
|
|
||||||
}
|
|
||||||
data.put("modifiers", modifiersJSON);
|
|
||||||
return data;
|
|
||||||
} catch (JSONException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ContentValues toContentValues() {
|
Uri getContentUri() {
|
||||||
ContentValues cv = new ContentValues();
|
return contentUri;
|
||||||
cv.put(Table.COLUMN_MEDIA_URI, mediaUri.toString());
|
|
||||||
cv.put(Table.COLUMN_DATA, toJSON().toString());
|
|
||||||
return cv;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ModifierSequence fromCursor(Cursor cursor) {
|
void setContentUri(Uri contentUri) {
|
||||||
// Hardcoding column positions!
|
this.contentUri = contentUri;
|
||||||
ModifierSequence ms = null;
|
|
||||||
try {
|
|
||||||
ms = new ModifierSequence(Uri.parse(cursor.getString(1)),
|
|
||||||
new JSONObject(cursor.getString(2)));
|
|
||||||
} catch (JSONException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
ms.contentUri = ModificationsContentProvider.uriForId(cursor.getInt(0));
|
|
||||||
|
|
||||||
return ms;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void save() {
|
|
||||||
try {
|
|
||||||
if (contentUri == null) {
|
|
||||||
contentUri = client.insert(ModificationsContentProvider.BASE_URI, this.toContentValues());
|
|
||||||
} else {
|
|
||||||
client.update(contentUri, toContentValues(), null, null);
|
|
||||||
}
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void delete() {
|
|
||||||
try {
|
|
||||||
client.delete(contentUri, null, null);
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setContentProviderClient(ContentProviderClient client) {
|
|
||||||
this.client = client;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Table {
|
|
||||||
public static final String TABLE_NAME = "modifications";
|
|
||||||
|
|
||||||
public static final String COLUMN_ID = "_id";
|
|
||||||
public static final String COLUMN_MEDIA_URI = "mediauri";
|
|
||||||
public static final String COLUMN_DATA = "data";
|
|
||||||
|
|
||||||
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
|
|
||||||
public static final String[] ALL_FIELDS = {
|
|
||||||
COLUMN_ID,
|
|
||||||
COLUMN_MEDIA_URI,
|
|
||||||
COLUMN_DATA
|
|
||||||
};
|
|
||||||
|
|
||||||
private static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
|
|
||||||
+ "_id INTEGER PRIMARY KEY,"
|
|
||||||
+ "mediauri STRING,"
|
|
||||||
+ "data STRING"
|
|
||||||
+ ");";
|
|
||||||
|
|
||||||
public static void onCreate(SQLiteDatabase db) {
|
|
||||||
db.execSQL(CREATE_TABLE_STATEMENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void onUpdate(SQLiteDatabase db, int from, int to) {
|
|
||||||
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
|
|
||||||
onCreate(db);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void onDelete(SQLiteDatabase db) {
|
|
||||||
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
|
|
||||||
onCreate(db);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
package fr.free.nrw.commons.modifications;
|
||||||
|
|
||||||
|
import android.content.ContentProviderClient;
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
import javax.inject.Provider;
|
||||||
|
|
||||||
|
public class ModifierSequenceDao {
|
||||||
|
|
||||||
|
private final Provider<ContentProviderClient> clientProvider;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public ModifierSequenceDao(@Named("modification") Provider<ContentProviderClient> clientProvider) {
|
||||||
|
this.clientProvider = clientProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void save(ModifierSequence sequence) {
|
||||||
|
ContentProviderClient db = clientProvider.get();
|
||||||
|
try {
|
||||||
|
if (sequence.getContentUri() == null) {
|
||||||
|
sequence.setContentUri(db.insert(ModificationsContentProvider.BASE_URI, toContentValues(sequence)));
|
||||||
|
} else {
|
||||||
|
db.update(sequence.getContentUri(), toContentValues(sequence), null, null);
|
||||||
|
}
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
} finally {
|
||||||
|
db.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(ModifierSequence sequence) {
|
||||||
|
ContentProviderClient db = clientProvider.get();
|
||||||
|
try {
|
||||||
|
db.delete(sequence.getContentUri(), null, null);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
} finally {
|
||||||
|
db.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ModifierSequence fromCursor(Cursor cursor) {
|
||||||
|
// Hardcoding column positions!
|
||||||
|
ModifierSequence ms = null;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package fr.free.nrw.commons.mwapi;
|
package fr.free.nrw.commons.mwapi;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
|
@ -21,10 +23,14 @@ import org.apache.http.params.CoreProtocolPNames;
|
||||||
import org.apache.http.util.EntityUtils;
|
import org.apache.http.util.EntityUtils;
|
||||||
import org.mediawiki.api.ApiResult;
|
import org.mediawiki.api.ApiResult;
|
||||||
import org.mediawiki.api.MWApi;
|
import org.mediawiki.api.MWApi;
|
||||||
|
import org.w3c.dom.Node;
|
||||||
|
import org.w3c.dom.NodeList;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
@ -34,12 +40,17 @@ import java.util.concurrent.Callable;
|
||||||
|
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
import fr.free.nrw.commons.BuildConfig;
|
||||||
import fr.free.nrw.commons.PageTitle;
|
import fr.free.nrw.commons.PageTitle;
|
||||||
import fr.free.nrw.commons.Utils;
|
import fr.free.nrw.commons.notification.Notification;
|
||||||
import in.yuvi.http.fluent.Http;
|
import in.yuvi.http.fluent.Http;
|
||||||
import io.reactivex.Observable;
|
import io.reactivex.Observable;
|
||||||
import io.reactivex.Single;
|
import io.reactivex.Single;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
import static fr.free.nrw.commons.notification.NotificationType.UNKNOWN;
|
||||||
|
import static fr.free.nrw.commons.notification.NotificationUtils.getNotificationFromApiResult;
|
||||||
|
import static fr.free.nrw.commons.notification.NotificationUtils.getNotificationType;
|
||||||
|
import static fr.free.nrw.commons.notification.NotificationUtils.isCommonsNotification;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Addshore
|
* @author Addshore
|
||||||
*/
|
*/
|
||||||
|
|
@ -49,17 +60,27 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
||||||
private static final String THUMB_SIZE = "640";
|
private static final String THUMB_SIZE = "640";
|
||||||
private AbstractHttpClient httpClient;
|
private AbstractHttpClient httpClient;
|
||||||
private MWApi api;
|
private MWApi api;
|
||||||
|
private Context context;
|
||||||
|
private SharedPreferences sharedPreferences;
|
||||||
|
|
||||||
public ApacheHttpClientMediaWikiApi(String apiURL) {
|
public ApacheHttpClientMediaWikiApi(Context context, String apiURL, SharedPreferences sharedPreferences) {
|
||||||
|
this.context = context;
|
||||||
BasicHttpParams params = new BasicHttpParams();
|
BasicHttpParams params = new BasicHttpParams();
|
||||||
SchemeRegistry schemeRegistry = new SchemeRegistry();
|
SchemeRegistry schemeRegistry = new SchemeRegistry();
|
||||||
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
|
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
|
||||||
final SSLSocketFactory sslSocketFactory = SSLSocketFactory.getSocketFactory();
|
final SSLSocketFactory sslSocketFactory = SSLSocketFactory.getSocketFactory();
|
||||||
schemeRegistry.register(new Scheme("https", sslSocketFactory, 443));
|
schemeRegistry.register(new Scheme("https", sslSocketFactory, 443));
|
||||||
ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry);
|
ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry);
|
||||||
params.setParameter(CoreProtocolPNames.USER_AGENT, "Commons/" + BuildConfig.VERSION_NAME + " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE);
|
params.setParameter(CoreProtocolPNames.USER_AGENT, getUserAgent());
|
||||||
httpClient = new DefaultHttpClient(cm, params);
|
httpClient = new DefaultHttpClient(cm, params);
|
||||||
api = new MWApi(apiURL, httpClient);
|
api = new MWApi(apiURL, httpClient);
|
||||||
|
this.sharedPreferences = sharedPreferences;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NonNull
|
||||||
|
public String getUserAgent() {
|
||||||
|
return "Commons/" + BuildConfig.VERSION_NAME + " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
|
|
@ -74,11 +95,13 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
||||||
* @throws IOException On api request IO issue
|
* @throws IOException On api request IO issue
|
||||||
*/
|
*/
|
||||||
public String login(String username, String password) throws IOException {
|
public String login(String username, String password) throws IOException {
|
||||||
|
String loginToken = getLoginToken();
|
||||||
|
Timber.d("Login token is %s", loginToken);
|
||||||
return getErrorCodeToReturn(api.action("clientlogin")
|
return getErrorCodeToReturn(api.action("clientlogin")
|
||||||
.param("rememberMe", "1")
|
.param("rememberMe", "1")
|
||||||
.param("username", username)
|
.param("username", username)
|
||||||
.param("password", password)
|
.param("password", password)
|
||||||
.param("logintoken", getLoginToken())
|
.param("logintoken", loginToken)
|
||||||
.param("loginreturnurl", "https://commons.wikimedia.org")
|
.param("loginreturnurl", "https://commons.wikimedia.org")
|
||||||
.post());
|
.post());
|
||||||
}
|
}
|
||||||
|
|
@ -91,12 +114,14 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
||||||
* @throws IOException On api request IO issue
|
* @throws IOException On api request IO issue
|
||||||
*/
|
*/
|
||||||
public String login(String username, String password, String twoFactorCode) throws IOException {
|
public String login(String username, String password, String twoFactorCode) throws IOException {
|
||||||
|
String loginToken = getLoginToken();
|
||||||
|
Timber.d("Login token is %s", loginToken);
|
||||||
return getErrorCodeToReturn(api.action("clientlogin")
|
return getErrorCodeToReturn(api.action("clientlogin")
|
||||||
.param("rememberMe", "1")
|
.param("rememberMe", "true")
|
||||||
.param("username", username)
|
.param("username", username)
|
||||||
.param("password", password)
|
.param("password", password)
|
||||||
.param("logintoken", getLoginToken())
|
.param("logintoken", loginToken)
|
||||||
.param("logincontinue", "1")
|
.param("logincontinue", "true")
|
||||||
.param("OATHToken", twoFactorCode)
|
.param("OATHToken", twoFactorCode)
|
||||||
.post());
|
.post());
|
||||||
}
|
}
|
||||||
|
|
@ -121,14 +146,17 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
||||||
String status = loginApiResult.getString("/api/clientlogin/@status");
|
String status = loginApiResult.getString("/api/clientlogin/@status");
|
||||||
if (status.equals("PASS")) {
|
if (status.equals("PASS")) {
|
||||||
api.isLoggedIn = true;
|
api.isLoggedIn = true;
|
||||||
|
setAuthCookieOnLogin(true);
|
||||||
return status;
|
return status;
|
||||||
} else if (status.equals("FAIL")) {
|
} else if (status.equals("FAIL")) {
|
||||||
|
setAuthCookieOnLogin(false);
|
||||||
return loginApiResult.getString("/api/clientlogin/@messagecode");
|
return loginApiResult.getString("/api/clientlogin/@messagecode");
|
||||||
} else if (
|
} else if (
|
||||||
status.equals("UI")
|
status.equals("UI")
|
||||||
&& loginApiResult.getString("/api/clientlogin/requests/_v/@id").equals("TOTPAuthenticationRequest")
|
&& loginApiResult.getString("/api/clientlogin/requests/_v/@id").equals("TOTPAuthenticationRequest")
|
||||||
&& loginApiResult.getString("/api/clientlogin/requests/_v/@provider").equals("Two-factor authentication (OATH).")
|
&& loginApiResult.getString("/api/clientlogin/requests/_v/@provider").equals("Two-factor authentication (OATH).")
|
||||||
) {
|
) {
|
||||||
|
setAuthCookieOnLogin(false);
|
||||||
return "2FA";
|
return "2FA";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,6 +164,18 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
||||||
return "genericerror-" + status;
|
return "genericerror-" + status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setAuthCookieOnLogin(boolean isLoggedIn) {
|
||||||
|
SharedPreferences.Editor editor = sharedPreferences.edit();
|
||||||
|
if (isLoggedIn) {
|
||||||
|
editor.putBoolean("isUserLoggedIn", true);
|
||||||
|
editor.putString("getAuthCookie", api.getAuthCookie());
|
||||||
|
} else {
|
||||||
|
editor.putBoolean("isUserLoggedIn", false);
|
||||||
|
editor.remove("getAuthCookie");
|
||||||
|
}
|
||||||
|
editor.apply();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getAuthCookie() {
|
public String getAuthCookie() {
|
||||||
return api.getAuthCookie();
|
return api.getAuthCookie();
|
||||||
|
|
@ -335,7 +375,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
||||||
logEvents.add(new LogEventResult.LogEvent(
|
logEvents.add(new LogEventResult.LogEvent(
|
||||||
image.getString("@pageid"),
|
image.getString("@pageid"),
|
||||||
image.getString("@title"),
|
image.getString("@title"),
|
||||||
Utils.parseMWDate(image.getString("@timestamp")))
|
parseMWDate(image.getString("@timestamp")))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return logEvents;
|
return logEvents;
|
||||||
|
|
@ -352,6 +392,42 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
||||||
.getString("/api/query/pages/page/revisions/rev");
|
.getString("/api/query/pages/page/revisions/rev");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NonNull
|
||||||
|
public List<Notification> getNotifications() {
|
||||||
|
ApiResult notificationNode = null;
|
||||||
|
try {
|
||||||
|
notificationNode = api.action("query")
|
||||||
|
.param("notprop", "list")
|
||||||
|
.param("format", "xml")
|
||||||
|
.param("meta", "notifications")
|
||||||
|
.param("notfilter", "!read")
|
||||||
|
.get()
|
||||||
|
.getNode("/api/query/notifications/list");
|
||||||
|
} catch (IOException e) {
|
||||||
|
Timber.e("Failed to obtain searchCategories", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notificationNode == null) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Notification> notifications = new ArrayList<>();
|
||||||
|
|
||||||
|
NodeList childNodes = notificationNode.getDocument().getChildNodes();
|
||||||
|
|
||||||
|
for (int i = 0; i < childNodes.getLength(); i++) {
|
||||||
|
Node node = childNodes.item(i);
|
||||||
|
if (isCommonsNotification(node)
|
||||||
|
&& !getNotificationType(node).equals(UNKNOWN)) {
|
||||||
|
notifications.add(getNotificationFromApiResult(context, node));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return notifications;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean existingFile(String fileSha1) throws IOException {
|
public boolean existingFile(String fileSha1) throws IOException {
|
||||||
return api.action("query")
|
return api.action("query")
|
||||||
|
|
@ -402,7 +478,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
||||||
String errorCode = result.getString("/api/error/@code");
|
String errorCode = result.getString("/api/error/@code");
|
||||||
return new UploadResult(resultStatus, errorCode);
|
return new UploadResult(resultStatus, errorCode);
|
||||||
} else {
|
} else {
|
||||||
Date dateUploaded = Utils.parseMWDate(result.getString("/api/upload/imageinfo/@timestamp"));
|
Date dateUploaded = parseMWDate(result.getString("/api/upload/imageinfo/@timestamp"));
|
||||||
String canonicalFilename = "File:" + result.getString("/api/upload/@filename").replace("_", " "); // Title vs Filename
|
String canonicalFilename = "File:" + result.getString("/api/upload/@filename").replace("_", " "); // Title vs Filename
|
||||||
String imageUrl = result.getString("/api/upload/imageinfo/@url");
|
String imageUrl = result.getString("/api/upload/imageinfo/@url");
|
||||||
return new UploadResult(resultStatus, dateUploaded, canonicalFilename, imageUrl);
|
return new UploadResult(resultStatus, dateUploaded, canonicalFilename, imageUrl);
|
||||||
|
|
@ -428,4 +504,13 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
||||||
return Integer.parseInt(uploadCount);
|
return Integer.parseInt(uploadCount);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Date parseMWDate(String mwDate) {
|
||||||
|
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH); // Assuming MW always gives me UTC
|
||||||
|
try {
|
||||||
|
return isoFormat.parse(mwDate);
|
||||||
|
} catch (ParseException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package fr.free.nrw.commons.mwapi;
|
package fr.free.nrw.commons.mwapi;
|
||||||
|
|
||||||
|
import android.content.SharedPreferences;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
|
||||||
import fr.free.nrw.commons.Utils;
|
import fr.free.nrw.commons.Utils;
|
||||||
|
|
@ -15,14 +16,14 @@ public class EventLog {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static LogBuilder schema(String schema, long revision) {
|
private static LogBuilder schema(String schema, long revision, MediaWikiApi mwApi, SharedPreferences prefs) {
|
||||||
return new LogBuilder(schema, revision);
|
return new LogBuilder(schema, revision, mwApi, prefs);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LogBuilder schema(Object[] scid) {
|
public static LogBuilder schema(Object[] scid, MediaWikiApi mwApi, SharedPreferences prefs) {
|
||||||
if (scid.length != 2) {
|
if (scid.length != 2) {
|
||||||
throw new IllegalArgumentException("Needs an object array with schema as first param and revision as second");
|
throw new IllegalArgumentException("Needs an object array with schema as first param and revision as second");
|
||||||
}
|
}
|
||||||
return schema((String) scid[0], (Long) scid[1]);
|
return schema((String) scid[0], (Long) scid[1], mwApi, prefs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package fr.free.nrw.commons.mwapi;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
|
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
@ -12,21 +11,39 @@ import java.net.MalformedURLException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
import fr.free.nrw.commons.BuildConfig;
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
import fr.free.nrw.commons.Utils;
|
import fr.free.nrw.commons.Utils;
|
||||||
import fr.free.nrw.commons.settings.Prefs;
|
import fr.free.nrw.commons.settings.Prefs;
|
||||||
|
|
||||||
|
@SuppressWarnings("WeakerAccess")
|
||||||
public class LogBuilder {
|
public class LogBuilder {
|
||||||
private JSONObject data;
|
private final MediaWikiApi mwApi;
|
||||||
private long rev;
|
private final JSONObject data;
|
||||||
private String schema;
|
private final long rev;
|
||||||
|
private final String schema;
|
||||||
|
private final SharedPreferences prefs;
|
||||||
|
|
||||||
LogBuilder(String schema, long revision) {
|
/**
|
||||||
data = new JSONObject();
|
* Main constructor of LogBuilder
|
||||||
|
*
|
||||||
|
* @param schema Log schema
|
||||||
|
* @param revision Log revision
|
||||||
|
* @param mwApi Wiki media API instance
|
||||||
|
* @param prefs Instance of SharedPreferences
|
||||||
|
*/
|
||||||
|
LogBuilder(String schema, long revision, MediaWikiApi mwApi, SharedPreferences prefs) {
|
||||||
|
this.prefs = prefs;
|
||||||
|
this.data = new JSONObject();
|
||||||
this.schema = schema;
|
this.schema = schema;
|
||||||
this.rev = revision;
|
this.rev = revision;
|
||||||
|
this.mwApi = mwApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds data to preferences
|
||||||
|
* @param key Log key
|
||||||
|
* @param value Log object value
|
||||||
|
* @return LogBuilder
|
||||||
|
*/
|
||||||
public LogBuilder param(String key, Object value) {
|
public LogBuilder param(String key, Object value) {
|
||||||
try {
|
try {
|
||||||
data.put(key, value);
|
data.put(key, value);
|
||||||
|
|
@ -36,6 +53,10 @@ public class LogBuilder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes JSON object to URL
|
||||||
|
* @return URL to JSON object
|
||||||
|
*/
|
||||||
URL toUrl() {
|
URL toUrl() {
|
||||||
JSONObject fullData = new JSONObject();
|
JSONObject fullData = new JSONObject();
|
||||||
try {
|
try {
|
||||||
|
|
@ -56,14 +77,13 @@ public class LogBuilder {
|
||||||
// Use *only* for tracking the user preference change for EventLogging
|
// Use *only* for tracking the user preference change for EventLogging
|
||||||
// Attempting to use anywhere else will cause kitten explosions
|
// Attempting to use anywhere else will cause kitten explosions
|
||||||
public void log(boolean force) {
|
public void log(boolean force) {
|
||||||
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(CommonsApplication.getInstance());
|
if (!prefs.getBoolean(Prefs.TRACKING_ENABLED, true) && !force) {
|
||||||
if (!settings.getBoolean(Prefs.TRACKING_ENABLED, true) && !force) {
|
|
||||||
return; // User has disabled tracking
|
return; // User has disabled tracking
|
||||||
}
|
}
|
||||||
LogTask logTask = new LogTask();
|
LogTask logTask = new LogTask(mwApi);
|
||||||
logTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, this);
|
logTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void log() {
|
public void log() {
|
||||||
log(false);
|
log(false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,26 @@ package fr.free.nrw.commons.mwapi;
|
||||||
|
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
|
|
||||||
class LogTask extends AsyncTask<LogBuilder, Void, Boolean> {
|
class LogTask extends AsyncTask<LogBuilder, Void, Boolean> {
|
||||||
|
|
||||||
|
private final MediaWikiApi mwApi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main constructor of LogTask
|
||||||
|
*
|
||||||
|
* @param mwApi Media wiki API instance
|
||||||
|
*/
|
||||||
|
public LogTask(MediaWikiApi mwApi) {
|
||||||
|
this.mwApi = mwApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs events in background
|
||||||
|
* @param logBuilders LogBuilder instance
|
||||||
|
* @return Background success state ( TRUE or FALSE )
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected Boolean doInBackground(LogBuilder... logBuilders) {
|
protected Boolean doInBackground(LogBuilder... logBuilders) {
|
||||||
return CommonsApplication.getInstance().getMWApi().logEvents(logBuilders);
|
return mwApi.logEvents(logBuilders);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,29 @@ public class MediaResult {
|
||||||
private final String wikiSource;
|
private final String wikiSource;
|
||||||
private final String parseTreeXmlSource;
|
private final String parseTreeXmlSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full-fledged constructor of MediaResult
|
||||||
|
*
|
||||||
|
* @param wikiSource Media wiki source
|
||||||
|
* @param parseTreeXmlSource Media tree parsed in XML
|
||||||
|
*/
|
||||||
MediaResult(String wikiSource, String parseTreeXmlSource) {
|
MediaResult(String wikiSource, String parseTreeXmlSource) {
|
||||||
this.wikiSource = wikiSource;
|
this.wikiSource = wikiSource;
|
||||||
this.parseTreeXmlSource = parseTreeXmlSource;
|
this.parseTreeXmlSource = parseTreeXmlSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets wiki source
|
||||||
|
* @return Wiki source
|
||||||
|
*/
|
||||||
public String getWikiSource() {
|
public String getWikiSource() {
|
||||||
return wikiSource;
|
return wikiSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets tree parsed in XML
|
||||||
|
* @return XML parsed tree
|
||||||
|
*/
|
||||||
public String getParseTreeXmlSource() {
|
public String getParseTreeXmlSource() {
|
||||||
return parseTreeXmlSource;
|
return parseTreeXmlSource;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,15 @@ import android.support.annotation.Nullable;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.notification.Notification;
|
||||||
import io.reactivex.Observable;
|
import io.reactivex.Observable;
|
||||||
import io.reactivex.Single;
|
import io.reactivex.Single;
|
||||||
|
|
||||||
public interface MediaWikiApi {
|
public interface MediaWikiApi {
|
||||||
|
String getUserAgent();
|
||||||
|
|
||||||
String getAuthCookie();
|
String getAuthCookie();
|
||||||
|
|
||||||
void setAuthCookie(String authCookie);
|
void setAuthCookie(String authCookie);
|
||||||
|
|
@ -43,6 +47,9 @@ public interface MediaWikiApi {
|
||||||
@NonNull
|
@NonNull
|
||||||
Observable<String> allCategories(String filter, int searchCatsLimit);
|
Observable<String> allCategories(String filter, int searchCatsLimit);
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
List<Notification> getNotifications() throws IOException;
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
Observable<String> searchTitles(String title, int searchCatsLimit);
|
Observable<String> searchTitles(String title, int searchCatsLimit);
|
||||||
|
|
||||||
|
|
@ -51,6 +58,8 @@ public interface MediaWikiApi {
|
||||||
|
|
||||||
boolean existingFile(String fileSha1) throws IOException;
|
boolean existingFile(String fileSha1) throws IOException;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
LogEventResult logEvents(String user, String lastModified, String queryContinue, int limit) throws IOException;
|
LogEventResult logEvents(String user, String lastModified, String queryContinue, int limit) throws IOException;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,24 @@ public class UploadResult {
|
||||||
private String imageUrl;
|
private String imageUrl;
|
||||||
private String canonicalFilename;
|
private String canonicalFilename;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal constructor
|
||||||
|
*
|
||||||
|
* @param resultStatus Upload result status
|
||||||
|
* @param errorCode Upload error code
|
||||||
|
*/
|
||||||
UploadResult(String resultStatus, String errorCode) {
|
UploadResult(String resultStatus, String errorCode) {
|
||||||
this.resultStatus = resultStatus;
|
this.resultStatus = resultStatus;
|
||||||
this.errorCode = errorCode;
|
this.errorCode = errorCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full-fledged constructor
|
||||||
|
* @param resultStatus Upload result status
|
||||||
|
* @param dateUploaded Uploaded date
|
||||||
|
* @param canonicalFilename Uploaded file name
|
||||||
|
* @param imageUrl Uploaded image file name
|
||||||
|
*/
|
||||||
UploadResult(String resultStatus, Date dateUploaded, String canonicalFilename, String imageUrl) {
|
UploadResult(String resultStatus, Date dateUploaded, String canonicalFilename, String imageUrl) {
|
||||||
this.resultStatus = resultStatus;
|
this.resultStatus = resultStatus;
|
||||||
this.dateUploaded = dateUploaded;
|
this.dateUploaded = dateUploaded;
|
||||||
|
|
@ -21,22 +34,42 @@ public class UploadResult {
|
||||||
this.imageUrl = imageUrl;
|
this.imageUrl = imageUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets uploaded date
|
||||||
|
* @return Upload date
|
||||||
|
*/
|
||||||
public Date getDateUploaded() {
|
public Date getDateUploaded() {
|
||||||
return dateUploaded;
|
return dateUploaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets image url
|
||||||
|
* @return Uploaded image url
|
||||||
|
*/
|
||||||
public String getImageUrl() {
|
public String getImageUrl() {
|
||||||
return imageUrl;
|
return imageUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets canonical file name
|
||||||
|
* @return Uploaded file name
|
||||||
|
*/
|
||||||
public String getCanonicalFilename() {
|
public String getCanonicalFilename() {
|
||||||
return canonicalFilename;
|
return canonicalFilename;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets upload error code
|
||||||
|
* @return Error code
|
||||||
|
*/
|
||||||
public String getErrorCode() {
|
public String getErrorCode() {
|
||||||
return errorCode;
|
return errorCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets upload result status
|
||||||
|
* @return Upload result status
|
||||||
|
*/
|
||||||
public String getResultStatus() {
|
public String getResultStatus() {
|
||||||
return resultStatus;
|
return resultStatus;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,6 @@ import javax.inject.Inject;
|
||||||
|
|
||||||
import butterknife.BindView;
|
import butterknife.BindView;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
import dagger.android.AndroidInjection;
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.location.LatLng;
|
import fr.free.nrw.commons.location.LatLng;
|
||||||
import fr.free.nrw.commons.location.LocationServiceManager;
|
import fr.free.nrw.commons.location.LocationServiceManager;
|
||||||
|
|
@ -48,12 +46,17 @@ import static fr.free.nrw.commons.location.LocationServiceManager.LOCATION_REQUE
|
||||||
|
|
||||||
public class NearbyActivity extends NavigationBaseActivity implements LocationUpdateListener {
|
public class NearbyActivity extends NavigationBaseActivity implements LocationUpdateListener {
|
||||||
|
|
||||||
|
private static final int LOCATION_REQUEST = 1;
|
||||||
|
private static final String MAP_LAST_USED_PREFERENCE = "mapLastUsed";
|
||||||
|
|
||||||
@BindView(R.id.progressBar)
|
@BindView(R.id.progressBar)
|
||||||
ProgressBar progressBar;
|
ProgressBar progressBar;
|
||||||
private static final String MAP_LAST_USED_PREFERENCE = "mapLastUsed";
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
LocationServiceManager locationManager;
|
LocationServiceManager locationManager;
|
||||||
|
@Inject
|
||||||
|
NearbyController nearbyController;
|
||||||
|
|
||||||
private LatLng curLatLang;
|
private LatLng curLatLang;
|
||||||
private Bundle bundle;
|
private Bundle bundle;
|
||||||
private SharedPreferences sharedPreferences;
|
private SharedPreferences sharedPreferences;
|
||||||
|
|
@ -64,7 +67,6 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
AndroidInjection.inject(this);
|
|
||||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
|
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
|
||||||
setContentView(R.layout.activity_nearby);
|
setContentView(R.layout.activity_nearby);
|
||||||
ButterKnife.bind(this);
|
ButterKnife.bind(this);
|
||||||
|
|
@ -279,20 +281,14 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
|
||||||
}
|
}
|
||||||
|
|
||||||
progressBar.setVisibility(View.VISIBLE);
|
progressBar.setVisibility(View.VISIBLE);
|
||||||
setupPlaceList(this);
|
placesDisposable = Observable.fromCallable(() -> nearbyController
|
||||||
}
|
.loadAttractionsFromLocation(curLatLang, this))
|
||||||
|
|
||||||
private void setupPlaceList(Context context) {
|
|
||||||
placesDisposable = Observable.fromCallable(() -> NearbyController
|
|
||||||
.loadAttractionsFromLocation(curLatLang, CommonsApplication.getInstance()))
|
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe((result) -> {
|
.subscribe(this::populatePlaces);
|
||||||
populatePlaces(context, result);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void populatePlaces(Context context, List<Place> placeList) {
|
private void populatePlaces(List<Place> placeList) {
|
||||||
Gson gson = new GsonBuilder()
|
Gson gson = new GsonBuilder()
|
||||||
.registerTypeAdapter(Uri.class, new UriSerializer())
|
.registerTypeAdapter(Uri.class, new UriSerializer())
|
||||||
.create();
|
.create();
|
||||||
|
|
@ -301,7 +297,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
|
||||||
|
|
||||||
if (placeList.size() == 0) {
|
if (placeList.size() == 0) {
|
||||||
int duration = Toast.LENGTH_SHORT;
|
int duration = Toast.LENGTH_SHORT;
|
||||||
Toast toast = Toast.makeText(context, R.string.no_nearby, duration);
|
Toast toast = Toast.makeText(this, R.string.no_nearby, duration);
|
||||||
toast.show();
|
toast.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package fr.free.nrw.commons.nearby;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.support.graphics.drawable.VectorDrawableCompat;
|
import android.support.graphics.drawable.VectorDrawableCompat;
|
||||||
|
|
||||||
import com.mapbox.mapboxsdk.annotations.IconFactory;
|
import com.mapbox.mapboxsdk.annotations.IconFactory;
|
||||||
|
|
@ -15,7 +14,9 @@ import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.location.LatLng;
|
import fr.free.nrw.commons.location.LatLng;
|
||||||
import fr.free.nrw.commons.utils.UiUtils;
|
import fr.free.nrw.commons.utils.UiUtils;
|
||||||
|
|
@ -24,9 +25,17 @@ import timber.log.Timber;
|
||||||
import static fr.free.nrw.commons.utils.LengthUtils.computeDistanceBetween;
|
import static fr.free.nrw.commons.utils.LengthUtils.computeDistanceBetween;
|
||||||
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
|
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
|
||||||
|
|
||||||
|
|
||||||
public class NearbyController {
|
public class NearbyController {
|
||||||
private static final int MAX_RESULTS = 1000;
|
private static final int MAX_RESULTS = 1000;
|
||||||
|
private final NearbyPlaces nearbyPlaces;
|
||||||
|
private final SharedPreferences prefs;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public NearbyController(NearbyPlaces nearbyPlaces,
|
||||||
|
@Named("default_preferences") SharedPreferences prefs) {
|
||||||
|
this.nearbyPlaces = nearbyPlaces;
|
||||||
|
this.prefs = prefs;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepares Place list to make their distance information update later.
|
* Prepares Place list to make their distance information update later.
|
||||||
|
|
@ -34,13 +43,11 @@ public class NearbyController {
|
||||||
* @param context context
|
* @param context context
|
||||||
* @return Place list without distance information
|
* @return Place list without distance information
|
||||||
*/
|
*/
|
||||||
public static List<Place> loadAttractionsFromLocation(LatLng curLatLng, Context context) {
|
public List<Place> loadAttractionsFromLocation(LatLng curLatLng, Context context) {
|
||||||
Timber.d("Loading attractions near %s", curLatLng);
|
Timber.d("Loading attractions near %s", curLatLng);
|
||||||
if (curLatLng == null) {
|
if (curLatLng == null) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
NearbyPlaces nearbyPlaces = CommonsApplication.getInstance().getNearbyPlaces();
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
|
||||||
List<Place> places = prefs.getBoolean("useWikidata", true)
|
List<Place> places = prefs.getBoolean("useWikidata", true)
|
||||||
? nearbyPlaces.getFromWikidataQuery(curLatLng, Locale.getDefault().getLanguage())
|
? nearbyPlaces.getFromWikidataQuery(curLatLng, Locale.getDefault().getLanguage())
|
||||||
: nearbyPlaces.getFromWikiNeedsPictures();
|
: nearbyPlaces.getFromWikiNeedsPictures();
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ public class NearbyInfoDialog extends OverlayDialog {
|
||||||
NearbyInfoDialog mDialog = new NearbyInfoDialog();
|
NearbyInfoDialog mDialog = new NearbyInfoDialog();
|
||||||
Bundle bundle = new Bundle();
|
Bundle bundle = new Bundle();
|
||||||
bundle.putString(ARG_TITLE, place.name);
|
bundle.putString(ARG_TITLE, place.name);
|
||||||
bundle.putString(ARG_DESC, place.getDescription().getText());
|
bundle.putString(ARG_DESC, place.getLongDescription());
|
||||||
bundle.putDouble(ARG_LATITUDE, place.location.getLatitude());
|
bundle.putDouble(ARG_LATITUDE, place.location.getLatitude());
|
||||||
bundle.putDouble(ARG_LONGITUDE, place.location.getLongitude());
|
bundle.putDouble(ARG_LONGITUDE, place.location.getLongitude());
|
||||||
bundle.putParcelable(ARG_SITE_LINK, place.siteLinks);
|
bundle.putParcelable(ARG_SITE_LINK, place.siteLinks);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package fr.free.nrw.commons.nearby;
|
package fr.free.nrw.commons.nearby;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.app.Fragment;
|
import android.support.v4.app.Fragment;
|
||||||
|
|
@ -17,6 +18,8 @@ import java.lang.reflect.Type;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import dagger.android.support.AndroidSupportInjection;
|
||||||
|
import dagger.android.support.DaggerFragment;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.location.LatLng;
|
import fr.free.nrw.commons.location.LatLng;
|
||||||
import fr.free.nrw.commons.utils.UriDeserializer;
|
import fr.free.nrw.commons.utils.UriDeserializer;
|
||||||
|
|
@ -40,6 +43,12 @@ public class NearbyListFragment extends Fragment {
|
||||||
setRetainInstance(true);
|
setRetainInstance(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttach(Context context) {
|
||||||
|
AndroidSupportInjection.inject(this);
|
||||||
|
super.onAttach(context);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater,
|
public View onCreateView(LayoutInflater inflater,
|
||||||
ViewGroup container,
|
ViewGroup container,
|
||||||
|
|
@ -60,7 +69,7 @@ public class NearbyListFragment extends Fragment {
|
||||||
|
|
||||||
Bundle bundle = this.getArguments();
|
Bundle bundle = this.getArguments();
|
||||||
if (bundle != null) {
|
if (bundle != null) {
|
||||||
String gsonPlaceList = bundle.getString("PlaceList");
|
String gsonPlaceList = bundle.getString("PlaceList", "[]");
|
||||||
placeList = gson.fromJson(gsonPlaceList, LIST_TYPE);
|
placeList = gson.fromJson(gsonPlaceList, LIST_TYPE);
|
||||||
|
|
||||||
String gsonLatLng = bundle.getString("CurLatLng");
|
String gsonLatLng = bundle.getString("CurLatLng");
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package fr.free.nrw.commons.nearby;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
@ -77,6 +76,8 @@ public class NearbyMapFragment extends android.support.v4.app.Fragment {
|
||||||
private void setupMapView(Bundle savedInstanceState) {
|
private void setupMapView(Bundle savedInstanceState) {
|
||||||
MapboxMapOptions options = new MapboxMapOptions()
|
MapboxMapOptions options = new MapboxMapOptions()
|
||||||
.styleUrl(Style.OUTDOORS)
|
.styleUrl(Style.OUTDOORS)
|
||||||
|
.logoEnabled(false)
|
||||||
|
.attributionEnabled(false)
|
||||||
.camera(new CameraPosition.Builder()
|
.camera(new CameraPosition.Builder()
|
||||||
.target(new LatLng(curLatLng.getLatitude(), curLatLng.getLongitude()))
|
.target(new LatLng(curLatLng.getLatitude(), curLatLng.getLongitude()))
|
||||||
.zoom(11)
|
.zoom(11)
|
||||||
|
|
@ -99,11 +100,8 @@ public class NearbyMapFragment extends android.support.v4.app.Fragment {
|
||||||
|
|
||||||
addCurrentLocationMarker(mapboxMap);
|
addCurrentLocationMarker(mapboxMap);
|
||||||
});
|
});
|
||||||
if (PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean("theme",false)) {
|
|
||||||
mapView.setStyleUrl(getResources().getString(R.string.map_theme_dark));
|
mapView.setStyleUrl("asset://mapstyle.json");
|
||||||
} else {
|
|
||||||
mapView.setStyleUrl(getResources().getString(R.string.map_theme_light));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ public class NearbyPlaces {
|
||||||
|
|
||||||
public NearbyPlaces() {
|
public NearbyPlaces() {
|
||||||
try {
|
try {
|
||||||
wikidataQuery = FileUtils.readFromResource("/assets/queries/nearby_query.rq");
|
wikidataQuery = FileUtils.readFromResource("/queries/nearby_query.rq");
|
||||||
Timber.v(wikidataQuery);
|
Timber.v(wikidataQuery);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
|
|
@ -126,7 +126,7 @@ public class NearbyPlaces {
|
||||||
|
|
||||||
places.add(new Place(
|
places.add(new Place(
|
||||||
name,
|
name,
|
||||||
Place.Description.fromText(type), // list
|
Place.Label.fromText(type), // list
|
||||||
type, // details
|
type, // details
|
||||||
Uri.parse(icon),
|
Uri.parse(icon),
|
||||||
new LatLng(latitude, longitude, 0),
|
new LatLng(latitude, longitude, 0),
|
||||||
|
|
@ -188,7 +188,7 @@ public class NearbyPlaces {
|
||||||
|
|
||||||
places.add(new Place(
|
places.add(new Place(
|
||||||
name,
|
name,
|
||||||
Place.Description.fromText(type), // list
|
Place.Label.fromText(type), // list
|
||||||
type, // details
|
type, // details
|
||||||
null,
|
null,
|
||||||
new LatLng(latitude, longitude, 0),
|
new LatLng(latitude, longitude, 0),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package fr.free.nrw.commons.nearby;
|
package fr.free.nrw.commons.nearby;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.app.Fragment;
|
import android.support.v4.app.Fragment;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
|
|
@ -7,6 +8,8 @@ import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
|
import dagger.android.support.AndroidSupportInjection;
|
||||||
|
import dagger.android.support.DaggerFragment;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
|
@ -18,6 +21,12 @@ public class NoPermissionsFragment extends Fragment {
|
||||||
public NoPermissionsFragment() {
|
public NoPermissionsFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttach(Context context) {
|
||||||
|
AndroidSupportInjection.inject(this);
|
||||||
|
super.onAttach(context);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||||
Bundle savedInstanceState) {
|
Bundle savedInstanceState) {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import fr.free.nrw.commons.location.LatLng;
|
||||||
public class Place {
|
public class Place {
|
||||||
|
|
||||||
public final String name;
|
public final String name;
|
||||||
private final Description description;
|
private final Label label;
|
||||||
private final String longDescription;
|
private final String longDescription;
|
||||||
private final Uri secondaryImageUrl;
|
private final Uri secondaryImageUrl;
|
||||||
public final LatLng location;
|
public final LatLng location;
|
||||||
|
|
@ -24,18 +24,22 @@ public class Place {
|
||||||
public final Sitelinks siteLinks;
|
public final Sitelinks siteLinks;
|
||||||
|
|
||||||
|
|
||||||
public Place(String name, Description description, String longDescription,
|
public Place(String name, Label label, String longDescription,
|
||||||
Uri secondaryImageUrl, LatLng location, Sitelinks siteLinks) {
|
Uri secondaryImageUrl, LatLng location, Sitelinks siteLinks) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.description = description;
|
this.label = label;
|
||||||
this.longDescription = longDescription;
|
this.longDescription = longDescription;
|
||||||
this.secondaryImageUrl = secondaryImageUrl;
|
this.secondaryImageUrl = secondaryImageUrl;
|
||||||
this.location = location;
|
this.location = location;
|
||||||
this.siteLinks = siteLinks;
|
this.siteLinks = siteLinks;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Description getDescription() {
|
public Label getLabel() {
|
||||||
return description;
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLongDescription() {
|
||||||
|
return longDescription;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDistance(String distance) {
|
public void setDistance(String distance) {
|
||||||
|
|
@ -67,10 +71,8 @@ public class Place {
|
||||||
* Most common types of desc: building, house, cottage, farmhouse,
|
* Most common types of desc: building, house, cottage, farmhouse,
|
||||||
* village, civil parish, church, railway station,
|
* village, civil parish, church, railway station,
|
||||||
* gatehouse, milestone, inn, secondary school, hotel
|
* gatehouse, milestone, inn, secondary school, hotel
|
||||||
*
|
|
||||||
* TODO Give a more accurate class name (see issue #742).
|
|
||||||
*/
|
*/
|
||||||
public enum Description {
|
public enum Label {
|
||||||
|
|
||||||
BUILDING("building", R.drawable.round_icon_generic_building),
|
BUILDING("building", R.drawable.round_icon_generic_building),
|
||||||
HOUSE("house", R.drawable.round_icon_house),
|
HOUSE("house", R.drawable.round_icon_house),
|
||||||
|
|
@ -95,19 +97,19 @@ public class Place {
|
||||||
WATERFALL("waterfall", R.drawable.round_icon_waterfall),
|
WATERFALL("waterfall", R.drawable.round_icon_waterfall),
|
||||||
UNKNOWN("?", R.drawable.round_icon_unknown);
|
UNKNOWN("?", R.drawable.round_icon_unknown);
|
||||||
|
|
||||||
private static final Map<String, Description> TEXT_TO_DESCRIPTION
|
private static final Map<String, Label> TEXT_TO_DESCRIPTION
|
||||||
= new HashMap<>(Description.values().length);
|
= new HashMap<>(Label.values().length);
|
||||||
|
|
||||||
static {
|
static {
|
||||||
for (Description description : values()) {
|
for (Label label : values()) {
|
||||||
TEXT_TO_DESCRIPTION.put(description.text, description);
|
TEXT_TO_DESCRIPTION.put(label.text, label);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final String text;
|
private final String text;
|
||||||
@DrawableRes private final int icon;
|
@DrawableRes private final int icon;
|
||||||
|
|
||||||
Description(String text, @DrawableRes int icon) {
|
Label(String text, @DrawableRes int icon) {
|
||||||
this.text = text;
|
this.text = text;
|
||||||
this.icon = icon;
|
this.icon = icon;
|
||||||
}
|
}
|
||||||
|
|
@ -121,9 +123,9 @@ public class Place {
|
||||||
return icon;
|
return icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Description fromText(String text) {
|
public static Label fromText(String text) {
|
||||||
Description description = TEXT_TO_DESCRIPTION.get(text);
|
Label label = TEXT_TO_DESCRIPTION.get(text);
|
||||||
return description == null ? UNKNOWN : description;
|
return label == null ? UNKNOWN : label;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,13 +43,13 @@ class PlaceRenderer extends Renderer<Place> {
|
||||||
public void render() {
|
public void render() {
|
||||||
Place place = getContent();
|
Place place = getContent();
|
||||||
tvName.setText(place.name);
|
tvName.setText(place.name);
|
||||||
String descriptionText = place.getDescription().getText();
|
String descriptionText = place.getLongDescription();
|
||||||
if (descriptionText.equals("?")) {
|
if (descriptionText.equals("?")) {
|
||||||
descriptionText = getContext().getString(R.string.no_description_found);
|
descriptionText = getContext().getString(R.string.no_description_found);
|
||||||
}
|
}
|
||||||
tvDesc.setText(descriptionText);
|
tvDesc.setText(descriptionText);
|
||||||
distance.setText(place.distance);
|
distance.setText(place.distance);
|
||||||
icon.setImageResource(place.getDescription().getIcon());
|
icon.setImageResource(place.getLabel().getIcon());
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PlaceClickedListener {
|
interface PlaceClickedListener {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package fr.free.nrw.commons.notification;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
|
public class MarkReadResponse {
|
||||||
|
@SuppressWarnings("unused") @Nullable
|
||||||
|
private String result;
|
||||||
|
|
||||||
|
public String result() {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class QueryMarkReadResponse {
|
||||||
|
@SuppressWarnings("unused") @Nullable private MarkReadResponse echomarkread;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package fr.free.nrw.commons.notification;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by root on 18.12.2017.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class Notification {
|
||||||
|
public NotificationType notificationType;
|
||||||
|
public String notificationText;
|
||||||
|
public String date;
|
||||||
|
public String description;
|
||||||
|
public String link;
|
||||||
|
|
||||||
|
public Notification(NotificationType notificationType, String notificationText, String date, String description, String link) {
|
||||||
|
this.notificationType = notificationType;
|
||||||
|
this.notificationText = notificationText;
|
||||||
|
this.date = date;
|
||||||
|
this.description = description;
|
||||||
|
this.link = link;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
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,68 @@
|
||||||
|
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 java.util.Calendar;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import butterknife.BindView;
|
||||||
|
import butterknife.ButterKnife;
|
||||||
|
import fr.free.nrw.commons.R;
|
||||||
|
import fr.free.nrw.commons.utils.DateUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,57 +1,70 @@
|
||||||
package fr.free.nrw.commons.settings;
|
package fr.free.nrw.commons.settings;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.os.Bundle;
|
||||||
import android.content.Intent;
|
import android.preference.PreferenceManager;
|
||||||
import android.os.Bundle;
|
import android.support.v7.app.AppCompatDelegate;
|
||||||
import android.preference.PreferenceManager;
|
import android.view.MenuItem;
|
||||||
import android.support.v7.app.AppCompatDelegate;
|
|
||||||
import android.view.MenuItem;
|
import butterknife.ButterKnife;
|
||||||
|
import fr.free.nrw.commons.R;
|
||||||
import butterknife.ButterKnife;
|
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
||||||
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;
|
public class SettingsActivity extends NavigationBaseActivity {
|
||||||
|
private AppCompatDelegate settingsDelegate;
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
/**
|
||||||
// Check prefs on every activity starts
|
* to be called when the activity starts
|
||||||
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme",false)) {
|
* @param savedInstanceState the previously saved state
|
||||||
setTheme(R.style.DarkAppTheme);
|
*/
|
||||||
} else {
|
@Override
|
||||||
setTheme(R.style.LightAppTheme);
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
}
|
// Check prefs on every activity starts
|
||||||
|
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme",false)) {
|
||||||
super.onCreate(savedInstanceState);
|
setTheme(R.style.DarkAppTheme);
|
||||||
setContentView(R.layout.activity_settings);
|
} else {
|
||||||
|
setTheme(R.style.LightAppTheme);
|
||||||
ButterKnife.bind(this);
|
}
|
||||||
initDrawer();
|
|
||||||
}
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_settings);
|
||||||
// Get an action bar
|
|
||||||
@Override
|
ButterKnife.bind(this);
|
||||||
protected void onPostCreate(Bundle savedInstanceState) {
|
initDrawer();
|
||||||
super.onPostCreate(savedInstanceState);
|
}
|
||||||
if (settingsDelegate == null) {
|
|
||||||
settingsDelegate = AppCompatDelegate.create(this, null);
|
// Get an action bar
|
||||||
}
|
/**
|
||||||
settingsDelegate.onPostCreate(savedInstanceState);
|
* takes care of actions taken after the creation has happened
|
||||||
|
* @param savedInstanceState the saved state
|
||||||
//Get an up button
|
*/
|
||||||
//settingsDelegate.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
@Override
|
||||||
}
|
protected void onPostCreate(Bundle savedInstanceState) {
|
||||||
|
super.onPostCreate(savedInstanceState);
|
||||||
//Handle action-bar clicks
|
if (settingsDelegate == null) {
|
||||||
@Override
|
settingsDelegate = AppCompatDelegate.create(this, null);
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
}
|
||||||
switch (item.getItemId()) {
|
settingsDelegate.onPostCreate(savedInstanceState);
|
||||||
case android.R.id.home:
|
|
||||||
finish();
|
//Get an up button
|
||||||
return true;
|
//settingsDelegate.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
default:
|
}
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
/**
|
||||||
}
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -15,28 +15,37 @@ import android.preference.EditTextPreference;
|
||||||
import android.preference.ListPreference;
|
import android.preference.ListPreference;
|
||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
import android.preference.PreferenceFragment;
|
import android.preference.PreferenceFragment;
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.v4.app.ActivityCompat;
|
|
||||||
import android.support.v4.content.ContextCompat;
|
import android.support.v4.content.ContextCompat;
|
||||||
import android.support.v4.content.FileProvider;
|
import android.support.v4.content.FileProvider;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
|
||||||
|
import dagger.android.AndroidInjection;
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
import fr.free.nrw.commons.BuildConfig;
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
import fr.free.nrw.commons.CommonsApplication;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.Utils;
|
import fr.free.nrw.commons.Utils;
|
||||||
|
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||||
import fr.free.nrw.commons.utils.FileUtils;
|
import fr.free.nrw.commons.utils.FileUtils;
|
||||||
|
|
||||||
public class SettingsFragment extends PreferenceFragment {
|
public class SettingsFragment extends PreferenceFragment {
|
||||||
|
|
||||||
private static final int REQUEST_CODE_WRITE_EXTERNAL_STORAGE = 100;
|
private static final int REQUEST_CODE_WRITE_EXTERNAL_STORAGE = 100;
|
||||||
|
|
||||||
|
@Inject @Named("default_preferences") SharedPreferences prefs;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
ApplicationlessInjection
|
||||||
|
.getInstance(getActivity().getApplicationContext())
|
||||||
|
.getCommonsApplicationComponent()
|
||||||
|
.inject(this);
|
||||||
|
|
||||||
// Load the preferences from an XML resource
|
// Load the preferences from an XML resource
|
||||||
addPreferencesFromResource(R.xml.preferences);
|
addPreferencesFromResource(R.xml.preferences);
|
||||||
|
|
@ -58,14 +67,12 @@ public class SettingsFragment extends PreferenceFragment {
|
||||||
});
|
});
|
||||||
|
|
||||||
final EditTextPreference uploadLimit = (EditTextPreference) findPreference("uploads");
|
final EditTextPreference uploadLimit = (EditTextPreference) findPreference("uploads");
|
||||||
final SharedPreferences sharedPref = PreferenceManager
|
int uploads = prefs.getInt(Prefs.UPLOADS_SHOWING, 100);
|
||||||
.getDefaultSharedPreferences(CommonsApplication.getInstance());
|
|
||||||
int uploads = sharedPref.getInt(Prefs.UPLOADS_SHOWING, 100);
|
|
||||||
uploadLimit.setText(uploads + "");
|
uploadLimit.setText(uploads + "");
|
||||||
uploadLimit.setSummary(uploads + "");
|
uploadLimit.setSummary(uploads + "");
|
||||||
uploadLimit.setOnPreferenceChangeListener((preference, newValue) -> {
|
uploadLimit.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
int value = Integer.parseInt(newValue.toString());
|
int value = Integer.parseInt(newValue.toString());
|
||||||
final SharedPreferences.Editor editor = sharedPref.edit();
|
final SharedPreferences.Editor editor = prefs.edit();
|
||||||
if (value > 500) {
|
if (value > 500) {
|
||||||
new AlertDialog.Builder(getActivity())
|
new AlertDialog.Builder(getActivity())
|
||||||
.setTitle(R.string.maximum_limit)
|
.setTitle(R.string.maximum_limit)
|
||||||
|
|
@ -133,7 +140,7 @@ public class SettingsFragment extends PreferenceFragment {
|
||||||
Intent feedbackIntent = new Intent(Intent.ACTION_SEND);
|
Intent feedbackIntent = new Intent(Intent.ACTION_SEND);
|
||||||
feedbackIntent.setType("message/rfc822");
|
feedbackIntent.setType("message/rfc822");
|
||||||
feedbackIntent.putExtra(Intent.EXTRA_EMAIL,
|
feedbackIntent.putExtra(Intent.EXTRA_EMAIL,
|
||||||
new String[]{CommonsApplication.FEEDBACK_EMAIL});
|
new String[]{CommonsApplication.LOGS_PRIVATE_EMAIL});
|
||||||
feedbackIntent.putExtra(Intent.EXTRA_SUBJECT,
|
feedbackIntent.putExtra(Intent.EXTRA_SUBJECT,
|
||||||
String.format(CommonsApplication.FEEDBACK_EMAIL_SUBJECT,
|
String.format(CommonsApplication.FEEDBACK_EMAIL_SUBJECT,
|
||||||
BuildConfig.VERSION_NAME));
|
BuildConfig.VERSION_NAME));
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,17 @@ package fr.free.nrw.commons.theme;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.support.v7.app.AppCompatActivity;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.Utils;
|
import fr.free.nrw.commons.di.CommonsDaggerAppCompatActivity;
|
||||||
|
|
||||||
public class BaseActivity extends AppCompatActivity {
|
public abstract class BaseActivity extends CommonsDaggerAppCompatActivity {
|
||||||
boolean currentTheme;
|
boolean currentTheme;
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
if (Utils.isDarkTheme(this)) {
|
boolean currentThemeIsDark = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme", false);
|
||||||
|
if (currentThemeIsDark){
|
||||||
currentTheme = true;
|
currentTheme = true;
|
||||||
setTheme(R.style.DarkAppTheme);
|
setTheme(R.style.DarkAppTheme);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -27,7 +26,7 @@ public class BaseActivity extends AppCompatActivity {
|
||||||
@Override
|
@Override
|
||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
// Restart activity if theme is changed
|
// Restart activity if theme is changed
|
||||||
boolean newTheme = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme",false);
|
boolean newTheme = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme", false);
|
||||||
if (currentTheme != newTheme) { //is activity theme changed
|
if (currentTheme != newTheme) { //is activity theme changed
|
||||||
Intent intent = getIntent();
|
Intent intent = getIntent();
|
||||||
finish();
|
finish();
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import fr.free.nrw.commons.auth.AccountUtil;
|
||||||
import fr.free.nrw.commons.auth.LoginActivity;
|
import fr.free.nrw.commons.auth.LoginActivity;
|
||||||
import fr.free.nrw.commons.contributions.ContributionsActivity;
|
import fr.free.nrw.commons.contributions.ContributionsActivity;
|
||||||
import fr.free.nrw.commons.nearby.NearbyActivity;
|
import fr.free.nrw.commons.nearby.NearbyActivity;
|
||||||
|
import fr.free.nrw.commons.notification.NotificationActivity;
|
||||||
import fr.free.nrw.commons.settings.SettingsActivity;
|
import fr.free.nrw.commons.settings.SettingsActivity;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
|
@ -62,10 +63,10 @@ public abstract class NavigationBaseActivity extends BaseActivity
|
||||||
private void setUserName() {
|
private void setUserName() {
|
||||||
|
|
||||||
View navHeaderView = navigationView.getHeaderView(0);
|
View navHeaderView = navigationView.getHeaderView(0);
|
||||||
TextView username = (TextView) navHeaderView.findViewById(R.id.username);
|
TextView username = navHeaderView.findViewById(R.id.username);
|
||||||
|
|
||||||
AccountManager accountManager = AccountManager.get(this);
|
AccountManager accountManager = AccountManager.get(this);
|
||||||
Account[] allAccounts = accountManager.getAccountsByType(AccountUtil.accountType());
|
Account[] allAccounts = accountManager.getAccountsByType(AccountUtil.ACCOUNT_TYPE);
|
||||||
if (allAccounts.length != 0) {
|
if (allAccounts.length != 0) {
|
||||||
username.setText(allAccounts[0].name);
|
username.setText(allAccounts[0].name);
|
||||||
}
|
}
|
||||||
|
|
@ -143,6 +144,10 @@ public abstract class NavigationBaseActivity extends BaseActivity
|
||||||
.setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel())
|
.setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel())
|
||||||
.show();
|
.show();
|
||||||
return true;
|
return true;
|
||||||
|
case R.id.action_notifications:
|
||||||
|
drawerLayout.closeDrawer(navigationView);
|
||||||
|
NotificationActivity.startYourself(this);
|
||||||
|
return true;
|
||||||
default:
|
default:
|
||||||
Timber.e("Unknown option [%s] selected from the navigation menu", itemId);
|
Timber.e("Unknown option [%s] selected from the navigation menu", itemId);
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -1,73 +1,96 @@
|
||||||
package fr.free.nrw.commons.ui.widget;
|
package fr.free.nrw.commons.ui.widget;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by mikel on 07/08/2017.
|
* Created by mikel on 07/08/2017.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.TypedArray;
|
import android.content.res.TypedArray;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v4.view.ViewCompat;
|
import android.support.v4.view.ViewCompat;
|
||||||
import android.support.v7.widget.AppCompatDrawableManager;
|
import android.support.v7.widget.AppCompatDrawableManager;
|
||||||
import android.support.v7.widget.AppCompatTextView;
|
import android.support.v7.widget.AppCompatTextView;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.utils.UiUtils;
|
import fr.free.nrw.commons.utils.UiUtils;
|
||||||
|
|
||||||
public class CompatTextView extends AppCompatTextView {
|
/**
|
||||||
public CompatTextView(Context context) {
|
* a text view compatible with older versions of the platform
|
||||||
super(context);
|
*/
|
||||||
init(null);
|
public class CompatTextView extends AppCompatTextView {
|
||||||
}
|
|
||||||
|
/**
|
||||||
public CompatTextView(Context context, AttributeSet attrs) {
|
* Constructs a new instance of CompatTextView
|
||||||
super(context, attrs);
|
* @param context the view context
|
||||||
init(attrs);
|
*/
|
||||||
}
|
public CompatTextView(Context context) {
|
||||||
|
super(context);
|
||||||
public CompatTextView(Context context, AttributeSet attrs, int defStyleAttr) {
|
init(null);
|
||||||
super(context, attrs, defStyleAttr);
|
}
|
||||||
init(attrs);
|
|
||||||
}
|
/**
|
||||||
|
* Constructs a new instance of CompatTextView
|
||||||
private void init(@Nullable AttributeSet attrs) {
|
* @param context the view context
|
||||||
if (attrs != null) {
|
* @param attrs the set of attributes for the view
|
||||||
Context context = getContext();
|
*/
|
||||||
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CompatTextView);
|
public CompatTextView(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
// Obtain DrawableManager used to pull Drawables safely, and check if we're in RTL
|
init(attrs);
|
||||||
AppCompatDrawableManager dm = AppCompatDrawableManager.get();
|
}
|
||||||
boolean rtl = ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL;
|
|
||||||
|
/**
|
||||||
// Grab the compat drawable padding from the XML
|
* Constructs a new instance of CompatTextView
|
||||||
float drawablePadding = a.getDimension(R.styleable.CompatTextView_drawablePadding, 0);
|
* @param context
|
||||||
|
* @param attrs
|
||||||
// Grab the compat drawable resources from the XML
|
* @param defStyleAttr
|
||||||
int startDrawableRes = a.getResourceId(R.styleable.CompatTextView_drawableStart, 0);
|
*/
|
||||||
int topDrawableRes = a.getResourceId(R.styleable.CompatTextView_drawableTop, 0);
|
public CompatTextView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
int endDrawableRes = a.getResourceId(R.styleable.CompatTextView_drawableEnd, 0);
|
super(context, attrs, defStyleAttr);
|
||||||
int bottomDrawableRes = a.getResourceId(R.styleable.CompatTextView_drawableBottom, 0);
|
init(attrs);
|
||||||
|
}
|
||||||
// Load the used drawables, fall back to whatever was set in an "android:"
|
|
||||||
Drawable[] currentDrawables = getCompoundDrawables();
|
/**
|
||||||
Drawable left = startDrawableRes != 0
|
* initializes the view
|
||||||
? dm.getDrawable(context, startDrawableRes) : currentDrawables[0];
|
* @param attrs the attribute set of the view, which can be null
|
||||||
Drawable right = endDrawableRes != 0
|
*/
|
||||||
? dm.getDrawable(context, endDrawableRes) : currentDrawables[1];
|
private void init(@Nullable AttributeSet attrs) {
|
||||||
Drawable top = topDrawableRes != 0
|
if (attrs != null) {
|
||||||
? dm.getDrawable(context, topDrawableRes) : currentDrawables[2];
|
Context context = getContext();
|
||||||
Drawable bottom = bottomDrawableRes != 0
|
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CompatTextView);
|
||||||
? dm.getDrawable(context, bottomDrawableRes) : currentDrawables[3];
|
|
||||||
|
// Obtain DrawableManager used to pull Drawables safely, and check if we're in RTL
|
||||||
// Account for RTL and apply the compound Drawables
|
AppCompatDrawableManager dm = AppCompatDrawableManager.get();
|
||||||
Drawable start = rtl ? right : left;
|
boolean rtl = ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL;
|
||||||
Drawable end = rtl ? left : right;
|
|
||||||
setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom);
|
// Grab the compat drawable padding from the XML
|
||||||
setCompoundDrawablePadding((int) UiUtils.convertDpToPixel(drawablePadding, getContext()));
|
float drawablePadding = a.getDimension(R.styleable.CompatTextView_drawablePadding, 0);
|
||||||
|
|
||||||
a.recycle();
|
// 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;
|
package fr.free.nrw.commons.ui.widget;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.support.v7.widget.AppCompatTextView;
|
import android.os.Build;
|
||||||
import android.text.method.LinkMovementMethod;
|
import android.support.v7.widget.AppCompatTextView;
|
||||||
import android.util.AttributeSet;
|
import android.text.Html;
|
||||||
|
import android.text.Spanned;
|
||||||
import fr.free.nrw.commons.Utils;
|
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.
|
* An {@link AppCompatTextView} which formats the text to HTML displayable text and makes any
|
||||||
*/
|
* links clickable.
|
||||||
public class HtmlTextView extends AppCompatTextView {
|
*/
|
||||||
|
public class HtmlTextView extends AppCompatTextView {
|
||||||
public HtmlTextView(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
/**
|
||||||
|
* Constructs a new instance of HtmlTextView
|
||||||
setMovementMethod(LinkMovementMethod.getInstance());
|
* @param context the context of the view
|
||||||
setText(Utils.fromHtml(getText().toString()));
|
* @param attrs the set of attributes for the view
|
||||||
}
|
*/
|
||||||
|
public HtmlTextView(Context context, AttributeSet attrs) {
|
||||||
public void setHtmlText(String newText) {
|
super(context, attrs);
|
||||||
setText(Utils.fromHtml(newText));
|
|
||||||
}
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,46 +1,69 @@
|
||||||
package fr.free.nrw.commons.ui.widget;
|
package fr.free.nrw.commons.ui.widget;
|
||||||
|
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.drawable.ColorDrawable;
|
import android.graphics.drawable.ColorDrawable;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.v4.app.DialogFragment;
|
import android.support.v4.app.DialogFragment;
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.Window;
|
import android.view.Window;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
|
|
||||||
public abstract class OverlayDialog extends DialogFragment {
|
/**
|
||||||
|
* a formatted dialog fragment
|
||||||
@Override
|
* This class is used by NearbyInfoDialog
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
*/
|
||||||
super.onCreate(savedInstanceState);
|
public abstract class OverlayDialog extends DialogFragment {
|
||||||
setStyle(STYLE_NO_FRAME, android.R.style.Theme_Holo_Light);
|
|
||||||
}
|
/**
|
||||||
|
* creates a DialogFragment with the correct style and theme
|
||||||
@Override
|
* @param savedInstanceState
|
||||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
*/
|
||||||
setDialogLayoutToFullScreen();
|
@Override
|
||||||
super.onViewCreated(view, savedInstanceState);
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
}
|
super.onCreate(savedInstanceState);
|
||||||
|
setStyle(STYLE_NO_FRAME, android.R.style.Theme_Holo_Light);
|
||||||
private void setDialogLayoutToFullScreen() {
|
}
|
||||||
Window window = getDialog().getWindow();
|
|
||||||
WindowManager.LayoutParams wlp = window.getAttributes();
|
/**
|
||||||
window.requestFeature(Window.FEATURE_NO_TITLE);
|
* When the view is created, sets the dialog layout to full screen
|
||||||
wlp.gravity = Gravity.BOTTOM;
|
*
|
||||||
wlp.width = WindowManager.LayoutParams.MATCH_PARENT;
|
* @param view the view being used
|
||||||
wlp.height = WindowManager.LayoutParams.MATCH_PARENT;
|
* @param savedInstanceState bundle re-constructed from a previous saved state
|
||||||
window.setAttributes(wlp);
|
*/
|
||||||
}
|
@Override
|
||||||
|
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||||
@NonNull
|
setDialogLayoutToFullScreen();
|
||||||
@Override
|
super.onViewCreated(view, savedInstanceState);
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
}
|
||||||
Dialog dialog = super.onCreateDialog(savedInstanceState);
|
|
||||||
Window window = dialog.getWindow();
|
/**
|
||||||
window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
|
* sets the dialog layout to fullscreen
|
||||||
return dialog;
|
*/
|
||||||
}
|
private void setDialogLayoutToFullScreen() {
|
||||||
}
|
Window window = getDialog().getWindow();
|
||||||
|
WindowManager.LayoutParams wlp = window.getAttributes();
|
||||||
|
window.requestFeature(Window.FEATURE_NO_TITLE);
|
||||||
|
wlp.gravity = Gravity.BOTTOM;
|
||||||
|
wlp.width = WindowManager.LayoutParams.MATCH_PARENT;
|
||||||
|
wlp.height = WindowManager.LayoutParams.MATCH_PARENT;
|
||||||
|
window.setAttributes(wlp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* builds custom dialog container
|
||||||
|
*
|
||||||
|
* @param savedInstanceState the previously saved state
|
||||||
|
* @return the dialog
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||||
|
Dialog dialog = super.onCreateDialog(savedInstanceState);
|
||||||
|
Window window = dialog.getWindow();
|
||||||
|
window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
|
||||||
|
return dialog;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import android.support.v7.app.AlertDialog;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.contributions.ContributionsActivity;
|
import fr.free.nrw.commons.contributions.ContributionsActivity;
|
||||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
|
|
@ -18,6 +17,7 @@ import timber.log.Timber;
|
||||||
* Displays a warning to the user if the file already exists on Commons
|
* Displays a warning to the user if the file already exists on Commons
|
||||||
*/
|
*/
|
||||||
public class ExistingFileAsync extends AsyncTask<Void, Void, Boolean> {
|
public class ExistingFileAsync extends AsyncTask<Void, Void, Boolean> {
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
void onResult(Result result);
|
void onResult(Result result);
|
||||||
}
|
}
|
||||||
|
|
@ -28,14 +28,16 @@ public class ExistingFileAsync extends AsyncTask<Void, Void, Boolean> {
|
||||||
DUPLICATE_CANCELLED
|
DUPLICATE_CANCELLED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final MediaWikiApi api;
|
||||||
private final String fileSha1;
|
private final String fileSha1;
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final Callback callback;
|
private final Callback callback;
|
||||||
|
|
||||||
public ExistingFileAsync(String fileSha1, Context context, Callback callback) {
|
public ExistingFileAsync(String fileSha1, Context context, Callback callback, MediaWikiApi mwApi) {
|
||||||
this.fileSha1 = fileSha1;
|
this.fileSha1 = fileSha1;
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
|
this.api = mwApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -45,7 +47,6 @@ public class ExistingFileAsync extends AsyncTask<Void, Void, Boolean> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Boolean doInBackground(Void... voids) {
|
protected Boolean doInBackground(Void... voids) {
|
||||||
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
|
|
||||||
|
|
||||||
// https://commons.wikimedia.org/w/api.php?action=query&list=allimages&format=xml&aisha1=801957214aba50cb63bb6eb1b0effa50188900ba
|
// https://commons.wikimedia.org/w/api.php?action=query&list=allimages&format=xml&aisha1=801957214aba50cb63bb6eb1b0effa50188900ba
|
||||||
boolean fileExists;
|
boolean fileExists;
|
||||||
|
|
|
||||||
|
|
@ -49,18 +49,14 @@ public class FileUtils {
|
||||||
if ("primary".equalsIgnoreCase(type)) {
|
if ("primary".equalsIgnoreCase(type)) {
|
||||||
return Environment.getExternalStorageDirectory() + "/" + split[1];
|
return Environment.getExternalStorageDirectory() + "/" + split[1];
|
||||||
}
|
}
|
||||||
}
|
} else if (isDownloadsDocument(uri)) { // DownloadsProvider
|
||||||
// DownloadsProvider
|
|
||||||
else if (isDownloadsDocument(uri)) {
|
|
||||||
|
|
||||||
final String id = DocumentsContract.getDocumentId(uri);
|
final String id = DocumentsContract.getDocumentId(uri);
|
||||||
final Uri contentUri = ContentUris.withAppendedId(
|
final Uri contentUri = ContentUris.withAppendedId(
|
||||||
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
|
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
|
||||||
|
|
||||||
return getDataColumn(context, contentUri, null, null);
|
return getDataColumn(context, contentUri, null, null);
|
||||||
}
|
} else if (isMediaDocument(uri)) { // MediaProvider
|
||||||
// MediaProvider
|
|
||||||
else if (isMediaDocument(uri)) {
|
|
||||||
final String docId = DocumentsContract.getDocumentId(uri);
|
final String docId = DocumentsContract.getDocumentId(uri);
|
||||||
final String[] split = docId.split(":");
|
final String[] split = docId.split(":");
|
||||||
final String type = split[0];
|
final String type = split[0];
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import android.location.LocationListener;
|
||||||
import android.location.LocationManager;
|
import android.location.LocationManager;
|
||||||
import android.media.ExifInterface;
|
import android.media.ExifInterface;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.annotation.RequiresApi;
|
import android.support.annotation.RequiresApi;
|
||||||
|
|
@ -16,7 +15,6 @@ import android.support.annotation.RequiresApi;
|
||||||
import java.io.FileDescriptor;
|
import java.io.FileDescriptor;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -26,6 +24,8 @@ import timber.log.Timber;
|
||||||
*/
|
*/
|
||||||
public class GPSExtractor {
|
public class GPSExtractor {
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
private SharedPreferences prefs;
|
||||||
private ExifInterface exif;
|
private ExifInterface exif;
|
||||||
private double decLatitude;
|
private double decLatitude;
|
||||||
private double decLongitude;
|
private double decLongitude;
|
||||||
|
|
@ -38,9 +38,12 @@ public class GPSExtractor {
|
||||||
/**
|
/**
|
||||||
* Construct from the file descriptor of the image (only for API 24 or newer).
|
* Construct from the file descriptor of the image (only for API 24 or newer).
|
||||||
* @param fileDescriptor the file descriptor of the image
|
* @param fileDescriptor the file descriptor of the image
|
||||||
|
* @param context the context
|
||||||
*/
|
*/
|
||||||
@RequiresApi(24)
|
@RequiresApi(24)
|
||||||
public GPSExtractor(@NonNull FileDescriptor fileDescriptor) {
|
public GPSExtractor(@NonNull FileDescriptor fileDescriptor, Context context, SharedPreferences prefs) {
|
||||||
|
this.context = context;
|
||||||
|
this.prefs = prefs;
|
||||||
try {
|
try {
|
||||||
exif = new ExifInterface(fileDescriptor);
|
exif = new ExifInterface(fileDescriptor);
|
||||||
} catch (IOException | IllegalArgumentException e) {
|
} catch (IOException | IllegalArgumentException e) {
|
||||||
|
|
@ -51,13 +54,16 @@ public class GPSExtractor {
|
||||||
/**
|
/**
|
||||||
* Construct from the file path of the image.
|
* Construct from the file path of the image.
|
||||||
* @param path file path of the image
|
* @param path file path of the image
|
||||||
|
* @param context the context
|
||||||
*/
|
*/
|
||||||
public GPSExtractor(@NonNull String path) {
|
public GPSExtractor(@NonNull String path, Context context, SharedPreferences prefs) {
|
||||||
|
this.prefs = prefs;
|
||||||
try {
|
try {
|
||||||
exif = new ExifInterface(path);
|
exif = new ExifInterface(path);
|
||||||
} catch (IOException | IllegalArgumentException e) {
|
} catch (IOException | IllegalArgumentException e) {
|
||||||
Timber.w(e);
|
Timber.w(e);
|
||||||
}
|
}
|
||||||
|
this.context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -65,9 +71,7 @@ public class GPSExtractor {
|
||||||
* @return true if enabled, false if disabled
|
* @return true if enabled, false if disabled
|
||||||
*/
|
*/
|
||||||
private boolean gpsPreferenceEnabled() {
|
private boolean gpsPreferenceEnabled() {
|
||||||
SharedPreferences sharedPref
|
boolean gpsPref = prefs.getBoolean("allowGps", false);
|
||||||
= PreferenceManager.getDefaultSharedPreferences(CommonsApplication.getInstance());
|
|
||||||
boolean gpsPref = sharedPref.getBoolean("allowGps", false);
|
|
||||||
Timber.d("Gps pref set to: %b", gpsPref);
|
Timber.d("Gps pref set to: %b", gpsPref);
|
||||||
return gpsPref;
|
return gpsPref;
|
||||||
}
|
}
|
||||||
|
|
@ -76,8 +80,7 @@ public class GPSExtractor {
|
||||||
* Registers a LocationManager to listen for current location
|
* Registers a LocationManager to listen for current location
|
||||||
*/
|
*/
|
||||||
protected void registerLocationManager() {
|
protected void registerLocationManager() {
|
||||||
locationManager = (LocationManager) CommonsApplication.getInstance()
|
locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
|
||||||
.getSystemService(Context.LOCATION_SERVICE);
|
|
||||||
Criteria criteria = new Criteria();
|
Criteria criteria = new Criteria();
|
||||||
String provider = locationManager.getBestProvider(criteria, true);
|
String provider = locationManager.getBestProvider(criteria, true);
|
||||||
myLocationListener = new MyLocationListener();
|
myLocationListener = new MyLocationListener();
|
||||||
|
|
|
||||||
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