mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-27 04:43:54 +01:00
Merge branch 'master' into featuredImages
This commit is contained in:
commit
463673f942
343 changed files with 11434 additions and 3062 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -27,3 +27,10 @@ app/gradle/wrapper/gradle-wrapper.jar
|
||||||
app/gradlew
|
app/gradlew
|
||||||
app/gradlew.bat
|
app/gradlew.bat
|
||||||
app/gradle/wrapper/gradle-wrapper.properties
|
app/gradle/wrapper/gradle-wrapper.properties
|
||||||
|
|
||||||
|
#related to OpenCV
|
||||||
|
/libraries/opencv/build
|
||||||
|
app/src/main/jniLibs
|
||||||
|
#Below removes all the HTML files related to OpenCV documentation. The documentation can be otherwise found at:
|
||||||
|
#https://docs.opencv.org/3.3.0/
|
||||||
|
/libraries/opencv/javadoc/
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -1,5 +1,15 @@
|
||||||
# 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
|
## v2.6.5 beta
|
||||||
- Changed "send log" feature to only send logs to private Google group forum
|
- 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
|
- Switched to using Wikimedia maps server instead of Mapbox for privacy reasons
|
||||||
|
|
|
||||||
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.
|
||||||
|
|
||||||
|
|
|
||||||
39
ISSUE_TEMPLATE.md
Normal file
39
ISSUE_TEMPLATE.md
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
_Before creating an issue, please search the existing issues to see if a similar one has already been created. You can search issues by specific labels (e.g. `label:nearby `) or just by typing keywords into the search filter._
|
||||||
|
|
||||||
|
**Summary:**
|
||||||
|
|
||||||
|
Summarize your issue in one sentence (what goes wrong, what did you expect to happen)
|
||||||
|
|
||||||
|
**Steps to reproduce:**
|
||||||
|
|
||||||
|
How can we reproduce the issue?
|
||||||
|
|
||||||
|
**Add System logs:**
|
||||||
|
|
||||||
|
Add logcat files here (if possible).
|
||||||
|
|
||||||
|
**Expected behavior:**
|
||||||
|
|
||||||
|
What did you expect the App to do?
|
||||||
|
|
||||||
|
**Observed behavior:**
|
||||||
|
|
||||||
|
What did you see instead? Describe your issue in detail here.
|
||||||
|
|
||||||
|
**Device and Android version:**
|
||||||
|
|
||||||
|
What make and model device (e.g., Samsung J7) did you encounter this on? What Android
|
||||||
|
version (e.g., Android 4.0 Ice Cream Sandwich or Android 6.0 Marshmallow) are you running? Is it
|
||||||
|
the stock version from the manufacturer or a custom ROM ?
|
||||||
|
|
||||||
|
**Commons app version:**
|
||||||
|
|
||||||
|
You can find this information by going to the navigation drawer in the app and tapping 'About'
|
||||||
|
|
||||||
|
**Screen-shots:**
|
||||||
|
|
||||||
|
Can be created by pressing the Volume Down and Power Button at the same time on Android 4.0 and higher.
|
||||||
|
|
||||||
|
**Would you like to work on the issue?**
|
||||||
|
|
||||||
|
Please let us know whether you want to fix the issue by yourself. If not, anyone can get the issue assigned to them.
|
||||||
15
PULL_REQUEST_TEMPLATE.md
Normal file
15
PULL_REQUEST_TEMPLATE.md
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Fixes #{GitHub issue number}
|
||||||
|
|
||||||
|
{Describe the changes made and why they were made.}
|
||||||
|
|
||||||
|
## Tests performed
|
||||||
|
|
||||||
|
Tested on {API level & name of device/emulator}, with {build variant, e.g. ProdDebug}.
|
||||||
|
|
||||||
|
{Please test your PR at least once before submitting.}
|
||||||
|
|
||||||
|
## Screenshots showing what changed
|
||||||
|
|
||||||
|
{Only for user interface changes, otherwise remove this section. See [how to take a screenshot](https://android.stackexchange.com/questions/1759/how-to-take-a-screenshot-with-an-android-device)}
|
||||||
|
|
@ -18,7 +18,7 @@ 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.2.1@aar'){
|
implementation ('com.mapbox.mapboxsdk:mapbox-android-sdk:5.4.1@aar'){
|
||||||
transitive=true
|
transitive=true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -26,6 +26,7 @@ dependencies {
|
||||||
implementation "com.android.support:support-v4:$SUPPORT_LIB_VERSION"
|
implementation "com.android.support:support-v4:$SUPPORT_LIB_VERSION"
|
||||||
implementation "com.android.support:appcompat-v7:$SUPPORT_LIB_VERSION"
|
implementation "com.android.support:appcompat-v7:$SUPPORT_LIB_VERSION"
|
||||||
implementation "com.android.support:design:$SUPPORT_LIB_VERSION"
|
implementation "com.android.support:design:$SUPPORT_LIB_VERSION"
|
||||||
|
implementation "com.android.support:customtabs:$SUPPORT_LIB_VERSION"
|
||||||
|
|
||||||
implementation "com.android.support:cardview-v7:$SUPPORT_LIB_VERSION"
|
implementation "com.android.support:cardview-v7:$SUPPORT_LIB_VERSION"
|
||||||
|
|
||||||
|
|
@ -38,6 +39,10 @@ dependencies {
|
||||||
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
|
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
|
||||||
// Because RxAndroid releases are few and far between, it is recommended you also
|
// Because RxAndroid releases are few and far between, it is recommended you also
|
||||||
// explicitly depend on RxJava's latest version for bug fixes and new features.
|
// explicitly depend on RxJava's latest version for bug fixes and new features.
|
||||||
|
implementation 'com.android.support:multidex:1.0.3'
|
||||||
|
|
||||||
|
testImplementation "org.robolectric:multidex:3.4.2"
|
||||||
|
|
||||||
implementation 'io.reactivex.rxjava2:rxjava:2.1.2'
|
implementation 'io.reactivex.rxjava2:rxjava:2.1.2'
|
||||||
implementation 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
|
implementation 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
|
||||||
implementation 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.0.0'
|
implementation 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.0.0'
|
||||||
|
|
@ -57,7 +62,7 @@ dependencies {
|
||||||
androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
|
androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.12'
|
||||||
testImplementation 'org.robolectric:robolectric:3.4'
|
testImplementation 'org.robolectric:robolectric:3.7.1'
|
||||||
testImplementation 'org.mockito:mockito-all:1.10.19'
|
testImplementation 'org.mockito:mockito-all:1.10.19'
|
||||||
|
|
||||||
testImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1'
|
testImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1'
|
||||||
|
|
@ -73,6 +78,9 @@ dependencies {
|
||||||
implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
|
implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
|
||||||
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
|
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
|
||||||
kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
|
kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
|
||||||
|
|
||||||
|
implementation 'com.borjabravo:readmoretextview:2.1.0'
|
||||||
|
implementation 'com.android.support.constraint:constraint-layout:1.0.2'
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
@ -83,18 +91,31 @@ android {
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId 'fr.free.nrw.commons'
|
applicationId 'fr.free.nrw.commons'
|
||||||
versionCode 80
|
versionCode 82
|
||||||
versionName '2.6.5'
|
versionName '2.6.7'
|
||||||
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
|
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
|
||||||
|
|
||||||
minSdkVersion project.minSdkVersion
|
minSdkVersion project.minSdkVersion
|
||||||
targetSdkVersion project.targetSdkVersion
|
targetSdkVersion project.targetSdkVersion
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|
||||||
|
multiDexEnabled true
|
||||||
|
}
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
unitTests.all {
|
||||||
|
jvmArgs '-noverify'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
|
@ -121,6 +142,7 @@ android {
|
||||||
buildConfigField "String", "EVENTLOG_WIKI", "\"commonswiki\""
|
buildConfigField "String", "EVENTLOG_WIKI", "\"commonswiki\""
|
||||||
buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\""
|
buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\""
|
||||||
buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Main_Page&welcome=yes\""
|
buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Main_Page&welcome=yes\""
|
||||||
|
buildConfigField "String", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.org/wiki/Special:PasswordReset\""
|
||||||
dimension 'tier'
|
dimension 'tier'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -135,6 +157,7 @@ android {
|
||||||
buildConfigField "String", "EVENTLOG_WIKI", "\"commonswiki\""
|
buildConfigField "String", "EVENTLOG_WIKI", "\"commonswiki\""
|
||||||
buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\""
|
buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\""
|
||||||
buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Main_Page&welcome=yes\""
|
buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Main_Page&welcome=yes\""
|
||||||
|
buildConfigField "String", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/Special:PasswordReset\""
|
||||||
dimension 'tier'
|
dimension 'tier'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import android.preference.PreferenceManager;
|
||||||
import android.support.test.espresso.Espresso;
|
import android.support.test.espresso.Espresso;
|
||||||
import android.support.test.espresso.action.ViewActions;
|
import android.support.test.espresso.action.ViewActions;
|
||||||
import android.support.test.espresso.assertion.ViewAssertions;
|
import android.support.test.espresso.assertion.ViewAssertions;
|
||||||
|
import android.support.test.espresso.matcher.PreferenceMatchers;
|
||||||
import android.support.test.espresso.matcher.ViewMatchers;
|
import android.support.test.espresso.matcher.ViewMatchers;
|
||||||
import android.support.test.filters.LargeTest;
|
import android.support.test.filters.LargeTest;
|
||||||
import android.support.test.rule.ActivityTestRule;
|
import android.support.test.rule.ActivityTestRule;
|
||||||
|
|
@ -61,7 +62,7 @@ public class SettingsActivityTest {
|
||||||
@Test
|
@Test
|
||||||
public void oneLicenseIsChecked() {
|
public void oneLicenseIsChecked() {
|
||||||
// click "License" (the first item)
|
// click "License" (the first item)
|
||||||
Espresso.onData(Matchers.anything())
|
Espresso.onData(PreferenceMatchers.withKey("defaultLicense"))
|
||||||
.inAdapterView(ViewMatchers.withId(android.R.id.list))
|
.inAdapterView(ViewMatchers.withId(android.R.id.list))
|
||||||
.atPosition(0)
|
.atPosition(0)
|
||||||
.perform(ViewActions.click());
|
.perform(ViewActions.click());
|
||||||
|
|
@ -74,7 +75,7 @@ public class SettingsActivityTest {
|
||||||
@Test
|
@Test
|
||||||
public void afterClickingCcby4ItWillStay() {
|
public void afterClickingCcby4ItWillStay() {
|
||||||
// click "License" (the first item)
|
// click "License" (the first item)
|
||||||
Espresso.onData(Matchers.anything())
|
Espresso.onData(PreferenceMatchers.withKey("defaultLicense"))
|
||||||
.inAdapterView(ViewMatchers.withId(android.R.id.list))
|
.inAdapterView(ViewMatchers.withId(android.R.id.list))
|
||||||
.atPosition(0)
|
.atPosition(0)
|
||||||
.perform(ViewActions.click());
|
.perform(ViewActions.click());
|
||||||
|
|
@ -85,7 +86,7 @@ public class SettingsActivityTest {
|
||||||
).perform(ViewActions.click());
|
).perform(ViewActions.click());
|
||||||
|
|
||||||
// click "License" (the first item)
|
// click "License" (the first item)
|
||||||
Espresso.onData(Matchers.anything())
|
Espresso.onData(PreferenceMatchers.withKey("defaultLicense"))
|
||||||
.inAdapterView(ViewMatchers.withId(android.R.id.list))
|
.inAdapterView(ViewMatchers.withId(android.R.id.list))
|
||||||
.atPosition(0)
|
.atPosition(0)
|
||||||
.perform(ViewActions.click());
|
.perform(ViewActions.click());
|
||||||
|
|
@ -96,4 +97,4 @@ public class SettingsActivityTest {
|
||||||
ViewMatchers.withText(R.string.license_name_cc_by_four)
|
ViewMatchers.withText(R.string.license_name_cc_by_four)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,48 @@
|
||||||
package fr.free.nrw.commons;
|
package fr.free.nrw.commons;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.text.SpannableString;
|
||||||
|
import android.text.style.UnderlineSpan;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.support.customtabs.CustomTabsIntent;
|
||||||
|
import android.support.v4.content.ContextCompat;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.Spinner;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
|
import static android.widget.Toast.LENGTH_SHORT;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents about screen of this app
|
* 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;
|
||||||
|
@BindView(R.id.about_faq) TextView faqText;
|
||||||
|
|
||||||
|
String language[] = { "Kazakh", "Afrikaans", "Arabic", "Bengali", "Asturianu", "azərbaycanca", "Bikol Central",
|
||||||
|
"Bulgarain", "বাংলা", "Bosanski", "Brezhoneg","català","کوردی", " čeština", " kaszëbsczi", "Cymraeg", "dansk", "Deutsch"
|
||||||
|
,"Zazaki", "डोटेली","Ελληνικά","euskara","español","فارسی","suomi", "français" ,"Nordfriisk", "galego", "Hawaiʻi"
|
||||||
|
,"हिन्दी","Hunsrik","עברית","hornjoserbsce","magyar","interlingua","Bahasa Indonesia", "íslenska","Italian","japanese",
|
||||||
|
"Basa Jawa", "ქართული", " ភាសាខ្មែរ","ಕನ್ನಡ", "한국어","къарачай-малкъар","Кыргызча", "latina", "Lëtzebuergesch", "lietuvių",
|
||||||
|
"latviešu", "Malagasy", "македонски"," മലയാളം","монгол","मराठी","Bahasa Melayu","Malti", "नेपाली", "norsk bokmål",
|
||||||
|
" Nederlands","occitan","ଓଡ଼ିଆ","ਪੰਜਾਬੀ","polsk","Piemontèis","پښتو","português","română","русский"," سنڌي", " සිංහල",
|
||||||
|
"slovenčina"," سرائیکی", "svenska", "தமிழ்", "ತುಳು"," తెలుగు"," ไทย", "Türkçe","українська", "اردو", "Tiếng Việt",
|
||||||
|
" მარგალური","ייִדיש",};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method helps in the creation About screen
|
* This method helps in the creation About screen
|
||||||
|
|
@ -21,16 +50,95 @@ public class AboutActivity extends NavigationBaseActivity {
|
||||||
* @param savedInstanceState Data bundle
|
* @param savedInstanceState Data bundle
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
|
@SuppressLint("StringFormatInvalid")
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_about);
|
setContentView(R.layout.activity_about);
|
||||||
|
|
||||||
ButterKnife.bind(this);
|
ButterKnife.bind(this);
|
||||||
|
String aboutText = getString(R.string.about_license);
|
||||||
String aboutText = getString(R.string.about_license, getString(R.string.trademarked_name));
|
|
||||||
aboutLicenseText.setHtmlText(aboutText);
|
aboutLicenseText.setHtmlText(aboutText);
|
||||||
|
SpannableString content = new SpannableString(getString(R.string.about_faq));
|
||||||
|
content.setSpan(new UnderlineSpan(), 0, content.length(), 0);
|
||||||
|
faqText.setText(content);
|
||||||
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) {
|
||||||
|
Utils.handleWebUrl(this,Uri.parse("https://www.facebook.com/" + "1921335171459985"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnClick(R.id.github_launch_icon)
|
||||||
|
public void launchGithub(View view) {
|
||||||
|
Utils.handleWebUrl(this,Uri.parse("https://github.com/commons-app/apps-android-commons\\"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnClick(R.id.website_launch_icon)
|
||||||
|
public void launchWebsite(View view) {
|
||||||
|
Utils.handleWebUrl(this,Uri.parse("https://commons-app.github.io/\\"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnClick(R.id.about_rate_us)
|
||||||
|
public void launchRatings(View view){
|
||||||
|
Utils.rateApp(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnClick(R.id.about_credits)
|
||||||
|
public void launchCredits(View view) {
|
||||||
|
Utils.handleWebUrl(this,Uri.parse("https://github.com/commons-app/apps-android-commons/blob/master/CREDITS/\\"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnClick(R.id.about_privacy_policy)
|
||||||
|
public void launchPrivacyPolicy(View view) {
|
||||||
|
Utils.handleWebUrl(this,Uri.parse("https://github.com/commons-app/apps-android-commons/wiki/Privacy-policy\\"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@OnClick(R.id.about_faq)
|
||||||
|
public void launchFrequentlyAskedQuesions(View view) {
|
||||||
|
Utils.handleWebUrl(this,Uri.parse("https://github.com/commons-app/apps-android-commons/wiki/Frequently-Asked-Questions\\"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnClick(R.id.about_translate)
|
||||||
|
public void launchTranslate(View view) {
|
||||||
|
final ArrayAdapter<String> languageAdapter = new ArrayAdapter<String>(AboutActivity.this,
|
||||||
|
android.R.layout.simple_spinner_item, language);
|
||||||
|
final Spinner spinner = new Spinner(AboutActivity.this);
|
||||||
|
spinner.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
|
||||||
|
spinner.setAdapter(languageAdapter);
|
||||||
|
spinner.setGravity(17);
|
||||||
|
|
||||||
|
AlertDialog.Builder builder = new AlertDialog.Builder(AboutActivity.this);
|
||||||
|
builder.setView(spinner);
|
||||||
|
builder.setTitle(R.string.about_translate_title)
|
||||||
|
.setMessage(R.string.about_translate_message)
|
||||||
|
.setPositiveButton(R.string.about_translate_proceed, new DialogInterface.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
String languageSelected = spinner.getSelectedItem().toString();
|
||||||
|
TokensTranslations tokensTranslations = new TokensTranslations();
|
||||||
|
tokensTranslations.initailize();
|
||||||
|
String token = tokensTranslations.getTranslationToken(languageSelected);
|
||||||
|
Utils.handleWebUrl(AboutActivity.this,Uri.parse("https://translatewiki.net/w/i.php?title=Special:Translate&language="+token+"&group=commons-android-strings&filter=%21translated&action=translate ?"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
builder.setNegativeButton(R.string.about_translate_cancel, new DialogInterface.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
builder.create().show();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
package fr.free.nrw.commons;
|
package fr.free.nrw.commons;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.support.multidex.MultiDexApplication;
|
||||||
|
|
||||||
import com.facebook.drawee.backends.pipeline.Fresco;
|
import com.facebook.drawee.backends.pipeline.Fresco;
|
||||||
|
import com.facebook.imagepipeline.core.ImagePipelineConfig;
|
||||||
import com.facebook.stetho.Stetho;
|
import com.facebook.stetho.Stetho;
|
||||||
import com.squareup.leakcanary.LeakCanary;
|
import com.squareup.leakcanary.LeakCanary;
|
||||||
import com.squareup.leakcanary.RefWatcher;
|
import com.squareup.leakcanary.RefWatcher;
|
||||||
|
|
@ -18,15 +21,11 @@ import java.io.File;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
|
|
||||||
import dagger.android.AndroidInjector;
|
|
||||||
import dagger.android.DaggerApplication;
|
|
||||||
import fr.free.nrw.commons.auth.SessionManager;
|
import fr.free.nrw.commons.auth.SessionManager;
|
||||||
|
import fr.free.nrw.commons.category.CategoryDao;
|
||||||
import fr.free.nrw.commons.contributions.ContributionDao;
|
import fr.free.nrw.commons.contributions.ContributionDao;
|
||||||
import fr.free.nrw.commons.data.CategoryDao;
|
|
||||||
import fr.free.nrw.commons.data.DBOpenHelper;
|
import fr.free.nrw.commons.data.DBOpenHelper;
|
||||||
import fr.free.nrw.commons.di.CommonsApplicationComponent;
|
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||||
import fr.free.nrw.commons.di.CommonsApplicationModule;
|
|
||||||
import fr.free.nrw.commons.di.DaggerCommonsApplicationComponent;
|
|
||||||
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
|
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
|
||||||
import fr.free.nrw.commons.utils.FileUtils;
|
import fr.free.nrw.commons.utils.FileUtils;
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
|
@ -42,15 +41,16 @@ 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 DaggerApplication {
|
public class CommonsApplication extends MultiDexApplication {
|
||||||
|
|
||||||
@Inject SessionManager sessionManager;
|
@Inject SessionManager sessionManager;
|
||||||
@Inject DBOpenHelper dbOpenHelper;
|
@Inject DBOpenHelper dbOpenHelper;
|
||||||
|
|
||||||
@Inject @Named("default_preferences") SharedPreferences defaultPrefs;
|
@Inject @Named("default_preferences") SharedPreferences defaultPrefs;
|
||||||
@Inject @Named("application_preferences") SharedPreferences applicationPrefs;
|
@Inject @Named("application_preferences") SharedPreferences applicationPrefs;
|
||||||
@Inject @Named("prefs") SharedPreferences otherPrefs;
|
@Inject @Named("prefs") SharedPreferences otherPrefs;
|
||||||
|
|
||||||
public static final String DEFAULT_EDIT_SUMMARY = "Uploaded using Android Commons app";
|
public static final String DEFAULT_EDIT_SUMMARY = "Uploaded using [[COM:MOA|Commons Mobile App]]";
|
||||||
|
|
||||||
public static final String FEEDBACK_EMAIL = "commons-app-android@googlegroups.com";
|
public static final String FEEDBACK_EMAIL = "commons-app-android@googlegroups.com";
|
||||||
|
|
||||||
|
|
@ -58,9 +58,9 @@ public class CommonsApplication extends DaggerApplication {
|
||||||
|
|
||||||
public static final String FEEDBACK_EMAIL_SUBJECT = "Commons Android App (%s) Feedback";
|
public static final String FEEDBACK_EMAIL_SUBJECT = "Commons Android App (%s) Feedback";
|
||||||
|
|
||||||
private CommonsApplicationComponent component;
|
|
||||||
private RefWatcher refWatcher;
|
private RefWatcher refWatcher;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to declare and initialize various components and dependencies
|
* Used to declare and initialize various components and dependencies
|
||||||
*/
|
*/
|
||||||
|
|
@ -68,7 +68,15 @@ public class CommonsApplication extends DaggerApplication {
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
|
|
||||||
Fresco.initialize(this);
|
ApplicationlessInjection
|
||||||
|
.getInstance(this)
|
||||||
|
.getCommonsApplicationComponent()
|
||||||
|
.inject(this);
|
||||||
|
// Set DownsampleEnabled to True to downsample the image in case it's heavy
|
||||||
|
ImagePipelineConfig config = ImagePipelineConfig.newBuilder(this)
|
||||||
|
.setDownsampleEnabled(true)
|
||||||
|
.build();
|
||||||
|
Fresco.initialize(this,config);
|
||||||
if (setupLeakCanary() == RefWatcher.DISABLED) {
|
if (setupLeakCanary() == RefWatcher.DISABLED) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -85,6 +93,7 @@ public class CommonsApplication extends DaggerApplication {
|
||||||
System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0");
|
System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helps in setting up LeakCanary library
|
* Helps in setting up LeakCanary library
|
||||||
* @return instance of LeakCanary
|
* @return instance of LeakCanary
|
||||||
|
|
@ -107,28 +116,6 @@ public class CommonsApplication extends DaggerApplication {
|
||||||
return application.refWatcher;
|
return application.refWatcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Helps in injecting dependency library Dagger
|
|
||||||
* @return Dagger injector
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected AndroidInjector<? extends DaggerApplication> applicationInjector() {
|
|
||||||
return injector();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* used to create injector of application component
|
|
||||||
* @return Application component of Dagger
|
|
||||||
*/
|
|
||||||
public CommonsApplicationComponent injector() {
|
|
||||||
if (component == null) {
|
|
||||||
component = DaggerCommonsApplicationComponent.builder()
|
|
||||||
.appModule(new CommonsApplicationModule(this))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
return component;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* clears data of current application
|
* clears data of current application
|
||||||
* @param context Application context
|
* @param context Application context
|
||||||
|
|
@ -152,9 +139,10 @@ public class CommonsApplication extends DaggerApplication {
|
||||||
.subscribe(() -> {
|
.subscribe(() -> {
|
||||||
Timber.d("All accounts have been removed");
|
Timber.d("All accounts have been removed");
|
||||||
//TODO: fix preference manager
|
//TODO: fix preference manager
|
||||||
defaultPrefs.edit().clear().commit();
|
defaultPrefs.edit().clear().apply();
|
||||||
applicationPrefs.edit().clear().commit();
|
applicationPrefs.edit().clear().apply();
|
||||||
applicationPrefs.edit().putBoolean("firstrun", false).apply();otherPrefs.edit().clear().commit();
|
applicationPrefs.edit().putBoolean("firstrun", false).apply();
|
||||||
|
otherPrefs.edit().clear().apply();
|
||||||
updateAllDatabases();
|
updateAllDatabases();
|
||||||
|
|
||||||
logoutListener.onLogoutComplete();
|
logoutListener.onLogoutComplete();
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@ import android.os.IBinder;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
|
|
||||||
import dagger.android.DaggerService;
|
import fr.free.nrw.commons.di.CommonsDaggerService;
|
||||||
|
|
||||||
public abstract class HandlerService<T> extends DaggerService {
|
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;
|
||||||
|
|
|
||||||
|
|
@ -283,7 +283,7 @@ public class MediaDataExtractor {
|
||||||
/**
|
/**
|
||||||
* Take our metadata and inject it into a live Media object.
|
* Take our metadata and inject it into a live Media object.
|
||||||
* Media object might contain stale or cached data, or emptiness.
|
* Media object might contain stale or cached data, or emptiness.
|
||||||
* @param media
|
* @param media Media object to inject into
|
||||||
*/
|
*/
|
||||||
public void fill(Media media) {
|
public void fill(Media media) {
|
||||||
if (!fetched) {
|
if (!fetched) {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import com.facebook.drawee.view.SimpleDraweeView;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
|
@ -71,7 +72,11 @@ public class MediaWikiImageView extends SimpleDraweeView {
|
||||||
* Initializes MediaWikiImageView.
|
* Initializes MediaWikiImageView.
|
||||||
*/
|
*/
|
||||||
private void init() {
|
private void init() {
|
||||||
((CommonsApplication) getContext().getApplicationContext()).injector().inject(this);
|
ApplicationlessInjection
|
||||||
|
.getInstance(getContext()
|
||||||
|
.getApplicationContext())
|
||||||
|
.getCommonsApplicationComponent()
|
||||||
|
.inject(this);
|
||||||
setHierarchy(GenericDraweeHierarchyBuilder
|
setHierarchy(GenericDraweeHierarchyBuilder
|
||||||
.newInstance(getResources())
|
.newInstance(getResources())
|
||||||
.setPlaceholderImage(VectorDrawableCompat.create(getResources(),
|
.setPlaceholderImage(VectorDrawableCompat.create(getResources(),
|
||||||
|
|
|
||||||
105
app/src/main/java/fr/free/nrw/commons/TokensTranslations.java
Normal file
105
app/src/main/java/fr/free/nrw/commons/TokensTranslations.java
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
package fr.free.nrw.commons;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by Dell on 3/16/2018.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class TokensTranslations {
|
||||||
|
HashMap<String,String> translationToken = new HashMap<String,String>();
|
||||||
|
|
||||||
|
public void initailize() {
|
||||||
|
translationToken.put("Kazakh", "ab");
|
||||||
|
translationToken.put("Afrikaans", "af");
|
||||||
|
translationToken.put("Arabic", "ar");
|
||||||
|
translationToken.put("Bengali", "as");
|
||||||
|
translationToken.put("Asturianu", "ast");
|
||||||
|
translationToken.put("azərbaycanca", "az");
|
||||||
|
translationToken.put("Bikol Central", "bcl");
|
||||||
|
translationToken.put("Bulgarain","bg");
|
||||||
|
translationToken.put("বাংলা", "bn");
|
||||||
|
translationToken.put("Brezhoneg", "br");
|
||||||
|
translationToken.put("Bosanski", "bs");
|
||||||
|
translationToken.put("català", "ca");
|
||||||
|
translationToken.put("کوردی","ckb");
|
||||||
|
translationToken.put("čeština", "cs");
|
||||||
|
translationToken.put("kaszëbsczi", "csb");
|
||||||
|
translationToken.put("Cymraeg", "cy");
|
||||||
|
translationToken.put("dansk", "da");
|
||||||
|
translationToken.put("Deutsch", "de");
|
||||||
|
translationToken.put("Zazaki", "diq");
|
||||||
|
translationToken.put("डोटेली","diq");
|
||||||
|
translationToken.put("Ελληνικά","el");
|
||||||
|
translationToken.put("euskara","eu");
|
||||||
|
translationToken.put("español", "es");
|
||||||
|
translationToken.put("فارسی","fa");
|
||||||
|
translationToken.put("suomi", "fi");
|
||||||
|
translationToken.put("føroyskt", "fo");
|
||||||
|
translationToken.put("français", "fr");
|
||||||
|
translationToken.put("Nordfriisk", "frr");
|
||||||
|
translationToken.put("galego", "gr");
|
||||||
|
translationToken.put("Hawaiʻi", "haw");
|
||||||
|
translationToken.put("עברית","he");
|
||||||
|
translationToken.put("हिन्दी","hi");
|
||||||
|
translationToken.put("Hunsrik", "hrx");
|
||||||
|
translationToken.put("hornjoserbsce", "hsb");
|
||||||
|
translationToken.put("magyar","hu");
|
||||||
|
translationToken.put("interlingua","ia");
|
||||||
|
translationToken.put("Bahasa Indonesia", "id");
|
||||||
|
translationToken.put("íslenska","is");
|
||||||
|
translationToken.put("Italian","it");
|
||||||
|
translationToken.put("japanese","ja");
|
||||||
|
translationToken.put("Basa Jawa","jv");
|
||||||
|
translationToken.put("ქართული", "ka");
|
||||||
|
translationToken.put("Taqbaylit","kab");
|
||||||
|
translationToken.put(" ភាសាខ្មែរ","km");
|
||||||
|
translationToken.put("ಕನ್ನಡ", "kn");
|
||||||
|
translationToken.put("한국어", "ko");
|
||||||
|
translationToken.put("къарачай-малкъар","krc");
|
||||||
|
translationToken.put("Кыргызча","ky");
|
||||||
|
translationToken.put("latina","la");
|
||||||
|
translationToken.put("Lëtzebuergesch","lb");
|
||||||
|
translationToken.put("lietuvių", "lt");
|
||||||
|
translationToken.put("latviešu","lv");
|
||||||
|
translationToken.put("Malagasy","mg");
|
||||||
|
translationToken.put("македонски", "mk");
|
||||||
|
translationToken.put("മലയാളം","ml");
|
||||||
|
translationToken.put("монгол","mn");
|
||||||
|
translationToken.put("मराठी","mr");
|
||||||
|
translationToken.put("Bahasa Melayu","ms");
|
||||||
|
translationToken.put("Malti","mt");
|
||||||
|
translationToken.put("norsk bokmål", "nb");
|
||||||
|
translationToken.put("नेपाली","ne");
|
||||||
|
translationToken.put("Nederlands","nl");
|
||||||
|
translationToken.put("occitan","oc");
|
||||||
|
translationToken.put("ଓଡ଼ିଆ","or");
|
||||||
|
translationToken.put("ਪੰਜਾਬੀ","pa");
|
||||||
|
translationToken.put("polsk", "pl");
|
||||||
|
translationToken.put("Piemontèis","pms");
|
||||||
|
translationToken.put("پښتو","ps");
|
||||||
|
translationToken.put("português","pt");
|
||||||
|
translationToken.put("română","ro");
|
||||||
|
translationToken.put("русский","ru");
|
||||||
|
translationToken.put(" سنڌي","sd");
|
||||||
|
translationToken.put(" සිංහල","si");
|
||||||
|
translationToken.put("slovenčina","sk");
|
||||||
|
translationToken.put(" سرائیکی","skr");
|
||||||
|
translationToken.put("Basa Sunda","su");
|
||||||
|
translationToken.put("svenska","sv");
|
||||||
|
translationToken.put("தமிழ்", "ta");
|
||||||
|
translationToken.put("ತುಳು", "tcy");
|
||||||
|
translationToken.put(" తెలుగు","te");
|
||||||
|
translationToken.put(" ไทย","th");
|
||||||
|
translationToken.put("Türkçe","tr");
|
||||||
|
translationToken.put("українська","uk");
|
||||||
|
translationToken.put("اردو","ur");
|
||||||
|
translationToken.put("Tiếng Việt","vi");
|
||||||
|
translationToken.put(" მარგალური", "xmf");
|
||||||
|
translationToken.put("ייִדיש","yi");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTranslationToken ( String language){
|
||||||
|
return translationToken.get(language);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,13 @@
|
||||||
package fr.free.nrw.commons;
|
package fr.free.nrw.commons;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.customtabs.CustomTabsIntent;
|
||||||
|
import android.support.v4.content.ContextCompat;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
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;
|
||||||
|
|
@ -19,6 +24,8 @@ import java.util.regex.Pattern;
|
||||||
import fr.free.nrw.commons.settings.Prefs;
|
import fr.free.nrw.commons.settings.Prefs;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
import static android.widget.Toast.LENGTH_SHORT;
|
||||||
|
|
||||||
public class Utils {
|
public class Utils {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -65,7 +72,7 @@ public class Utils {
|
||||||
/**
|
/**
|
||||||
* Capitalizes the first character of a string.
|
* Capitalizes the first character of a string.
|
||||||
*
|
*
|
||||||
* @param string
|
* @param string String to alter
|
||||||
* @return string with capitalized first character
|
* @return string with capitalized first character
|
||||||
*/
|
*/
|
||||||
public static String capitalize(String string) {
|
public static String capitalize(String string) {
|
||||||
|
|
@ -159,4 +166,32 @@ public class Utils {
|
||||||
|
|
||||||
return stringBuilder.toString();
|
return stringBuilder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void rateApp(Context context) {
|
||||||
|
final String appPackageName = BuildConfig.class.getPackage().getName();
|
||||||
|
try {
|
||||||
|
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + appPackageName)));
|
||||||
|
}
|
||||||
|
catch (android.content.ActivityNotFoundException anfe) {
|
||||||
|
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=" + appPackageName)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void handleWebUrl(Context context,Uri url){
|
||||||
|
Intent browserIntent = new Intent(Intent.ACTION_VIEW, url);
|
||||||
|
if (browserIntent.resolveActivity(context.getPackageManager()) == null) {
|
||||||
|
Toast toast = Toast.makeText(context, context.getString(R.string.no_web_browser), LENGTH_SHORT);
|
||||||
|
toast.show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
|
||||||
|
builder.setToolbarColor(ContextCompat.getColor(context, R.color.primaryColor));
|
||||||
|
builder.setSecondaryToolbarColor(ContextCompat.getColor(context, R.color.primaryDarkColor));
|
||||||
|
builder.setExitAnimations(context, android.R.anim.slide_in_left, android.R.anim.slide_out_right);
|
||||||
|
CustomTabsIntent customTabsIntent = builder.build();
|
||||||
|
customTabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
customTabsIntent.launchUrl(context, url);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import android.support.v4.view.PagerAdapter;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
import butterknife.OnClick;
|
import butterknife.OnClick;
|
||||||
|
|
@ -54,10 +55,18 @@ public class WelcomePagerAdapter extends PagerAdapter {
|
||||||
public Object instantiateItem(ViewGroup container, int position) {
|
public Object instantiateItem(ViewGroup container, int position) {
|
||||||
LayoutInflater inflater = LayoutInflater.from(container.getContext());
|
LayoutInflater inflater = LayoutInflater.from(container.getContext());
|
||||||
ViewGroup layout = (ViewGroup) inflater.inflate(PAGE_LAYOUTS[position], container, false);
|
ViewGroup layout = (ViewGroup) inflater.inflate(PAGE_LAYOUTS[position], container, false);
|
||||||
|
if( BuildConfig.FLAVOR == "beta"){
|
||||||
if (position == PAGE_FINAL) {
|
TextView textView = (TextView) layout.findViewById(R.id.welcomeYesButton);
|
||||||
|
if( textView.getVisibility() != View.VISIBLE){
|
||||||
|
textView.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
ViewHolder holder = new ViewHolder(layout);
|
ViewHolder holder = new ViewHolder(layout);
|
||||||
layout.setTag(holder);
|
layout.setTag(holder);
|
||||||
|
} else {
|
||||||
|
if (position == PAGE_FINAL) {
|
||||||
|
ViewHolder holder = new ViewHolder(layout);
|
||||||
|
layout.setTag(holder);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
container.addView(layout);
|
container.addView(layout);
|
||||||
return layout;
|
return layout;
|
||||||
|
|
@ -92,5 +101,6 @@ public class WelcomePagerAdapter extends PagerAdapter {
|
||||||
callback.onYesClicked();
|
callback.onYesClicked();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,17 +8,19 @@ import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
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.ERROR_CODE_REMOTE_EXCEPTION;
|
||||||
import static android.accounts.AccountManager.KEY_ACCOUNT_NAME;
|
import static android.accounts.AccountManager.KEY_ACCOUNT_NAME;
|
||||||
import static android.accounts.AccountManager.KEY_ACCOUNT_TYPE;
|
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 final String ACCOUNT_TYPE = "fr.free.nrw.commons";
|
public static final String ACCOUNT_TYPE = "fr.free.nrw.commons";
|
||||||
|
public static final String AUTH_COOKIE = "authCookie";
|
||||||
|
public static final String AUTH_TOKEN_TYPE = "CommonsAndroid";
|
||||||
private final Context context;
|
private final Context context;
|
||||||
|
|
||||||
public AccountUtil(Context context) {
|
public AccountUtil(Context context) {
|
||||||
|
|
@ -51,8 +53,8 @@ public class AccountUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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!
|
||||||
}
|
}
|
||||||
|
|
||||||
private AccountManager accountManager() {
|
private AccountManager accountManager() {
|
||||||
|
|
|
||||||
|
|
@ -1,71 +1,29 @@
|
||||||
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 javax.inject.Inject;
|
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 io.reactivex.schedulers.Schedulers;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
import static android.accounts.AccountManager.KEY_ACCOUNT_NAME;
|
import static fr.free.nrw.commons.auth.AccountUtil.AUTH_COOKIE;
|
||||||
import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE;
|
|
||||||
|
|
||||||
public abstract class AuthenticatedActivity extends NavigationBaseActivity {
|
public abstract class AuthenticatedActivity extends NavigationBaseActivity {
|
||||||
|
|
||||||
@Inject SessionManager sessionManager;
|
@Inject SessionManager sessionManager;
|
||||||
|
@Inject
|
||||||
|
MediaWikiApi mediaWikiApi;
|
||||||
private String authCookie;
|
private String authCookie;
|
||||||
|
|
||||||
|
|
||||||
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(ACCOUNT_TYPE, null, null,
|
|
||||||
null, AuthenticatedActivity.this, null, null))
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.map(AccountManagerFuture::getResult)
|
|
||||||
.doOnEvent((bundle, throwable) -> {
|
|
||||||
if (!bundle.containsKey(KEY_ACCOUNT_NAME)) {
|
|
||||||
throw new RuntimeException("Bundle doesn't contain account-name key: "
|
|
||||||
+ KEY_ACCOUNT_NAME);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map(bundle -> bundle.getString(KEY_ACCOUNT_NAME))
|
|
||||||
.doOnError(Timber::e)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(s -> {
|
|
||||||
Account[] allAccounts = accountManager.getAccountsByType(ACCOUNT_TYPE);
|
|
||||||
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 = sessionManager.getCurrentAccount();
|
if (authCookie != null) {
|
||||||
if (curAccount == null) {
|
onAuthCookieAcquired(authCookie);
|
||||||
addAccount(accountManager);
|
|
||||||
} else {
|
|
||||||
getAuthCookie(curAccount, accountManager);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,14 +32,14 @@ public abstract class AuthenticatedActivity extends NavigationBaseActivity {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
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,44 +1,63 @@
|
||||||
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.Activity;
|
||||||
import android.app.ProgressDialog;
|
import android.app.ProgressDialog;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
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;
|
||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
import android.text.TextWatcher;
|
import android.text.TextWatcher;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.view.inputmethod.InputMethodManager;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
|
|
||||||
import butterknife.BindView;
|
import butterknife.BindView;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
import dagger.android.AndroidInjection;
|
import butterknife.OnClick;
|
||||||
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.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.mwapi.MediaWikiApi;
|
||||||
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
||||||
|
import fr.free.nrw.commons.ui.widget.HtmlTextView;
|
||||||
|
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 {
|
||||||
|
|
||||||
|
|
@ -57,31 +76,77 @@ 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.login_credentials) TextView loginCredentials;
|
||||||
|
@BindView(R.id.two_factor_container) TextInputLayout twoFactorContainer;
|
||||||
|
@BindView(R.id.forgotPassword) HtmlTextView forgotPasswordText;
|
||||||
|
|
||||||
ProgressDialog progressDialog;
|
ProgressDialog progressDialog;
|
||||||
private AppCompatDelegate delegate;
|
private AppCompatDelegate delegate;
|
||||||
private LoginTextWatcher textWatcher = new LoginTextWatcher();
|
private LoginTextWatcher textWatcher = new LoginTextWatcher();
|
||||||
|
|
||||||
|
private Boolean loginCurrentlyInProgress = false;
|
||||||
|
private static final String LOGING_IN = "logingIn";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
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);
|
||||||
AndroidInjection.inject(this);
|
|
||||||
super.onCreate(savedInstanceState);
|
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);
|
||||||
|
|
||||||
usernameEdit.addTextChangedListener(textWatcher);
|
usernameEdit.addTextChangedListener(textWatcher);
|
||||||
|
usernameEdit.setOnFocusChangeListener((v, hasFocus) -> {
|
||||||
|
if (!hasFocus) {
|
||||||
|
hideKeyboard(v);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
passwordEdit.addTextChangedListener(textWatcher);
|
passwordEdit.addTextChangedListener(textWatcher);
|
||||||
|
passwordEdit.setOnFocusChangeListener((v, hasFocus) -> {
|
||||||
|
if (!hasFocus) {
|
||||||
|
hideKeyboard(v);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
twoFactorEdit.addTextChangedListener(textWatcher);
|
twoFactorEdit.addTextChangedListener(textWatcher);
|
||||||
passwordEdit.setOnEditorActionListener(newLoginInputActionListener());
|
passwordEdit.setOnEditorActionListener(newLoginInputActionListener());
|
||||||
|
|
||||||
loginButton.setOnClickListener(view -> performLogin());
|
loginButton.setOnClickListener(view -> performLogin());
|
||||||
signupButton.setOnClickListener(view -> signUp());
|
signupButton.setOnClickListener(view -> signUp());
|
||||||
|
|
||||||
|
forgotPasswordText.setOnClickListener(view -> forgotPassword());
|
||||||
|
|
||||||
|
if(BuildConfig.FLAVOR == "beta"){
|
||||||
|
loginCredentials.setText(getString(R.string.login_credential));
|
||||||
|
} else {
|
||||||
|
loginCredentials.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void forgotPassword() {
|
||||||
|
Utils.handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL));
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnClick(R.id.about_privacy_policy)
|
||||||
|
void onPrivacyPolicyClicked() {
|
||||||
|
Utils.handleWebUrl(this,Uri.parse("https://github.com/commons-app/apps-android-commons/wiki/Privacy-policy\\"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void hideKeyboard(View view) {
|
||||||
|
InputMethodManager inputMethodManager =(InputMethodManager)this.getSystemService(Activity.INPUT_METHOD_SERVICE);
|
||||||
|
inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPostCreate(Bundle savedInstanceState) {
|
protected void onPostCreate(Bundle savedInstanceState) {
|
||||||
super.onPostCreate(savedInstanceState);
|
super.onPostCreate(savedInstanceState);
|
||||||
|
|
@ -117,14 +182,111 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
private LoginTask getLoginTask() {
|
private void performLogin() {
|
||||||
return new LoginTask(
|
loginCurrentlyInProgress = true;
|
||||||
this,
|
Timber.d("Login to start!");
|
||||||
canonicializeUsername(usernameEdit.getText().toString()),
|
final String username = canonicializeUsername(usernameEdit.getText().toString());
|
||||||
passwordEdit.getText().toString(),
|
final String password = passwordEdit.getText().toString();
|
||||||
twoFactorEdit.getText().toString(),
|
String twoFactorCode = twoFactorEdit.getText().toString();
|
||||||
accountUtil, mwApi, defaultPrefs
|
|
||||||
);
|
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 {
|
||||||
|
loginCurrentlyInProgress = false;
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -175,15 +337,28 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
return getDelegate().getMenuInflater();
|
return getDelegate().getMenuInflater();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void askUserForTwoFactorAuth() {
|
@Override
|
||||||
if (BuildConfig.DEBUG) {
|
protected void onSaveInstanceState(Bundle outState) {
|
||||||
twoFactorEdit.setVisibility(View.VISIBLE);
|
super.onSaveInstanceState(outState);
|
||||||
showMessageAndCancelDialog(R.string.login_failed_2fa_needed);
|
outState.putBoolean(LOGING_IN, loginCurrentlyInProgress);
|
||||||
} else {
|
}
|
||||||
showMessageAndCancelDialog(R.string.login_failed_2fa_not_supported);
|
|
||||||
|
@Override
|
||||||
|
protected void onRestoreInstanceState(Bundle savedInstanceState) {
|
||||||
|
super.onRestoreInstanceState(savedInstanceState);
|
||||||
|
loginCurrentlyInProgress = savedInstanceState.getBoolean(LOGING_IN, false);
|
||||||
|
if(loginCurrentlyInProgress){
|
||||||
|
performLogin();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void askUserForTwoFactorAuth() {
|
||||||
|
progressDialog.dismiss();
|
||||||
|
twoFactorContainer.setVisibility(VISIBLE);
|
||||||
|
twoFactorEdit.setVisibility(VISIBLE);
|
||||||
|
showMessageAndCancelDialog(R.string.login_failed_2fa_needed);
|
||||||
|
}
|
||||||
|
|
||||||
public void showMessageAndCancelDialog(@StringRes int resId) {
|
public void showMessageAndCancelDialog(@StringRes int resId) {
|
||||||
showMessage(resId, R.color.secondaryDarkColor);
|
showMessage(resId, R.color.secondaryDarkColor);
|
||||||
progressDialog.cancel();
|
progressDialog.cancel();
|
||||||
|
|
@ -204,12 +379,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);
|
||||||
|
|
@ -233,7 +402,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
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() {
|
||||||
|
|
@ -255,7 +424,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,128 +0,0 @@
|
||||||
package fr.free.nrw.commons.auth;
|
|
||||||
|
|
||||||
import android.accounts.AccountAuthenticatorResponse;
|
|
||||||
import android.app.ProgressDialog;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.os.Bundle;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
|
||||||
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> {
|
|
||||||
|
|
||||||
private LoginActivity loginActivity;
|
|
||||||
private String username;
|
|
||||||
private String password;
|
|
||||||
private String twoFactorCode = "";
|
|
||||||
private AccountUtil accountUtil;
|
|
||||||
private MediaWikiApi mwApi;
|
|
||||||
|
|
||||||
public LoginTask(LoginActivity loginActivity, String username, String password,
|
|
||||||
String twoFactorCode, AccountUtil accountUtil,
|
|
||||||
MediaWikiApi mwApi, SharedPreferences prefs) {
|
|
||||||
this.loginActivity = loginActivity;
|
|
||||||
this.username = username;
|
|
||||||
this.password = password;
|
|
||||||
this.twoFactorCode = twoFactorCode;
|
|
||||||
this.accountUtil = accountUtil;
|
|
||||||
this.mwApi = mwApi;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPreExecute() {
|
|
||||||
super.onPreExecute();
|
|
||||||
loginActivity.progressDialog = new ProgressDialog(loginActivity);
|
|
||||||
loginActivity.progressDialog.setIndeterminate(true);
|
|
||||||
loginActivity.progressDialog.setTitle(loginActivity.getString(R.string.logging_in_title));
|
|
||||||
loginActivity.progressDialog.setMessage(loginActivity.getString(R.string.logging_in_message));
|
|
||||||
loginActivity.progressDialog.setCanceledOnTouchOutside(false);
|
|
||||||
loginActivity.progressDialog.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String doInBackground(String... params) {
|
|
||||||
try {
|
|
||||||
if (twoFactorCode.isEmpty()) {
|
|
||||||
return mwApi.login(username, password);
|
|
||||||
} else {
|
|
||||||
return mwApi.login(username, password, twoFactorCode);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
// Do something better!
|
|
||||||
return "NetworkFailure";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(String result) {
|
|
||||||
super.onPostExecute(result);
|
|
||||||
Timber.d("Login done!");
|
|
||||||
|
|
||||||
if (result.equals("PASS")) {
|
|
||||||
handlePassResult();
|
|
||||||
} else {
|
|
||||||
handleOtherResults(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handlePassResult() {
|
|
||||||
loginActivity.showSuccessAndDismissDialog();
|
|
||||||
|
|
||||||
AccountAuthenticatorResponse response = null;
|
|
||||||
|
|
||||||
Bundle extras = loginActivity.getIntent().getExtras();
|
|
||||||
if (extras != null) {
|
|
||||||
Timber.d("Bundle of extras: %s", extras);
|
|
||||||
response = extras.getParcelable(KEY_ACCOUNT_AUTHENTICATOR_RESPONSE);
|
|
||||||
if (response != null) {
|
|
||||||
Bundle authResult = new Bundle();
|
|
||||||
authResult.putString(KEY_ACCOUNT_NAME, username);
|
|
||||||
authResult.putString(KEY_ACCOUNT_TYPE, ACCOUNT_TYPE);
|
|
||||||
response.onResult(authResult);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
accountUtil.createAccount(response, username, password);
|
|
||||||
loginActivity.startMainActivity();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Match known failure message codes and provide messages.
|
|
||||||
* @param result String
|
|
||||||
*/
|
|
||||||
private void handleOtherResults(String result) {
|
|
||||||
if (result.equals("NetworkFailure")) {
|
|
||||||
// Matches NetworkFailure which is created by the doInBackground method
|
|
||||||
loginActivity.showMessageAndCancelDialog(R.string.login_failed_network);
|
|
||||||
} else if (result.toLowerCase().contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) {
|
|
||||||
// Matches nosuchuser, nosuchusershort, noname
|
|
||||||
loginActivity.showMessageAndCancelDialog(R.string.login_failed_username);
|
|
||||||
loginActivity.emptySensitiveEditFields();
|
|
||||||
} else if (result.toLowerCase().contains("wrongpassword".toLowerCase())) {
|
|
||||||
// Matches wrongpassword, wrongpasswordempty
|
|
||||||
loginActivity.showMessageAndCancelDialog(R.string.login_failed_password);
|
|
||||||
loginActivity.emptySensitiveEditFields();
|
|
||||||
} else if (result.toLowerCase().contains("throttle".toLowerCase())) {
|
|
||||||
// Matches unknown throttle error codes
|
|
||||||
loginActivity.showMessageAndCancelDialog(R.string.login_failed_throttled);
|
|
||||||
} else if (result.toLowerCase().contains("userblocked".toLowerCase())) {
|
|
||||||
// Matches login-userblocked
|
|
||||||
loginActivity.showMessageAndCancelDialog(R.string.login_failed_blocked);
|
|
||||||
} else if (result.equals("2FA")) {
|
|
||||||
loginActivity.askUserForTwoFactorAuth();
|
|
||||||
} else {
|
|
||||||
// Occurs with unhandled login failure codes
|
|
||||||
Timber.d("Login failed with reason: %s", result);
|
|
||||||
loginActivity.showMessageAndCancelDialog(R.string.login_failed_generic);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,15 +2,13 @@ package fr.free.nrw.commons.auth;
|
||||||
|
|
||||||
import android.accounts.Account;
|
import android.accounts.Account;
|
||||||
import android.accounts.AccountManager;
|
import android.accounts.AccountManager;
|
||||||
import android.accounts.AuthenticatorException;
|
|
||||||
import android.accounts.OperationCanceledException;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
import io.reactivex.Completable;
|
import io.reactivex.Completable;
|
||||||
import io.reactivex.Observable;
|
import io.reactivex.Observable;
|
||||||
|
import timber.log.Timber;
|
||||||
|
|
||||||
import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE;
|
import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE;
|
||||||
|
|
||||||
|
|
@ -21,11 +19,13 @@ public class SessionManager {
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final MediaWikiApi mediaWikiApi;
|
private final MediaWikiApi mediaWikiApi;
|
||||||
private Account currentAccount; // Unlike a savings account... ;-)
|
private Account currentAccount; // Unlike a savings account... ;-)
|
||||||
|
private SharedPreferences sharedPreferences;
|
||||||
|
|
||||||
public SessionManager(Context context, MediaWikiApi mediaWikiApi) {
|
public SessionManager(Context context, MediaWikiApi mediaWikiApi, SharedPreferences sharedPreferences) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.mediaWikiApi = mediaWikiApi;
|
this.mediaWikiApi = mediaWikiApi;
|
||||||
this.currentAccount = null;
|
this.currentAccount = null;
|
||||||
|
this.sharedPreferences = sharedPreferences;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -51,14 +51,28 @@ public class SessionManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
accountManager.invalidateAuthToken(ACCOUNT_TYPE, mediaWikiApi.getAuthCookie());
|
accountManager.invalidateAuthToken(ACCOUNT_TYPE, mediaWikiApi.getAuthCookie());
|
||||||
try {
|
String authCookie = getAuthCookie();
|
||||||
String authCookie = accountManager.blockingGetAuthToken(curAccount, "", false);
|
|
||||||
mediaWikiApi.setAuthCookie(authCookie);
|
if (authCookie == null) {
|
||||||
return true;
|
|
||||||
} catch (OperationCanceledException | NullPointerException | IOException | AuthenticatorException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
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() {
|
public Completable clearAllAccounts() {
|
||||||
|
|
|
||||||
|
|
@ -5,53 +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.mwapi.MediaWikiApi;
|
|
||||||
|
|
||||||
import static android.accounts.AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION;
|
|
||||||
import static android.accounts.AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE;
|
|
||||||
import static android.accounts.AccountManager.KEY_ACCOUNT_NAME;
|
|
||||||
import static android.accounts.AccountManager.KEY_ACCOUNT_TYPE;
|
|
||||||
import static android.accounts.AccountManager.KEY_AUTHTOKEN;
|
|
||||||
import static android.accounts.AccountManager.KEY_BOOLEAN_RESULT;
|
|
||||||
import static android.accounts.AccountManager.KEY_ERROR_CODE;
|
|
||||||
import static android.accounts.AccountManager.KEY_ERROR_MESSAGE;
|
|
||||||
import static android.accounts.AccountManager.KEY_INTENT;
|
|
||||||
import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE;
|
import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE;
|
||||||
import static fr.free.nrw.commons.auth.LoginActivity.PARAM_USERNAME;
|
import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE;
|
||||||
|
|
||||||
public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
|
public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
|
||||||
|
private static final String[] SYNC_AUTHORITIES = {ContributionsContentProvider.CONTRIBUTION_AUTHORITY, ModificationsContentProvider.MODIFICATIONS_AUTHORITY};
|
||||||
|
|
||||||
|
@NonNull
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private MediaWikiApi mediaWikiApi;
|
|
||||||
|
|
||||||
WikiAccountAuthenticator(Context context, MediaWikiApi mwApi) {
|
public WikiAccountAuthenticator(@NonNull Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.mediaWikiApi = mwApi;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 ACCOUNT_TYPE.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,
|
||||||
|
|
@ -59,86 +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();
|
||||||
//TODO add 2fa support here
|
bundle.putString("test", "getAuthToken");
|
||||||
String result = mediaWikiApi.login(username, password);
|
|
||||||
if (result.equals("PASS")) {
|
|
||||||
return mediaWikiApi.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, ACCOUNT_TYPE);
|
|
||||||
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
|
||||||
|
|
@ -147,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,30 +1,26 @@
|
||||||
package fr.free.nrw.commons.auth;
|
package fr.free.nrw.commons.auth;
|
||||||
|
|
||||||
|
import android.accounts.AbstractAccountAuthenticator;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import fr.free.nrw.commons.di.CommonsDaggerService;
|
||||||
|
|
||||||
import dagger.android.DaggerService;
|
public class WikiAccountAuthenticatorService extends CommonsDaggerService {
|
||||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
|
||||||
|
|
||||||
import static android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT;
|
@Nullable
|
||||||
|
private AbstractAccountAuthenticator authenticator;
|
||||||
public class WikiAccountAuthenticatorService extends DaggerService {
|
|
||||||
|
|
||||||
@Inject MediaWikiApi mwApi;
|
|
||||||
private WikiAccountAuthenticator wikiAccountAuthenticator = null;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IBinder onBind(Intent intent) {
|
public void onCreate() {
|
||||||
if (!intent.getAction().equals(ACTION_AUTHENTICATOR_INTENT)) {
|
super.onCreate();
|
||||||
return null;
|
authenticator = new WikiAccountAuthenticator(this);
|
||||||
}
|
|
||||||
|
|
||||||
if (wikiAccountAuthenticator == null) {
|
|
||||||
wikiAccountAuthenticator = new WikiAccountAuthenticator(this, mwApi);
|
|
||||||
}
|
|
||||||
return wikiAccountAuthenticator.getIBinder();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent intent) {
|
||||||
|
return authenticator == null ? null : authenticator.getIBinder();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,23 @@
|
||||||
package fr.free.nrw.commons.category;
|
package fr.free.nrw.commons.category;
|
||||||
|
|
||||||
import android.content.ContentProviderClient;
|
|
||||||
|
import android.app.Activity;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
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;
|
||||||
|
import android.text.Editable;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
import android.text.TextWatcher;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.view.inputmethod.InputMethodManager;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
@ -34,12 +39,11 @@ import javax.inject.Named;
|
||||||
|
|
||||||
import butterknife.BindView;
|
import butterknife.BindView;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
import dagger.android.support.DaggerFragment;
|
|
||||||
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.data.CategoryDao;
|
|
||||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
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.upload.SingleUploadFragment;
|
||||||
import fr.free.nrw.commons.utils.StringSortingUtils;
|
import fr.free.nrw.commons.utils.StringSortingUtils;
|
||||||
import io.reactivex.Observable;
|
import io.reactivex.Observable;
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
|
@ -48,12 +52,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 DaggerFragment {
|
public class CategorizationFragment extends CommonsDaggerSupportFragment {
|
||||||
|
|
||||||
public static final int SEARCH_CATS_LIMIT = 25;
|
public static final int SEARCH_CATS_LIMIT = 25;
|
||||||
|
|
||||||
|
|
@ -70,12 +73,16 @@ public class CategorizationFragment extends DaggerFragment {
|
||||||
|
|
||||||
@Inject MediaWikiApi mwApi;
|
@Inject MediaWikiApi mwApi;
|
||||||
@Inject @Named("default_preferences") SharedPreferences prefs;
|
@Inject @Named("default_preferences") SharedPreferences prefs;
|
||||||
|
@Inject @Named("prefs") SharedPreferences prefsPrefs;
|
||||||
|
@Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs;
|
||||||
|
@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 TitleTextWatcher textWatcher = new TitleTextWatcher();
|
||||||
|
private boolean hasDirectCategories = false;
|
||||||
|
|
||||||
private final CategoriesAdapterFactory adapterFactory = new CategoriesAdapterFactory(item -> {
|
private final CategoriesAdapterFactory adapterFactory = new CategoriesAdapterFactory(item -> {
|
||||||
if (item.isSelected()) {
|
if (item.isSelected()) {
|
||||||
|
|
@ -106,6 +113,15 @@ public class CategorizationFragment extends DaggerFragment {
|
||||||
categoriesAdapter = adapterFactory.create(items);
|
categoriesAdapter = adapterFactory.create(items);
|
||||||
categoriesList.setAdapter(categoriesAdapter);
|
categoriesList.setAdapter(categoriesAdapter);
|
||||||
|
|
||||||
|
|
||||||
|
categoriesFilter.addTextChangedListener(textWatcher);
|
||||||
|
|
||||||
|
categoriesFilter.setOnFocusChangeListener((v, hasFocus) -> {
|
||||||
|
if (!hasFocus) {
|
||||||
|
hideKeyboard(v);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
RxTextView.textChanges(categoriesFilter)
|
RxTextView.textChanges(categoriesFilter)
|
||||||
.takeUntil(RxView.detaches(categoriesFilter))
|
.takeUntil(RxView.detaches(categoriesFilter))
|
||||||
.debounce(500, TimeUnit.MILLISECONDS)
|
.debounce(500, TimeUnit.MILLISECONDS)
|
||||||
|
|
@ -114,6 +130,18 @@ public class CategorizationFragment extends DaggerFragment {
|
||||||
return rootView;
|
return rootView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void hideKeyboard(View view) {
|
||||||
|
InputMethodManager inputMethodManager = (InputMethodManager) getActivity().getSystemService(Activity.INPUT_METHOD_SERVICE);
|
||||||
|
inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
categoriesFilter.removeTextChangedListener(textWatcher);
|
||||||
|
super.onDestroyView();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||||
menu.clear();
|
menu.clear();
|
||||||
|
|
@ -138,12 +166,6 @@ public class CategorizationFragment extends DaggerFragment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@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);
|
||||||
|
|
@ -179,7 +201,6 @@ public class CategorizationFragment extends DaggerFragment {
|
||||||
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 +226,7 @@ public class CategorizationFragment extends DaggerFragment {
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
s -> categoriesAdapter.add(s),
|
s -> categoriesAdapter.add(s),
|
||||||
Timber::e,
|
Timber::e,
|
||||||
() -> {
|
() -> {
|
||||||
categoriesAdapter.notifyDataSetChanged();
|
categoriesAdapter.notifyDataSetChanged();
|
||||||
categoriesSearchInProgress.setVisibility(View.GONE);
|
categoriesSearchInProgress.setVisibility(View.GONE);
|
||||||
|
|
@ -240,9 +261,34 @@ public class CategorizationFragment extends DaggerFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Observable<CategoryItem> defaultCategories() {
|
private Observable<CategoryItem> defaultCategories() {
|
||||||
return gpsCategories()
|
|
||||||
.concatWith(titleCategories())
|
Observable<CategoryItem> directCat = directCategories();
|
||||||
.concatWith(recentCategories());
|
if (hasDirectCategories) {
|
||||||
|
Timber.d("Image has direct Cat");
|
||||||
|
return directCat
|
||||||
|
.concatWith(gpsCategories())
|
||||||
|
.concatWith(titleCategories())
|
||||||
|
.concatWith(recentCategories());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Timber.d("Image has no direct Cat");
|
||||||
|
return gpsCategories()
|
||||||
|
.concatWith(titleCategories())
|
||||||
|
.concatWith(recentCategories());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Observable<CategoryItem> directCategories() {
|
||||||
|
String directCategory = directPrefs.getString("Category", "");
|
||||||
|
List<String> categoryList = new ArrayList<>();
|
||||||
|
Timber.d("Direct category found: " + directCategory);
|
||||||
|
|
||||||
|
if (!directCategory.equals("")) {
|
||||||
|
hasDirectCategories = true;
|
||||||
|
categoryList.add(directCategory);
|
||||||
|
Timber.d("DirectCat does not equal emptyString. Direct Cat list has " + categoryList);
|
||||||
|
}
|
||||||
|
return Observable.fromIterable(categoryList).map(name -> new CategoryItem(name, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Observable<CategoryItem> gpsCategories() {
|
private Observable<CategoryItem> gpsCategories() {
|
||||||
|
|
@ -262,7 +308,7 @@ public class CategorizationFragment extends DaggerFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Observable<CategoryItem> recentCategories() {
|
private Observable<CategoryItem> recentCategories() {
|
||||||
return Observable.fromIterable(new CategoryDao(databaseClient).recentCategories(SEARCH_CATS_LIMIT))
|
return Observable.fromIterable(categoryDao.recentCategories(SEARCH_CATS_LIMIT))
|
||||||
.map(s -> new CategoryItem(s, false));
|
.map(s -> new CategoryItem(s, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -308,12 +354,13 @@ public class CategorizationFragment extends DaggerFragment {
|
||||||
//Check if item contains a 4-digit word anywhere within the string (.* is wildcard)
|
//Check if item contains a 4-digit word anywhere within the string (.* is wildcard)
|
||||||
//And that item does not equal the current year or previous year
|
//And that item does not equal the current year or previous year
|
||||||
//And if it is an irrelevant category such as Media_needing_categories_as_of_16_June_2017(Issue #750)
|
//And if it is an irrelevant category such as Media_needing_categories_as_of_16_June_2017(Issue #750)
|
||||||
|
//Check if the year in the form of XX(X)0s is relevant, i.e. in the 2000s or 2010s as stated in Issue #1029
|
||||||
return ((item.matches(".*(19|20)\\d{2}.*") && !item.contains(yearInString) && !item.contains(prevYearInString))
|
return ((item.matches(".*(19|20)\\d{2}.*") && !item.contains(yearInString) && !item.contains(prevYearInString))
|
||||||
|| item.matches("(.*)needing(.*)") || item.matches("(.*)taken on(.*)"));
|
|| item.matches("(.*)needing(.*)") || item.matches("(.*)taken on(.*)")
|
||||||
|
|| (item.matches(".*0s.*") && !item.matches(".*(200|201)0s.*")));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateCategoryCount(CategoryItem item) {
|
private void updateCategoryCount(CategoryItem item) {
|
||||||
CategoryDao categoryDao = new CategoryDao(databaseClient);
|
|
||||||
Category category = categoryDao.find(item.getName());
|
Category category = categoryDao.find(item.getName());
|
||||||
|
|
||||||
// Newly used category...
|
// Newly used category...
|
||||||
|
|
@ -361,4 +408,21 @@ public class CategorizationFragment extends DaggerFragment {
|
||||||
.create()
|
.create()
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class TitleTextWatcher implements TextWatcher {
|
||||||
|
@Override
|
||||||
|
public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable editable) {
|
||||||
|
if (getActivity() != null) {
|
||||||
|
getActivity().invalidateOptionsMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package fr.free.nrw.commons.data;
|
package fr.free.nrw.commons.category;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -12,16 +11,16 @@ import android.text.TextUtils;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import dagger.android.AndroidInjection;
|
|
||||||
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.CategoryDao.Table.ALL_FIELDS;
|
import static fr.free.nrw.commons.category.CategoryDao.Table.ALL_FIELDS;
|
||||||
import static fr.free.nrw.commons.data.CategoryDao.Table.COLUMN_ID;
|
import static fr.free.nrw.commons.category.CategoryDao.Table.COLUMN_ID;
|
||||||
import static fr.free.nrw.commons.data.CategoryDao.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
|
||||||
|
|
@ -44,12 +43,6 @@ public class CategoryContentProvider extends ContentProvider {
|
||||||
|
|
||||||
@Inject DBOpenHelper dbOpenHelper;
|
@Inject DBOpenHelper dbOpenHelper;
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCreate() {
|
|
||||||
AndroidInjection.inject(this);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
@SuppressWarnings("ConstantConditions")
|
||||||
@Override
|
@Override
|
||||||
public Cursor query(@NonNull Uri uri, String[] projection, String selection,
|
public Cursor query(@NonNull Uri uri, String[] projection, String selection,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
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;
|
||||||
|
|
@ -12,25 +12,31 @@ import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
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 CategoryDao {
|
public class CategoryDao {
|
||||||
|
|
||||||
private final ContentProviderClient client;
|
private final Provider<ContentProviderClient> clientProvider;
|
||||||
|
|
||||||
public CategoryDao(ContentProviderClient client) {
|
@Inject
|
||||||
this.client = client;
|
public CategoryDao(@Named("category") Provider<ContentProviderClient> clientProvider) {
|
||||||
|
this.clientProvider = clientProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void save(Category category) {
|
public void save(Category category) {
|
||||||
|
ContentProviderClient db = clientProvider.get();
|
||||||
try {
|
try {
|
||||||
if (category.getContentUri() == null) {
|
if (category.getContentUri() == null) {
|
||||||
category.setContentUri(client.insert(CategoryContentProvider.BASE_URI, toContentValues(category)));
|
category.setContentUri(db.insert(CategoryContentProvider.BASE_URI, toContentValues(category)));
|
||||||
} else {
|
} else {
|
||||||
client.update(category.getContentUri(), toContentValues(category), 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,11 +46,12 @@ public class CategoryDao {
|
||||||
* @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 @Nullable
|
@Nullable
|
||||||
Category find(String name) {
|
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,
|
||||||
Table.ALL_FIELDS,
|
Table.ALL_FIELDS,
|
||||||
Table.COLUMN_NAME + "=?",
|
Table.COLUMN_NAME + "=?",
|
||||||
|
|
@ -60,6 +67,7 @@ public class CategoryDao {
|
||||||
if (cursor != null) {
|
if (cursor != null) {
|
||||||
cursor.close();
|
cursor.close();
|
||||||
}
|
}
|
||||||
|
db.release();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -69,12 +77,13 @@ public class CategoryDao {
|
||||||
*
|
*
|
||||||
* @return a list containing recent categories
|
* @return a list containing recent categories
|
||||||
*/
|
*/
|
||||||
public @NonNull
|
@NonNull
|
||||||
List<String> recentCategories(int limit) {
|
List<String> recentCategories(int limit) {
|
||||||
List<String> items = new ArrayList<>();
|
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,
|
||||||
Table.ALL_FIELDS,
|
Table.ALL_FIELDS,
|
||||||
null,
|
null,
|
||||||
|
|
@ -91,6 +100,7 @@ public class CategoryDao {
|
||||||
if (cursor != null) {
|
if (cursor != null) {
|
||||||
cursor.close();
|
cursor.close();
|
||||||
}
|
}
|
||||||
|
db.release();
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
@ -98,10 +108,10 @@ public class CategoryDao {
|
||||||
Category fromCursor(Cursor cursor) {
|
Category fromCursor(Cursor cursor) {
|
||||||
// Hardcoding column positions!
|
// Hardcoding column positions!
|
||||||
return new Category(
|
return new Category(
|
||||||
CategoryContentProvider.uriForId(cursor.getInt(0)),
|
CategoryContentProvider.uriForId(cursor.getInt(cursor.getColumnIndex(Table.COLUMN_ID))),
|
||||||
cursor.getString(1),
|
cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME)),
|
||||||
new Date(cursor.getLong(2)),
|
new Date(cursor.getLong(cursor.getColumnIndex(Table.COLUMN_LAST_USED))),
|
||||||
cursor.getInt(3)
|
cursor.getInt(cursor.getColumnIndex(Table.COLUMN_TIMES_USED))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,7 +157,7 @@ public class CategoryDao {
|
||||||
onCreate(db);
|
onCreate(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void onUpdate(SQLiteDatabase db, int from, int to) {
|
public static void onUpdate(SQLiteDatabase db, int from, int to) {
|
||||||
if (from == to) {
|
if (from == to) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -197,6 +197,10 @@ public class Contribution extends Media {
|
||||||
this.localUri = localUri;
|
this.localUri = localUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setDecimalCoords(String decimalCoords) {
|
||||||
|
this.decimalCoords = decimalCoords;
|
||||||
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private String licenseTemplateFor(String license) {
|
private String licenseTemplateFor(String license) {
|
||||||
switch (license) {
|
switch (license) {
|
||||||
|
|
@ -215,6 +219,7 @@ public class Contribution extends Media {
|
||||||
case Prefs.Licenses.CC_BY_SA:
|
case Prefs.Licenses.CC_BY_SA:
|
||||||
return "{{self|cc-by-sa-3.0}}";
|
return "{{self|cc-by-sa-3.0}}";
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new RuntimeException("Unrecognized license value: " + license);
|
throw new RuntimeException("Unrecognized license value: " + license);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,14 +25,14 @@ import static fr.free.nrw.commons.contributions.Contribution.SOURCE_CAMERA;
|
||||||
import static fr.free.nrw.commons.contributions.Contribution.SOURCE_GALLERY;
|
import static fr.free.nrw.commons.contributions.Contribution.SOURCE_GALLERY;
|
||||||
import static fr.free.nrw.commons.upload.UploadService.EXTRA_SOURCE;
|
import static fr.free.nrw.commons.upload.UploadService.EXTRA_SOURCE;
|
||||||
|
|
||||||
class ContributionController {
|
public class ContributionController {
|
||||||
|
|
||||||
private static final int SELECT_FROM_GALLERY = 1;
|
private static final int SELECT_FROM_GALLERY = 1;
|
||||||
private static final int SELECT_FROM_CAMERA = 2;
|
private static final int SELECT_FROM_CAMERA = 2;
|
||||||
|
|
||||||
private Fragment fragment;
|
private Fragment fragment;
|
||||||
|
|
||||||
ContributionController(Fragment fragment) {
|
public ContributionController(Fragment fragment) {
|
||||||
this.fragment = fragment;
|
this.fragment = fragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,7 +61,7 @@ class ContributionController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void startCameraCapture() {
|
public void startCameraCapture() {
|
||||||
|
|
||||||
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||||
lastGeneratedCaptureUri = reGenerateImageCaptureUriInCache();
|
lastGeneratedCaptureUri = reGenerateImageCaptureUriInCache();
|
||||||
|
|
@ -70,6 +70,9 @@ class ContributionController {
|
||||||
requestWritePermission(fragment.getContext(), takePictureIntent, lastGeneratedCaptureUri);
|
requestWritePermission(fragment.getContext(), takePictureIntent, lastGeneratedCaptureUri);
|
||||||
|
|
||||||
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, lastGeneratedCaptureUri);
|
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, lastGeneratedCaptureUri);
|
||||||
|
if (!fragment.isAdded()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
fragment.startActivityForResult(takePictureIntent, SELECT_FROM_CAMERA);
|
fragment.startActivityForResult(takePictureIntent, SELECT_FROM_CAMERA);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,11 +80,19 @@ class ContributionController {
|
||||||
//FIXME: Starts gallery (opens Google Photos)
|
//FIXME: Starts gallery (opens Google Photos)
|
||||||
Intent pickImageIntent = new Intent(ACTION_GET_CONTENT);
|
Intent pickImageIntent = new Intent(ACTION_GET_CONTENT);
|
||||||
pickImageIntent.setType("image/*");
|
pickImageIntent.setType("image/*");
|
||||||
|
// See https://stackoverflow.com/questions/22366596/android-illegalstateexception-fragment-not-attached-to-activity-webview
|
||||||
|
if (!fragment.isAdded()) {
|
||||||
|
Timber.d("Fragment is not added, startActivityForResult cannot be called");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Timber.d("startGalleryPick() called with pickImageIntent");
|
||||||
|
|
||||||
fragment.startActivityForResult(pickImageIntent, SELECT_FROM_GALLERY);
|
fragment.startActivityForResult(pickImageIntent, SELECT_FROM_GALLERY);
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleImagePicked(int requestCode, Intent data) {
|
public void handleImagePicked(int requestCode, Intent data, boolean isDirectUpload) {
|
||||||
FragmentActivity activity = fragment.getActivity();
|
FragmentActivity activity = fragment.getActivity();
|
||||||
|
Timber.d("handleImagePicked() called with onActivityResult()");
|
||||||
Intent shareIntent = new Intent(activity, ShareActivity.class);
|
Intent shareIntent = new Intent(activity, ShareActivity.class);
|
||||||
shareIntent.setAction(ACTION_SEND);
|
shareIntent.setAction(ACTION_SEND);
|
||||||
switch (requestCode) {
|
switch (requestCode) {
|
||||||
|
|
@ -91,11 +102,23 @@ class ContributionController {
|
||||||
shareIntent.setType(activity.getContentResolver().getType(imageData));
|
shareIntent.setType(activity.getContentResolver().getType(imageData));
|
||||||
shareIntent.putExtra(EXTRA_STREAM, imageData);
|
shareIntent.putExtra(EXTRA_STREAM, imageData);
|
||||||
shareIntent.putExtra(EXTRA_SOURCE, SOURCE_GALLERY);
|
shareIntent.putExtra(EXTRA_SOURCE, SOURCE_GALLERY);
|
||||||
|
if (isDirectUpload) {
|
||||||
|
shareIntent.putExtra("isDirectUpload", true);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case SELECT_FROM_CAMERA:
|
case SELECT_FROM_CAMERA:
|
||||||
shareIntent.setType("image/jpeg"); //FIXME: Find out appropriate mime type
|
//FIXME: Find out appropriate mime type
|
||||||
|
// AFAIK this is the right type for a JPEG image
|
||||||
|
// https://developer.android.com/training/sharing/send.html#send-binary-content
|
||||||
|
shareIntent.setType("image/jpeg");
|
||||||
shareIntent.putExtra(EXTRA_STREAM, lastGeneratedCaptureUri);
|
shareIntent.putExtra(EXTRA_STREAM, lastGeneratedCaptureUri);
|
||||||
shareIntent.putExtra(EXTRA_SOURCE, SOURCE_CAMERA);
|
shareIntent.putExtra(EXTRA_SOURCE, SOURCE_CAMERA);
|
||||||
|
if (isDirectUpload) {
|
||||||
|
shareIntent.putExtra("isDirectUpload", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Timber.i("Image selected");
|
Timber.i("Image selected");
|
||||||
|
|
@ -117,5 +140,4 @@ class ContributionController {
|
||||||
lastGeneratedCaptureUri = savedInstanceState.getParcelable("lastGeneratedCaptureURI");
|
lastGeneratedCaptureUri = savedInstanceState.getParcelable("lastGeneratedCaptureURI");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,47 +8,85 @@ import android.net.Uri;
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
import javax.inject.Provider;
|
||||||
|
|
||||||
import fr.free.nrw.commons.settings.Prefs;
|
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.BASE_URI;
|
||||||
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.uriForId;
|
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.uriForId;
|
||||||
|
|
||||||
public class ContributionDao {
|
public class ContributionDao {
|
||||||
private final ContentProviderClient client;
|
/*
|
||||||
|
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)
|
||||||
|
|
||||||
public ContributionDao(ContentProviderClient client) {
|
This is why Contribution.STATE_COMPLETED is -1.
|
||||||
this.client = client;
|
*/
|
||||||
|
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) {
|
public void save(Contribution contribution) {
|
||||||
|
ContentProviderClient db = clientProvider.get();
|
||||||
try {
|
try {
|
||||||
if (contribution.getContentUri() == null) {
|
if (contribution.getContentUri() == null) {
|
||||||
contribution.setContentUri(client.insert(BASE_URI, toContentValues(contribution)));
|
contribution.setContentUri(db.insert(BASE_URI, toContentValues(contribution)));
|
||||||
} else {
|
} else {
|
||||||
client.update(contribution.getContentUri(), toContentValues(contribution), null, null);
|
db.update(contribution.getContentUri(), toContentValues(contribution), null, null);
|
||||||
}
|
}
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
|
} finally {
|
||||||
|
db.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void delete(Contribution contribution) {
|
public void delete(Contribution contribution) {
|
||||||
|
ContentProviderClient db = clientProvider.get();
|
||||||
try {
|
try {
|
||||||
if (contribution.getContentUri() == null) {
|
if (contribution.getContentUri() == null) {
|
||||||
// noooo
|
// noooo
|
||||||
throw new RuntimeException("tried to delete item with no content URI");
|
throw new RuntimeException("tried to delete item with no content URI");
|
||||||
} else {
|
} else {
|
||||||
client.delete(contribution.getContentUri(), null, null);
|
db.delete(contribution.getContentUri(), null, null);
|
||||||
}
|
}
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
|
} finally {
|
||||||
|
db.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ContentValues toContentValues(Contribution contribution) {
|
ContentValues toContentValues(Contribution contribution) {
|
||||||
ContentValues cv = new ContentValues();
|
ContentValues cv = new ContentValues();
|
||||||
cv.put(Table.COLUMN_FILENAME, contribution.getFilename());
|
cv.put(Table.COLUMN_FILENAME, contribution.getFilename());
|
||||||
if (contribution.getLocalUri() != null) {
|
if (contribution.getLocalUri() != null) {
|
||||||
|
|
@ -74,27 +112,34 @@ public class ContributionDao {
|
||||||
return cv;
|
return cv;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Contribution fromCursor(Cursor cursor) {
|
public Contribution fromCursor(Cursor cursor) {
|
||||||
// Hardcoding column positions!
|
// Hardcoding column positions!
|
||||||
//Check that cursor has a value to avoid CursorIndexOutOfBoundsException
|
//Check that cursor has a value to avoid CursorIndexOutOfBoundsException
|
||||||
if (cursor.getCount() > 0) {
|
if (cursor.getCount() > 0) {
|
||||||
|
int index;
|
||||||
|
if (cursor.getColumnIndex(Table.COLUMN_LICENSE) == -1){
|
||||||
|
index = 15;
|
||||||
|
} else {
|
||||||
|
index = cursor.getColumnIndex(Table.COLUMN_LICENSE);
|
||||||
|
}
|
||||||
return new Contribution(
|
return new Contribution(
|
||||||
uriForId(cursor.getInt(0)),
|
uriForId(cursor.getInt(cursor.getColumnIndex(Table.COLUMN_ID))),
|
||||||
cursor.getString(1),
|
cursor.getString(cursor.getColumnIndex(Table.COLUMN_FILENAME)),
|
||||||
parseUri(cursor.getString(2)),
|
parseUri(cursor.getString(cursor.getColumnIndex(Table.COLUMN_LOCAL_URI))),
|
||||||
cursor.getString(3),
|
cursor.getString(cursor.getColumnIndex(Table.COLUMN_IMAGE_URL)),
|
||||||
parseTimestamp(cursor.getLong(4)),
|
parseTimestamp(cursor.getLong(cursor.getColumnIndex(Table.COLUMN_TIMESTAMP))),
|
||||||
cursor.getInt(5),
|
cursor.getInt(cursor.getColumnIndex(Table.COLUMN_STATE)),
|
||||||
cursor.getLong(6),
|
cursor.getLong(cursor.getColumnIndex(Table.COLUMN_LENGTH)),
|
||||||
parseTimestamp(cursor.getLong(7)),
|
parseTimestamp(cursor.getLong(cursor.getColumnIndex(Table.COLUMN_UPLOADED))),
|
||||||
cursor.getLong(8),
|
cursor.getLong(cursor.getColumnIndex(Table.COLUMN_TRANSFERRED)),
|
||||||
cursor.getString(9),
|
cursor.getString(cursor.getColumnIndex(Table.COLUMN_SOURCE)),
|
||||||
cursor.getString(10),
|
cursor.getString(cursor.getColumnIndex(Table.COLUMN_DESCRIPTION)),
|
||||||
cursor.getString(11),
|
cursor.getString(cursor.getColumnIndex(Table.COLUMN_CREATOR)),
|
||||||
cursor.getInt(12) == 1,
|
cursor.getInt(cursor.getColumnIndex(Table.COLUMN_MULTIPLE)) == 1,
|
||||||
cursor.getInt(13),
|
cursor.getInt(cursor.getColumnIndex(Table.COLUMN_WIDTH)),
|
||||||
cursor.getInt(14),
|
cursor.getInt(cursor.getColumnIndex(Table.COLUMN_HEIGHT)),
|
||||||
cursor.getString(15));
|
cursor.getString(index)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
|
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
|
import fr.free.nrw.commons.BuildConfig;
|
||||||
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;
|
||||||
|
|
@ -43,7 +44,6 @@ 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.ContributionDao.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;
|
||||||
|
|
||||||
|
|
@ -58,6 +58,7 @@ public class ContributionsActivity
|
||||||
@Inject MediaWikiApi mediaWikiApi;
|
@Inject MediaWikiApi mediaWikiApi;
|
||||||
@Inject SessionManager sessionManager;
|
@Inject SessionManager sessionManager;
|
||||||
@Inject @Named("default_preferences") SharedPreferences prefs;
|
@Inject @Named("default_preferences") SharedPreferences prefs;
|
||||||
|
@Inject ContributionDao contributionDao;
|
||||||
|
|
||||||
private Cursor allContributions;
|
private Cursor allContributions;
|
||||||
private ContributionsListFragment contributionsList;
|
private ContributionsListFragment contributionsList;
|
||||||
|
|
@ -65,21 +66,6 @@ public class ContributionsActivity
|
||||||
private UploadService uploadService;
|
private UploadService uploadService;
|
||||||
private boolean isUploadServiceConnected;
|
private boolean isUploadServiceConnected;
|
||||||
private ArrayList<DataSetObserver> observersWaitingForLoad = new ArrayList<>();
|
private ArrayList<DataSetObserver> observersWaitingForLoad = new ArrayList<>();
|
||||||
private String CONTRIBUTION_SELECTION = "";
|
|
||||||
|
|
||||||
/*
|
|
||||||
This sorts in the following order:
|
|
||||||
Currently Uploading
|
|
||||||
Failed (Sorted in ascending order of time added - FIFO)
|
|
||||||
Queued to Upload (Sorted in ascending order of time added - FIFO)
|
|
||||||
Completed (Sorted in descending order of time added)
|
|
||||||
|
|
||||||
This is why Contribution.STATE_COMPLETED is -1.
|
|
||||||
*/
|
|
||||||
private String CONTRIBUTION_SORT = ContributionDao.Table.COLUMN_STATE + " DESC, "
|
|
||||||
+ ContributionDao.Table.COLUMN_UPLOADED + " DESC , ("
|
|
||||||
+ ContributionDao.Table.COLUMN_TIMESTAMP + " * "
|
|
||||||
+ ContributionDao.Table.COLUMN_STATE + ")";
|
|
||||||
|
|
||||||
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||||
|
|
||||||
|
|
@ -121,14 +107,13 @@ public class ContributionsActivity
|
||||||
@Override
|
@Override
|
||||||
protected void onAuthCookieAcquired(String authCookie) {
|
protected void onAuthCookieAcquired(String authCookie) {
|
||||||
// Do a sync everytime we get here!
|
// Do a sync everytime we get here!
|
||||||
requestSync(sessionManager.getCurrentAccount(), ContributionsContentProvider.AUTHORITY, new Bundle());
|
requestSync(sessionManager.getCurrentAccount(), ContributionsContentProvider.CONTRIBUTION_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);
|
||||||
}
|
}
|
||||||
|
|
@ -155,7 +140,11 @@ public class ContributionsActivity
|
||||||
requestAuthToken();
|
requestAuthToken();
|
||||||
initDrawer();
|
initDrawer();
|
||||||
setTitle(getString(R.string.title_activity_contributions));
|
setTitle(getString(R.string.title_activity_contributions));
|
||||||
setUploadCount();
|
|
||||||
|
if(!BuildConfig.FLAVOR.equalsIgnoreCase("beta")){
|
||||||
|
setUploadCount();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -186,7 +175,7 @@ public class ContributionsActivity
|
||||||
|
|
||||||
public void retryUpload(int i) {
|
public void retryUpload(int i) {
|
||||||
allContributions.moveToPosition(i);
|
allContributions.moveToPosition(i);
|
||||||
Contribution c = ContributionDao.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.toString());
|
Timber.d("Restarting for %s", c.toString());
|
||||||
|
|
@ -197,10 +186,10 @@ public class ContributionsActivity
|
||||||
|
|
||||||
public void deleteUpload(int i) {
|
public void deleteUpload(int i) {
|
||||||
allContributions.moveToPosition(i);
|
allContributions.moveToPosition(i);
|
||||||
Contribution c = ContributionDao.fromCursor(allContributions);
|
Contribution c = contributionDao.fromCursor(allContributions);
|
||||||
if (c.getState() == STATE_FAILED) {
|
if (c.getState() == STATE_FAILED) {
|
||||||
Timber.d("Deleting failed contrib %s", c.toString());
|
Timber.d("Deleting failed contrib %s", c.toString());
|
||||||
new ContributionDao(getContentResolver().acquireContentProviderClient(AUTHORITY)).delete(c);
|
contributionDao.delete(c);
|
||||||
} else {
|
} else {
|
||||||
Timber.d("Skipping deletion for non-failed contrib %s", c.toString());
|
Timber.d("Skipping deletion for non-failed contrib %s", c.toString());
|
||||||
}
|
}
|
||||||
|
|
@ -238,8 +227,8 @@ public class ContributionsActivity
|
||||||
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
|
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
|
||||||
int uploads = prefs.getInt(UPLOADS_SHOWING, 100);
|
int uploads = prefs.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
|
||||||
|
|
@ -248,7 +237,7 @@ public class ContributionsActivity
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
@ -269,7 +258,7 @@ public class ContributionsActivity
|
||||||
// not yet ready to return data
|
// not yet ready to return data
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
return ContributionDao.fromCursor((Cursor) contributionsList.getAdapter().getItem(i));
|
return contributionDao.fromCursor((Cursor) contributionsList.getAdapter().getItem(i));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -295,6 +284,12 @@ public class ContributionsActivity
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void betaSetUploadCount(int betaUploadCount){
|
||||||
|
getSupportActionBar().setSubtitle(getResources()
|
||||||
|
.getQuantityString(R.plurals.contributions_subtitle, betaUploadCount, betaUploadCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void notifyDatasetChanged() {
|
public void notifyDatasetChanged() {
|
||||||
// Do nothing for now
|
// Do nothing for now
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -12,27 +11,27 @@ import android.text.TextUtils;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import dagger.android.AndroidInjection;
|
|
||||||
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.contributions.ContributionDao.Table.ALL_FIELDS;
|
import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS;
|
||||||
import static fr.free.nrw.commons.contributions.ContributionDao.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) {
|
||||||
|
|
@ -41,12 +40,6 @@ public class ContributionsContentProvider extends ContentProvider {
|
||||||
|
|
||||||
@Inject DBOpenHelper dbOpenHelper;
|
@Inject DBOpenHelper dbOpenHelper;
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCreate() {
|
|
||||||
AndroidInjection.inject(this);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
@SuppressWarnings("ConstantConditions")
|
||||||
@Override
|
@Override
|
||||||
public Cursor query(@NonNull Uri uri, String[] projection, String selection,
|
public Cursor query(@NonNull Uri uri, String[] projection, String selection,
|
||||||
|
|
@ -93,7 +86,7 @@ public class ContributionsContentProvider extends ContentProvider {
|
||||||
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 = dbOpenHelper.getWritableDatabase();
|
SQLiteDatabase sqlDB = dbOpenHelper.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);
|
||||||
|
|
@ -165,7 +158,7 @@ public class ContributionsContentProvider extends ContentProvider {
|
||||||
*/
|
*/
|
||||||
int uriType = uriMatcher.match(uri);
|
int uriType = uriMatcher.match(uri);
|
||||||
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
SQLiteDatabase sqlDB = dbOpenHelper.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);
|
||||||
|
|
|
||||||
|
|
@ -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 = ContributionDao.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());
|
||||||
|
|
|
||||||
|
|
@ -20,13 +20,16 @@ import android.widget.ListAdapter;
|
||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
|
|
||||||
import butterknife.BindView;
|
import butterknife.BindView;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
import dagger.android.support.DaggerFragment;
|
import fr.free.nrw.commons.BuildConfig;
|
||||||
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;
|
||||||
|
|
||||||
|
|
@ -36,7 +39,7 @@ import static android.app.Activity.RESULT_OK;
|
||||||
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 DaggerFragment {
|
public class ContributionsListFragment extends CommonsDaggerSupportFragment {
|
||||||
|
|
||||||
@BindView(R.id.contributionsList)
|
@BindView(R.id.contributionsList)
|
||||||
GridView contributionsList;
|
GridView contributionsList;
|
||||||
|
|
@ -45,11 +48,16 @@ public class ContributionsListFragment extends DaggerFragment {
|
||||||
@BindView(R.id.loadingContributionsProgressBar)
|
@BindView(R.id.loadingContributionsProgressBar)
|
||||||
ProgressBar progressBar;
|
ProgressBar progressBar;
|
||||||
|
|
||||||
@Inject @Named("prefs") SharedPreferences prefs;
|
@Inject
|
||||||
@Inject @Named("default_preferences") SharedPreferences defaultPrefs;
|
@Named("prefs")
|
||||||
|
SharedPreferences prefs;
|
||||||
|
@Inject
|
||||||
|
@Named("default_preferences")
|
||||||
|
SharedPreferences defaultPrefs;
|
||||||
|
|
||||||
private ContributionController controller;
|
private ContributionController controller;
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
View v = inflater.inflate(R.layout.fragment_contributions, container, false);
|
View v = inflater.inflate(R.layout.fragment_contributions, container, false);
|
||||||
|
|
@ -81,6 +89,10 @@ public class ContributionsListFragment extends DaggerFragment {
|
||||||
|
|
||||||
public void setAdapter(ListAdapter adapter) {
|
public void setAdapter(ListAdapter adapter) {
|
||||||
this.contributionsList.setAdapter(adapter);
|
this.contributionsList.setAdapter(adapter);
|
||||||
|
|
||||||
|
if(BuildConfig.FLAVOR.equalsIgnoreCase("beta")){
|
||||||
|
((ContributionsActivity) getActivity()).betaSetUploadCount(adapter.getCount());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void changeProgressBarVisibility(boolean isVisible) {
|
public void changeProgressBarVisibility(boolean isVisible) {
|
||||||
|
|
@ -105,7 +117,7 @@ public class ContributionsListFragment extends DaggerFragment {
|
||||||
if (resultCode == RESULT_OK) {
|
if (resultCode == RESULT_OK) {
|
||||||
Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
|
Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
|
||||||
requestCode, resultCode, data);
|
requestCode, resultCode, data);
|
||||||
controller.handleImagePicked(requestCode, data);
|
controller.handleImagePicked(requestCode, data, false);
|
||||||
} else {
|
} else {
|
||||||
Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
|
Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
|
||||||
requestCode, resultCode, data);
|
requestCode, resultCode, data);
|
||||||
|
|
@ -208,7 +220,7 @@ public class ContributionsListFragment extends DaggerFragment {
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
|
||||||
@NonNull int[] grantResults) {
|
@NonNull int[] grantResults) {
|
||||||
Timber.d("onRequestPermissionsResult: req code = " + " perm = "
|
Timber.d("onRequestPermissionsResult: req code = " + " perm = "
|
||||||
+ permissions + " grant =" + grantResults);
|
+ Arrays.toString(permissions) + " grant =" + Arrays.toString(grantResults));
|
||||||
|
|
||||||
switch (requestCode) {
|
switch (requestCode) {
|
||||||
// 1 = Storage allowed when gallery selected
|
// 1 = Storage allowed when gallery selected
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,8 @@ import java.util.TimeZone;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
import fr.free.nrw.commons.Utils;
|
import fr.free.nrw.commons.Utils;
|
||||||
|
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||||
import fr.free.nrw.commons.mwapi.LogEventResult;
|
import fr.free.nrw.commons.mwapi.LogEventResult;
|
||||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
@ -81,7 +81,11 @@ 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) {
|
||||||
((CommonsApplication) getContext().getApplicationContext()).injector().inject(this);
|
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;
|
||||||
String lastModified = prefs.getString("lastSyncTimestamp", "");
|
String lastModified = prefs.getString("lastSyncTimestamp", "");
|
||||||
|
|
@ -89,6 +93,7 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||||
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 {
|
||||||
|
|
@ -121,7 +126,7 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||||
"", -1, dateUpdated, dateUpdated, user,
|
"", -1, dateUpdated, dateUpdated, user,
|
||||||
"", "");
|
"", "");
|
||||||
contrib.setState(STATE_COMPLETED);
|
contrib.setState(STATE_COMPLETED);
|
||||||
imageValues.add(ContributionDao.toContentValues(contrib));
|
imageValues.add(contributionDao.toContentValues(contrib));
|
||||||
|
|
||||||
if (imageValues.size() % COMMIT_THRESHOLD == 0) {
|
if (imageValues.size() % COMMIT_THRESHOLD == 0) {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ 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.category.CategoryDao;
|
||||||
import fr.free.nrw.commons.contributions.ContributionDao;
|
import fr.free.nrw.commons.contributions.ContributionDao;
|
||||||
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
|
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
|
||||||
|
|
||||||
|
|
|
||||||
174
app/src/main/java/fr/free/nrw/commons/delete/DeleteTask.java
Normal file
174
app/src/main/java/fr/free/nrw/commons/delete/DeleteTask.java
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
package fr.free.nrw.commons.delete;
|
||||||
|
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.Media;
|
||||||
|
import fr.free.nrw.commons.R;
|
||||||
|
import fr.free.nrw.commons.auth.SessionManager;
|
||||||
|
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||||
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
import static android.support.v4.content.ContextCompat.startActivity;
|
||||||
|
|
||||||
|
public class DeleteTask extends AsyncTask<Void, Void, Integer> {
|
||||||
|
|
||||||
|
private static final int SUCCESS = 0;
|
||||||
|
private static final int FAILED = -1;
|
||||||
|
private static final int ALREADY_DELETED = -2;
|
||||||
|
|
||||||
|
@Inject MediaWikiApi mwApi;
|
||||||
|
@Inject SessionManager sessionManager;
|
||||||
|
|
||||||
|
private Context context;
|
||||||
|
private Media media;
|
||||||
|
private String reason;
|
||||||
|
|
||||||
|
public DeleteTask (Context context, Media media, String reason){
|
||||||
|
this.context = context;
|
||||||
|
this.media = media;
|
||||||
|
this.reason = reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPreExecute(){
|
||||||
|
ApplicationlessInjection
|
||||||
|
.getInstance(context.getApplicationContext())
|
||||||
|
.getCommonsApplicationComponent()
|
||||||
|
.inject(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Integer doInBackground(Void ...voids) {
|
||||||
|
String editToken;
|
||||||
|
String authCookie;
|
||||||
|
String summary = "Nominating " + media.getFilename() +" for deletion.";
|
||||||
|
|
||||||
|
authCookie = sessionManager.getAuthCookie();
|
||||||
|
mwApi.setAuthCookie(authCookie);
|
||||||
|
|
||||||
|
try{
|
||||||
|
if (mwApi.pageExists("Commons:Deletion_requests/"+media.getFilename())){
|
||||||
|
return ALREADY_DELETED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
Timber.d(e.getMessage());
|
||||||
|
return FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
editToken = mwApi.getEditToken();
|
||||||
|
}
|
||||||
|
catch (Exception e){
|
||||||
|
Timber.d(e.getMessage());
|
||||||
|
return FAILED;
|
||||||
|
}
|
||||||
|
if (editToken.equals("+\\")) {
|
||||||
|
return FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
Calendar calendar = Calendar.getInstance();
|
||||||
|
String fileDeleteString = "{{delete|reason=" + reason +
|
||||||
|
"|subpage=" +media.getFilename() +
|
||||||
|
"|day=" + calendar.get(Calendar.DAY_OF_MONTH) +
|
||||||
|
"|month=" + calendar.getDisplayName(Calendar.MONTH,Calendar.LONG, Locale.getDefault()) +
|
||||||
|
"|year=" + calendar.get(Calendar.YEAR) +
|
||||||
|
"}}";
|
||||||
|
try{
|
||||||
|
mwApi.prependEdit(editToken,fileDeleteString+"\n",
|
||||||
|
media.getFilename(),summary);
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
Timber.d(e.getMessage());
|
||||||
|
return FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
String subpageString = "=== [[:" + media.getFilename() + "]] ===\n" +
|
||||||
|
reason +
|
||||||
|
" ~~~~";
|
||||||
|
try{
|
||||||
|
mwApi.edit(editToken,subpageString+"\n",
|
||||||
|
"Commons:Deletion_requests/"+media.getFilename(),summary);
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
Timber.d(e.getMessage());
|
||||||
|
return FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
String logPageString = "\n{{Commons:Deletion requests/" + media.getFilename() +
|
||||||
|
"}}\n";
|
||||||
|
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
|
||||||
|
String date = sdf.format(calendar.getTime());
|
||||||
|
try{
|
||||||
|
mwApi.appendEdit(editToken,logPageString+"\n",
|
||||||
|
"Commons:Deletion_requests/"+date,summary);
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
Timber.d(e.getMessage());
|
||||||
|
return FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
String userPageString = "\n{{subst:idw|" + media.getFilename() +
|
||||||
|
"}} ~~~~";
|
||||||
|
try{
|
||||||
|
mwApi.appendEdit(editToken,userPageString+"\n",
|
||||||
|
"User_Talk:"+sessionManager.getCurrentAccount().name,summary);
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
Timber.d(e.getMessage());
|
||||||
|
return FAILED;
|
||||||
|
}
|
||||||
|
return SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Integer result) {
|
||||||
|
String message = "";
|
||||||
|
String title = "";
|
||||||
|
switch (result){
|
||||||
|
case SUCCESS:
|
||||||
|
title = "Success";
|
||||||
|
message = "Successfully nominated " + media.getDisplayTitle() + " deletion.\n" +
|
||||||
|
"Check the webpage for more details";
|
||||||
|
break;
|
||||||
|
case FAILED:
|
||||||
|
title = "Failed";
|
||||||
|
message = "Could not request deletion. Something went wrong.";
|
||||||
|
break;
|
||||||
|
case ALREADY_DELETED:
|
||||||
|
title = "Already Nominated";
|
||||||
|
message = media.getDisplayTitle() + " has already been nominated for deletion.\n" +
|
||||||
|
"Check the webpage for more details";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
AlertDialog alert;
|
||||||
|
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||||
|
builder.setTitle(title);
|
||||||
|
builder.setMessage(message);
|
||||||
|
builder.setCancelable(true);
|
||||||
|
builder.setPositiveButton(
|
||||||
|
R.string.ok,
|
||||||
|
new DialogInterface.OnClickListener() {
|
||||||
|
public void onClick(DialogInterface dialog, int id) {}
|
||||||
|
});
|
||||||
|
builder.setNeutralButton(R.string.view_browser,
|
||||||
|
new DialogInterface.OnClickListener() {
|
||||||
|
public void onClick(DialogInterface dialog, int id) {
|
||||||
|
Intent browserIntent = new Intent(Intent.ACTION_VIEW, media.getFilePageTitle().getMobileUri());
|
||||||
|
startActivity(context,browserIntent,null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
alert = builder.create();
|
||||||
|
alert.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -8,8 +8,14 @@ import dagger.android.AndroidInjector;
|
||||||
import dagger.android.support.AndroidSupportInjectionModule;
|
import dagger.android.support.AndroidSupportInjectionModule;
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
import fr.free.nrw.commons.CommonsApplication;
|
||||||
import fr.free.nrw.commons.MediaWikiImageView;
|
import fr.free.nrw.commons.MediaWikiImageView;
|
||||||
|
import fr.free.nrw.commons.auth.LoginActivity;
|
||||||
|
import fr.free.nrw.commons.contributions.Contribution;
|
||||||
|
import fr.free.nrw.commons.contributions.ContributionsActivity;
|
||||||
import fr.free.nrw.commons.contributions.ContributionsSyncAdapter;
|
import fr.free.nrw.commons.contributions.ContributionsSyncAdapter;
|
||||||
|
import fr.free.nrw.commons.delete.DeleteTask;
|
||||||
import fr.free.nrw.commons.modifications.ModificationsSyncAdapter;
|
import fr.free.nrw.commons.modifications.ModificationsSyncAdapter;
|
||||||
|
import fr.free.nrw.commons.settings.SettingsFragment;
|
||||||
|
import fr.free.nrw.commons.nearby.PlaceRenderer;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Component(modules = {
|
@Component(modules = {
|
||||||
|
|
@ -21,7 +27,7 @@ import fr.free.nrw.commons.modifications.ModificationsSyncAdapter;
|
||||||
ServiceBuilderModule.class,
|
ServiceBuilderModule.class,
|
||||||
ContentProviderBuilderModule.class
|
ContentProviderBuilderModule.class
|
||||||
})
|
})
|
||||||
public interface CommonsApplicationComponent extends AndroidInjector<CommonsApplication> {
|
public interface CommonsApplicationComponent extends AndroidInjector<ApplicationlessInjection> {
|
||||||
void inject(CommonsApplication application);
|
void inject(CommonsApplication application);
|
||||||
|
|
||||||
void inject(ContributionsSyncAdapter syncAdapter);
|
void inject(ContributionsSyncAdapter syncAdapter);
|
||||||
|
|
@ -30,6 +36,17 @@ public interface CommonsApplicationComponent extends AndroidInjector<CommonsAppl
|
||||||
|
|
||||||
void inject(MediaWikiImageView mediaWikiImageView);
|
void inject(MediaWikiImageView mediaWikiImageView);
|
||||||
|
|
||||||
|
void inject(LoginActivity activity);
|
||||||
|
|
||||||
|
void inject(DeleteTask deleteTask);
|
||||||
|
|
||||||
|
void inject(SettingsFragment fragment);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void inject(ApplicationlessInjection instance);
|
||||||
|
|
||||||
|
void inject(PlaceRenderer placeRenderer);
|
||||||
|
|
||||||
@Component.Builder
|
@Component.Builder
|
||||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||||
interface Builder {
|
interface Builder {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package fr.free.nrw.commons.di;
|
package fr.free.nrw.commons.di;
|
||||||
|
|
||||||
|
import android.content.ContentProviderClient;
|
||||||
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.support.v4.util.LruCache;
|
import android.support.v4.util.LruCache;
|
||||||
|
|
@ -22,60 +24,96 @@ import fr.free.nrw.commons.nearby.NearbyPlaces;
|
||||||
import fr.free.nrw.commons.upload.UploadController;
|
import fr.free.nrw.commons.upload.UploadController;
|
||||||
|
|
||||||
import static android.content.Context.MODE_PRIVATE;
|
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
|
@Module
|
||||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||||
public class CommonsApplicationModule {
|
public class CommonsApplicationModule {
|
||||||
private CommonsApplication application;
|
public static final String CATEGORY_AUTHORITY = "fr.free.nrw.commons.categories.contentprovider";
|
||||||
|
public static final long OK_HTTP_CACHE_SIZE = 10 * 1024 * 1024;
|
||||||
|
|
||||||
public CommonsApplicationModule(CommonsApplication application) {
|
private Context applicationContext;
|
||||||
this.application = application;
|
|
||||||
|
public CommonsApplicationModule(Context applicationContext) {
|
||||||
|
this.applicationContext = applicationContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
public AccountUtil providesAccountUtil() {
|
public Context providesApplicationContext() {
|
||||||
return new AccountUtil(application);
|
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
|
@Provides
|
||||||
@Named("application_preferences")
|
@Named("application_preferences")
|
||||||
public SharedPreferences providesApplicationSharedPreferences() {
|
public SharedPreferences providesApplicationSharedPreferences(Context context) {
|
||||||
return application.getSharedPreferences("fr.free.nrw.commons", MODE_PRIVATE);
|
return context.getSharedPreferences("fr.free.nrw.commons", MODE_PRIVATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Named("default_preferences")
|
@Named("default_preferences")
|
||||||
public SharedPreferences providesDefaultSharedPreferences() {
|
public SharedPreferences providesDefaultSharedPreferences(Context context) {
|
||||||
return PreferenceManager.getDefaultSharedPreferences(application);
|
return PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Named("prefs")
|
@Named("prefs")
|
||||||
public SharedPreferences providesOtherSharedPreferences() {
|
public SharedPreferences providesOtherSharedPreferences(Context context) {
|
||||||
return application.getSharedPreferences("prefs", MODE_PRIVATE);
|
return context.getSharedPreferences("prefs", MODE_PRIVATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
public UploadController providesUploadController(SessionManager sessionManager, @Named("default_preferences") SharedPreferences sharedPreferences) {
|
@Named("direct_nearby_upload_prefs")
|
||||||
return new UploadController(sessionManager, application, sharedPreferences);
|
public SharedPreferences providesDirectNearbyUploadPreferences(Context context) {
|
||||||
|
return context.getSharedPreferences("direct_nearby_upload_prefs", MODE_PRIVATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
public UploadController providesUploadController(SessionManager sessionManager, @Named("default_preferences") SharedPreferences sharedPreferences, Context context) {
|
||||||
|
return new UploadController(sessionManager, context, sharedPreferences);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
public SessionManager providesSessionManager(MediaWikiApi mediaWikiApi) {
|
public SessionManager providesSessionManager(Context context,
|
||||||
return new SessionManager(application, mediaWikiApi);
|
MediaWikiApi mediaWikiApi,
|
||||||
|
@Named("default_preferences") SharedPreferences sharedPreferences) {
|
||||||
|
return new SessionManager(context, mediaWikiApi, sharedPreferences);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
public MediaWikiApi provideMediaWikiApi() {
|
public MediaWikiApi provideMediaWikiApi(Context context, @Named("default_preferences") SharedPreferences sharedPreferences) {
|
||||||
return new ApacheHttpClientMediaWikiApi(BuildConfig.WIKIMEDIA_API_HOST);
|
return new ApacheHttpClientMediaWikiApi(context, BuildConfig.WIKIMEDIA_API_HOST, sharedPreferences);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
public LocationServiceManager provideLocationServiceManager() {
|
public LocationServiceManager provideLocationServiceManager(Context context) {
|
||||||
return new LocationServiceManager(application);
|
return new LocationServiceManager(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
@ -86,8 +124,8 @@ public class CommonsApplicationModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
public DBOpenHelper provideDBOpenHelper() {
|
public DBOpenHelper provideDBOpenHelper(Context context) {
|
||||||
return new DBOpenHelper(application);
|
return new DBOpenHelper(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
@ -101,4 +139,4 @@ public class CommonsApplicationModule {
|
||||||
public LruCache<String, String> provideLruCache() {
|
public LruCache<String, String> provideLruCache() {
|
||||||
return new LruCache<>(1024);
|
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()));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ import fr.free.nrw.commons.featured.FeaturedImagesListFragment;
|
||||||
import fr.free.nrw.commons.media.MediaDetailFragment;
|
import fr.free.nrw.commons.media.MediaDetailFragment;
|
||||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
||||||
import fr.free.nrw.commons.nearby.NearbyListFragment;
|
import fr.free.nrw.commons.nearby.NearbyListFragment;
|
||||||
|
import fr.free.nrw.commons.nearby.NearbyMapFragment;
|
||||||
import fr.free.nrw.commons.nearby.NoPermissionsFragment;
|
import fr.free.nrw.commons.nearby.NoPermissionsFragment;
|
||||||
import fr.free.nrw.commons.settings.SettingsFragment;
|
import fr.free.nrw.commons.settings.SettingsFragment;
|
||||||
import fr.free.nrw.commons.upload.MultipleUploadListFragment;
|
import fr.free.nrw.commons.upload.MultipleUploadListFragment;
|
||||||
|
|
@ -32,6 +33,9 @@ public abstract class FragmentBuilderModule {
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract NearbyListFragment bindNearbyListFragment();
|
abstract NearbyListFragment bindNearbyListFragment();
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract NearbyMapFragment bindNearbyMapFragment();
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract NoPermissionsFragment bindNoPermissionsFragment();
|
abstract NoPermissionsFragment bindNoPermissionsFragment();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package fr.free.nrw.commons.location;
|
package fr.free.nrw.commons.location;
|
||||||
|
|
||||||
import android.location.Location;
|
import android.location.Location;
|
||||||
|
import android.net.Uri;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -11,15 +12,15 @@ public class LatLng {
|
||||||
private final double latitude;
|
private final double latitude;
|
||||||
private final double longitude;
|
private final double longitude;
|
||||||
private final float accuracy;
|
private final float accuracy;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accepts latitude and longitude.
|
* Accepts latitude and longitude.
|
||||||
* North and South values are cut off at 90°
|
* North and South values are cut off at 90°
|
||||||
*
|
*
|
||||||
* @param latitude the latitude
|
* @param latitude the latitude
|
||||||
* @param longitude the longitude
|
* @param longitude the longitude
|
||||||
* @param accuracy the accuracy
|
* @param accuracy the accuracy
|
||||||
*
|
*
|
||||||
* Examples:
|
* Examples:
|
||||||
* the Statue of Liberty is located at 40.69° N, 74.04° W
|
* the Statue of Liberty is located at 40.69° N, 74.04° W
|
||||||
* The Statue of Liberty could be constructed as LatLng(40.69, -74.04, 1.0)
|
* The Statue of Liberty could be constructed as LatLng(40.69, -74.04, 1.0)
|
||||||
|
|
@ -38,23 +39,22 @@ public class LatLng {
|
||||||
/**
|
/**
|
||||||
* gets the latitude and longitude of a given non-null location
|
* gets the latitude and longitude of a given non-null location
|
||||||
* @param location the non-null location of the user
|
* @param location the non-null location of the user
|
||||||
* @return
|
* @return LatLng the Latitude and Longitude of a given location
|
||||||
*/
|
*/
|
||||||
public static LatLng from(@NonNull Location location) {
|
public static LatLng from(@NonNull Location location) {
|
||||||
return new LatLng(location.getLatitude(), location.getLongitude(), location.getAccuracy());
|
return new LatLng(location.getLatitude(), location.getLongitude(), location.getAccuracy());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* creates a hash code for the longitude and longitude
|
* creates a hash code for the longitude and longitude
|
||||||
*/
|
*/
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
boolean var1 = true;
|
byte var1 = 1;
|
||||||
byte var2 = 1;
|
long var2 = Double.doubleToLongBits(this.latitude);
|
||||||
long var3 = Double.doubleToLongBits(this.latitude);
|
int var3 = 31 * var1 + (int)(var2 ^ var2 >>> 32);
|
||||||
int var5 = 31 * var2 + (int)(var3 ^ var3 >>> 32);
|
var2 = Double.doubleToLongBits(this.longitude);
|
||||||
var3 = Double.doubleToLongBits(this.longitude);
|
var3 = 31 * var3 + (int)(var2 ^ var2 >>> 32);
|
||||||
var5 = 31 * var5 + (int)(var3 ^ var3 >>> 32);
|
return var3;
|
||||||
return var5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -154,4 +154,9 @@ public class LatLng {
|
||||||
public double getLatitude() {
|
public double getLatitude() {
|
||||||
return latitude;
|
return latitude;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Uri getGmmIntentUri() {
|
||||||
|
return Uri.parse("geo:0,0?q=" + latitude + "," + longitude);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,19 +10,18 @@ import android.location.LocationManager;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.app.ActivityCompat;
|
import android.support.v4.app.ActivityCompat;
|
||||||
import android.support.v4.content.ContextCompat;
|
import android.support.v4.content.ContextCompat;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Singleton;
|
|
||||||
|
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
public class LocationServiceManager implements LocationListener {
|
public class LocationServiceManager implements LocationListener {
|
||||||
public static final int LOCATION_REQUEST = 1;
|
public static final int LOCATION_REQUEST = 1;
|
||||||
|
|
||||||
private static final long MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS = 2 * 60 * 1000;
|
// Maybe these values can be improved for efficiency
|
||||||
|
private static final long MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS = 2 * 60 * 100;
|
||||||
private static final long MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS = 10;
|
private static final long MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS = 10;
|
||||||
|
|
||||||
private Context context;
|
private Context context;
|
||||||
|
|
@ -33,6 +32,7 @@ public class LocationServiceManager implements LocationListener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new instance of LocationServiceManager.
|
* Constructs a new instance of LocationServiceManager.
|
||||||
|
*
|
||||||
* @param context the context
|
* @param context the context
|
||||||
*/
|
*/
|
||||||
public LocationServiceManager(Context context) {
|
public LocationServiceManager(Context context) {
|
||||||
|
|
@ -42,6 +42,7 @@ public class LocationServiceManager implements LocationListener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the current status of the GPS provider.
|
* Returns the current status of the GPS provider.
|
||||||
|
*
|
||||||
* @return true if the GPS provider is enabled
|
* @return true if the GPS provider is enabled
|
||||||
*/
|
*/
|
||||||
public boolean isProviderEnabled() {
|
public boolean isProviderEnabled() {
|
||||||
|
|
@ -50,6 +51,7 @@ public class LocationServiceManager implements LocationListener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether the location permission is granted.
|
* Returns whether the location permission is granted.
|
||||||
|
*
|
||||||
* @return true if the location permission is granted
|
* @return true if the location permission is granted
|
||||||
*/
|
*/
|
||||||
public boolean isLocationPermissionGranted() {
|
public boolean isLocationPermissionGranted() {
|
||||||
|
|
@ -59,6 +61,7 @@ public class LocationServiceManager implements LocationListener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Requests the location permission to be granted.
|
* Requests the location permission to be granted.
|
||||||
|
*
|
||||||
* @param activity the activity
|
* @param activity the activity
|
||||||
*/
|
*/
|
||||||
public void requestPermissions(Activity activity) {
|
public void requestPermissions(Activity activity) {
|
||||||
|
|
@ -71,11 +74,9 @@ public class LocationServiceManager implements LocationListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isPermissionExplanationRequired(Activity activity) {
|
public boolean isPermissionExplanationRequired(Activity activity) {
|
||||||
if (activity.isFinishing()) {
|
return !activity.isFinishing() &&
|
||||||
return false;
|
ActivityCompat.shouldShowRequestPermissionRationale(activity,
|
||||||
}
|
Manifest.permission.ACCESS_FINE_LOCATION);
|
||||||
return ActivityCompat.shouldShowRequestPermissionRationale(activity,
|
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public LatLng getLastLocation() {
|
public LatLng getLastLocation() {
|
||||||
|
|
@ -85,7 +86,8 @@ public class LocationServiceManager implements LocationListener {
|
||||||
return LatLng.from(lastLocation);
|
return LatLng.from(lastLocation);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Registers a LocationManager to listen for current location.
|
/**
|
||||||
|
* Registers a LocationManager to listen for current location.
|
||||||
*/
|
*/
|
||||||
public void registerLocationManager() {
|
public void registerLocationManager() {
|
||||||
if (!isLocationManagerRegistered)
|
if (!isLocationManagerRegistered)
|
||||||
|
|
@ -95,6 +97,7 @@ public class LocationServiceManager implements LocationListener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Requests location updates from the specified provider.
|
* Requests location updates from the specified provider.
|
||||||
|
*
|
||||||
* @param locationProvider the location provider
|
* @param locationProvider the location provider
|
||||||
* @return true if successful
|
* @return true if successful
|
||||||
*/
|
*/
|
||||||
|
|
@ -116,14 +119,17 @@ public class LocationServiceManager implements LocationListener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether a given location is better than the current best location.
|
* Returns whether a given location is better than the current best location.
|
||||||
* @param location the location to be tested
|
*
|
||||||
|
* @param location the location to be tested
|
||||||
* @param currentBestLocation the current best location
|
* @param currentBestLocation the current best location
|
||||||
* @return true if the given location is better
|
* @return LOCATION_SIGNIFICANTLY_CHANGED if location changed significantly
|
||||||
|
* LOCATION_SLIGHTLY_CHANGED if location changed slightly
|
||||||
*/
|
*/
|
||||||
protected boolean isBetterLocation(Location location, Location currentBestLocation) {
|
protected LocationChangeType 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
|
||||||
return true;
|
return LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check whether the new location fix is newer or older
|
// Check whether the new location fix is newer or older
|
||||||
|
|
@ -132,15 +138,6 @@ public class LocationServiceManager implements LocationListener {
|
||||||
boolean isSignificantlyOlder = timeDelta < -MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS;
|
boolean isSignificantlyOlder = timeDelta < -MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS;
|
||||||
boolean isNewer = timeDelta > 0;
|
boolean isNewer = timeDelta > 0;
|
||||||
|
|
||||||
// If it's been more than two minutes since the current location, use the new location
|
|
||||||
// because the user has likely moved
|
|
||||||
if (isSignificantlyNewer) {
|
|
||||||
return true;
|
|
||||||
// If the new location is more than two minutes older, it must be worse
|
|
||||||
} else if (isSignificantlyOlder) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check whether the new location fix is more or less accurate
|
// Check whether the new location fix is more or less accurate
|
||||||
int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy());
|
int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy());
|
||||||
boolean isLessAccurate = accuracyDelta > 0;
|
boolean isLessAccurate = accuracyDelta > 0;
|
||||||
|
|
@ -151,15 +148,28 @@ public class LocationServiceManager implements LocationListener {
|
||||||
boolean isFromSameProvider = isSameProvider(location.getProvider(),
|
boolean isFromSameProvider = isSameProvider(location.getProvider(),
|
||||||
currentBestLocation.getProvider());
|
currentBestLocation.getProvider());
|
||||||
|
|
||||||
// Determine location quality using a combination of timeliness and accuracy
|
float[] results = new float[5];
|
||||||
if (isMoreAccurate) {
|
Location.distanceBetween(
|
||||||
return true;
|
currentBestLocation.getLatitude(),
|
||||||
} else if (isNewer && !isLessAccurate) {
|
currentBestLocation.getLongitude(),
|
||||||
return true;
|
location.getLatitude(),
|
||||||
} else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) {
|
location.getLongitude(),
|
||||||
return true;
|
results);
|
||||||
|
|
||||||
|
// If it's been more than two minutes since the current location, use the new location
|
||||||
|
// because the user has likely moved
|
||||||
|
if (isSignificantlyNewer
|
||||||
|
|| isMoreAccurate
|
||||||
|
|| (isNewer && !isLessAccurate)
|
||||||
|
|| (isNewer && !isSignificantlyLessAccurate && isFromSameProvider)) {
|
||||||
|
if (results[0] < 1000) { // Means change is smaller than 1000 meter
|
||||||
|
return LocationChangeType.LOCATION_SLIGHTLY_CHANGED;
|
||||||
|
} else {
|
||||||
|
return LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED;
|
||||||
|
}
|
||||||
|
} else{
|
||||||
|
return LocationChangeType.LOCATION_NOT_CHANGED;
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -172,7 +182,8 @@ public class LocationServiceManager implements LocationListener {
|
||||||
return provider1.equals(provider2);
|
return provider1.equals(provider2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Unregisters location manager.
|
/**
|
||||||
|
* Unregisters location manager.
|
||||||
*/
|
*/
|
||||||
public void unregisterLocationManager() {
|
public void unregisterLocationManager() {
|
||||||
isLocationManagerRegistered = false;
|
isLocationManagerRegistered = false;
|
||||||
|
|
@ -185,6 +196,7 @@ public class LocationServiceManager implements LocationListener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a new listener to the list of location listeners.
|
* Adds a new listener to the list of location listeners.
|
||||||
|
*
|
||||||
* @param listener the new listener
|
* @param listener the new listener
|
||||||
*/
|
*/
|
||||||
public void addLocationListener(LocationUpdateListener listener) {
|
public void addLocationListener(LocationUpdateListener listener) {
|
||||||
|
|
@ -195,6 +207,7 @@ public class LocationServiceManager implements LocationListener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a listener from the list of location listeners.
|
* Removes a listener from the list of location listeners.
|
||||||
|
*
|
||||||
* @param listener the listener to be removed
|
* @param listener the listener to be removed
|
||||||
*/
|
*/
|
||||||
public void removeLocationListener(LocationUpdateListener listener) {
|
public void removeLocationListener(LocationUpdateListener listener) {
|
||||||
|
|
@ -203,12 +216,19 @@ public class LocationServiceManager implements LocationListener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLocationChanged(Location location) {
|
public void onLocationChanged(Location location) {
|
||||||
if (isBetterLocation(location, lastLocation)) {
|
if (isBetterLocation(location, lastLocation)
|
||||||
lastLocation = location;
|
.equals(LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED)) {
|
||||||
for (LocationUpdateListener listener : locationListeners) {
|
lastLocation = location;
|
||||||
listener.onLocationChanged(LatLng.from(lastLocation));
|
for (LocationUpdateListener listener : locationListeners) {
|
||||||
|
listener.onLocationChangedSignificantly(LatLng.from(lastLocation));
|
||||||
|
}
|
||||||
|
} else if (isBetterLocation(location, lastLocation)
|
||||||
|
.equals(LocationChangeType.LOCATION_SLIGHTLY_CHANGED)) {
|
||||||
|
lastLocation = location;
|
||||||
|
for (LocationUpdateListener listener : locationListeners) {
|
||||||
|
listener.onLocationChangedSlightly(LatLng.from(lastLocation));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -225,4 +245,10 @@ public class LocationServiceManager implements LocationListener {
|
||||||
public void onProviderDisabled(String provider) {
|
public void onProviderDisabled(String provider) {
|
||||||
Timber.d("Provider %s disabled", provider);
|
Timber.d("Provider %s disabled", provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum LocationChangeType{
|
||||||
|
LOCATION_SIGNIFICANTLY_CHANGED, //Went out of borders of nearby markers
|
||||||
|
LOCATION_SLIGHTLY_CHANGED, //User might be walking or driving
|
||||||
|
LOCATION_NOT_CHANGED
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package fr.free.nrw.commons.location;
|
package fr.free.nrw.commons.location;
|
||||||
|
|
||||||
public interface LocationUpdateListener {
|
public interface LocationUpdateListener {
|
||||||
void onLocationChanged(LatLng latLng);
|
void onLocationChangedSignificantly(LatLng latLng);
|
||||||
|
void onLocationChangedSlightly(LatLng latLng);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,27 @@
|
||||||
package fr.free.nrw.commons.media;
|
package fr.free.nrw.commons.media;
|
||||||
|
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.content.DialogInterface;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.database.DataSetObserver;
|
import android.database.DataSetObserver;
|
||||||
import android.net.Uri;
|
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.text.Editable;
|
||||||
|
import android.text.TextWatcher;
|
||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.ViewTreeObserver;
|
import android.view.ViewTreeObserver;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.EditText;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.ScrollView;
|
import android.widget.ScrollView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
|
|
@ -24,7 +32,6 @@ import java.util.Locale;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Provider;
|
import javax.inject.Provider;
|
||||||
|
|
||||||
import dagger.android.support.DaggerFragment;
|
|
||||||
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;
|
||||||
|
|
@ -32,12 +39,15 @@ 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.delete.DeleteTask;
|
||||||
|
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.mwapi.MediaWikiApi;
|
|
||||||
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 DaggerFragment {
|
import static android.widget.Toast.LENGTH_SHORT;
|
||||||
|
|
||||||
|
public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
||||||
|
|
||||||
private boolean editable;
|
private boolean editable;
|
||||||
private boolean isFeaturedMedia;
|
private boolean isFeaturedMedia;
|
||||||
|
|
@ -74,6 +84,7 @@ public class MediaDetailFragment extends DaggerFragment {
|
||||||
private TextView uploadedDate;
|
private TextView uploadedDate;
|
||||||
private LinearLayout categoryContainer;
|
private LinearLayout categoryContainer;
|
||||||
private LinearLayout authorLayout;
|
private LinearLayout authorLayout;
|
||||||
|
private Button delete;
|
||||||
private ScrollView scrollView;
|
private ScrollView scrollView;
|
||||||
private ArrayList<String> categoryNames;
|
private ArrayList<String> categoryNames;
|
||||||
private boolean categoriesLoaded = false;
|
private boolean categoriesLoaded = false;
|
||||||
|
|
@ -81,7 +92,7 @@ public class MediaDetailFragment extends DaggerFragment {
|
||||||
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;
|
||||||
private DataSetObserver dataObserver;
|
private DataSetObserver dataObserver;
|
||||||
private AsyncTask<Void,Void,Boolean> detailFetchTask;
|
private AsyncTask<Void, Void, Boolean> detailFetchTask;
|
||||||
private LicenseList licenseList;
|
private LicenseList licenseList;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -101,7 +112,7 @@ public class MediaDetailFragment extends DaggerFragment {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
detailProvider = (MediaDetailPagerFragment.MediaDetailProvider)getActivity();
|
detailProvider = (MediaDetailPagerFragment.MediaDetailProvider) getActivity();
|
||||||
|
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
editable = savedInstanceState.getBoolean("editable");
|
editable = savedInstanceState.getBoolean("editable");
|
||||||
|
|
@ -131,6 +142,7 @@ public class MediaDetailFragment extends DaggerFragment {
|
||||||
license = (TextView) view.findViewById(R.id.mediaDetailLicense);
|
license = (TextView) view.findViewById(R.id.mediaDetailLicense);
|
||||||
coordinates = (TextView) view.findViewById(R.id.mediaDetailCoordinates);
|
coordinates = (TextView) view.findViewById(R.id.mediaDetailCoordinates);
|
||||||
uploadedDate = (TextView) view.findViewById(R.id.mediaDetailuploadeddate);
|
uploadedDate = (TextView) view.findViewById(R.id.mediaDetailuploadeddate);
|
||||||
|
delete = (Button) view.findViewById(R.id.nominateDeletion);
|
||||||
categoryContainer = (LinearLayout) view.findViewById(R.id.mediaDetailCategoryContainer);
|
categoryContainer = (LinearLayout) view.findViewById(R.id.mediaDetailCategoryContainer);
|
||||||
authorLayout = (LinearLayout) view.findViewById(R.id.authorLinearLayout);
|
authorLayout = (LinearLayout) view.findViewById(R.id.authorLinearLayout);
|
||||||
|
|
||||||
|
|
@ -173,7 +185,8 @@ public class MediaDetailFragment extends DaggerFragment {
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public void onResume() {
|
@Override
|
||||||
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
Media media = detailProvider.getMediaAtPosition(index);
|
Media media = detailProvider.getMediaAtPosition(index);
|
||||||
if (media == null) {
|
if (media == null) {
|
||||||
|
|
@ -255,13 +268,13 @@ public class MediaDetailFragment extends DaggerFragment {
|
||||||
detailFetchTask.cancel(true);
|
detailFetchTask.cancel(true);
|
||||||
detailFetchTask = null;
|
detailFetchTask = null;
|
||||||
}
|
}
|
||||||
if (layoutListener != null) {
|
if (layoutListener != null && getView() != null) {
|
||||||
getView().getViewTreeObserver().removeGlobalOnLayoutListener(layoutListener); // old Android was on crack. CRACK IS WHACK
|
getView().getViewTreeObserver().removeGlobalOnLayoutListener(layoutListener); // old Android was on crack. CRACK IS WHACK
|
||||||
layoutListener = null;
|
layoutListener = null;
|
||||||
}
|
}
|
||||||
if (scrollListener != null) {
|
if (scrollListener != null && getView() != null) {
|
||||||
getView().getViewTreeObserver().removeOnScrollChangedListener(scrollListener);
|
getView().getViewTreeObserver().removeOnScrollChangedListener(scrollListener);
|
||||||
scrollListener = null;
|
scrollListener = null;
|
||||||
}
|
}
|
||||||
if (dataObserver != null) {
|
if (dataObserver != null) {
|
||||||
detailProvider.unregisterDataSetObserver(dataObserver);
|
detailProvider.unregisterDataSetObserver(dataObserver);
|
||||||
|
|
@ -286,13 +299,66 @@ public class MediaDetailFragment extends DaggerFragment {
|
||||||
categoryNames.add(getString(R.string.detail_panel_cats_none));
|
categoryNames.add(getString(R.string.detail_panel_cats_none));
|
||||||
}
|
}
|
||||||
rebuildCatList();
|
rebuildCatList();
|
||||||
|
|
||||||
|
delete.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setOnClickListeners(final Media media) {
|
private void setOnClickListeners(final Media media) {
|
||||||
license.setOnClickListener(v -> openWebBrowser(licenseLink(media)));
|
if (licenseLink(media) != null) {
|
||||||
|
license.setOnClickListener(v -> openWebBrowser(licenseLink(media)));
|
||||||
|
} else {
|
||||||
|
Toast toast = Toast.makeText(getContext(), getString(R.string.null_url), Toast.LENGTH_SHORT);
|
||||||
|
toast.show();
|
||||||
|
}
|
||||||
if (media.getCoordinates() != null) {
|
if (media.getCoordinates() != null) {
|
||||||
coordinates.setOnClickListener(v -> openMap(media.getCoordinates()));
|
coordinates.setOnClickListener(v -> openMap(media.getCoordinates()));
|
||||||
}
|
}
|
||||||
|
if (delete.getVisibility()==View.VISIBLE){
|
||||||
|
delete.setOnClickListener(v -> {
|
||||||
|
AlertDialog.Builder alert = new AlertDialog.Builder(getActivity());
|
||||||
|
alert.setMessage("Why should this file be deleted?");
|
||||||
|
final EditText input = new EditText(getActivity());
|
||||||
|
alert.setView(input);
|
||||||
|
input.requestFocus();
|
||||||
|
alert.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
|
||||||
|
public void onClick(DialogInterface dialog, int whichButton) {
|
||||||
|
String reason = input.getText().toString();
|
||||||
|
DeleteTask deleteTask = new DeleteTask(getActivity(), media, reason);
|
||||||
|
deleteTask.execute();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
alert.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
|
||||||
|
public void onClick(DialogInterface dialog, int whichButton) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
AlertDialog d = alert.create();
|
||||||
|
input.addTextChangedListener(new TextWatcher() {
|
||||||
|
private void handleText() {
|
||||||
|
final Button okButton = d.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||||
|
if (input.getText().length() == 0) {
|
||||||
|
okButton.setEnabled(false);
|
||||||
|
} else {
|
||||||
|
okButton.setEnabled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable arg0) {
|
||||||
|
handleText();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
d.show();
|
||||||
|
d.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void rebuildCatList() {
|
private void rebuildCatList() {
|
||||||
|
|
@ -306,7 +372,7 @@ public class MediaDetailFragment extends DaggerFragment {
|
||||||
|
|
||||||
private View buildCatLabel(final String catName, ViewGroup categoryContainer) {
|
private View buildCatLabel(final String catName, ViewGroup categoryContainer) {
|
||||||
final View item = LayoutInflater.from(getContext()).inflate(R.layout.detail_category_item, categoryContainer, false);
|
final View item = LayoutInflater.from(getContext()).inflate(R.layout.detail_category_item, categoryContainer, false);
|
||||||
final CompatTextView textView = (CompatTextView)item.findViewById(R.id.mediaDetailCategoryItemText);
|
final CompatTextView textView = (CompatTextView) item.findViewById(R.id.mediaDetailCategoryItemText);
|
||||||
|
|
||||||
textView.setText(catName);
|
textView.setText(catName);
|
||||||
if (categoriesLoaded && categoriesPresent) {
|
if (categoriesLoaded && categoriesPresent) {
|
||||||
|
|
@ -315,7 +381,13 @@ public class MediaDetailFragment extends DaggerFragment {
|
||||||
Intent viewIntent = new Intent();
|
Intent viewIntent = new Intent();
|
||||||
viewIntent.setAction(Intent.ACTION_VIEW);
|
viewIntent.setAction(Intent.ACTION_VIEW);
|
||||||
viewIntent.setData(new PageTitle(selectedCategoryTitle).getCanonicalUri());
|
viewIntent.setData(new PageTitle(selectedCategoryTitle).getCanonicalUri());
|
||||||
startActivity(viewIntent);
|
//check if web browser available
|
||||||
|
if(viewIntent.resolveActivity(getActivity().getPackageManager()) != null){
|
||||||
|
startActivity(viewIntent);
|
||||||
|
} else {
|
||||||
|
Toast toast = Toast.makeText(getContext(), getString(R.string.no_web_browser), LENGTH_SHORT);
|
||||||
|
toast.show();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return item;
|
return item;
|
||||||
|
|
@ -325,7 +397,7 @@ public class MediaDetailFragment extends DaggerFragment {
|
||||||
// You must face the darkness alone
|
// You must face the darkness alone
|
||||||
int scrollY = scrollView.getScrollY();
|
int scrollY = scrollView.getScrollY();
|
||||||
int scrollMax = getView().getHeight();
|
int scrollMax = getView().getHeight();
|
||||||
float scrollPercentage = (float)scrollY / (float)scrollMax;
|
float scrollPercentage = (float) scrollY / (float) scrollMax;
|
||||||
final float transparencyMax = 0.75f;
|
final float transparencyMax = 0.75f;
|
||||||
if (scrollPercentage > transparencyMax) {
|
if (scrollPercentage > transparencyMax) {
|
||||||
scrollPercentage = transparencyMax;
|
scrollPercentage = transparencyMax;
|
||||||
|
|
@ -379,7 +451,8 @@ public class MediaDetailFragment extends DaggerFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private @Nullable String licenseLink(Media media) {
|
private @Nullable
|
||||||
|
String licenseLink(Media media) {
|
||||||
String licenseKey = media.getLicense();
|
String licenseKey = media.getLicense();
|
||||||
if (licenseKey == null || licenseKey.equals("")) {
|
if (licenseKey == null || licenseKey.equals("")) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -394,7 +467,14 @@ public class MediaDetailFragment extends DaggerFragment {
|
||||||
|
|
||||||
private void openWebBrowser(String url) {
|
private void openWebBrowser(String url) {
|
||||||
Intent browser = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
Intent browser = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||||
startActivity(browser);
|
//check if web browser available
|
||||||
|
if (browser.resolveActivity(getActivity().getPackageManager()) != null) {
|
||||||
|
startActivity(browser);
|
||||||
|
} else {
|
||||||
|
Toast toast = Toast.makeText(getContext(), getString(R.string.no_web_browser), LENGTH_SHORT);
|
||||||
|
toast.show();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openMap(LatLng coordinates) {
|
private void openMap(LatLng coordinates) {
|
||||||
|
|
|
||||||
|
|
@ -24,28 +24,34 @@ import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
|
|
||||||
import dagger.android.support.DaggerFragment;
|
|
||||||
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.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.di.CommonsDaggerSupportFragment;
|
||||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
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 android.widget.Toast.LENGTH_SHORT;
|
||||||
|
|
||||||
public class MediaDetailPagerFragment extends DaggerFragment implements ViewPager.OnPageChangeListener {
|
public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment implements ViewPager.OnPageChangeListener {
|
||||||
|
|
||||||
@Inject MediaWikiApi mwApi;
|
@Inject
|
||||||
@Inject SessionManager sessionManager;
|
MediaWikiApi mwApi;
|
||||||
@Inject @Named("default_preferences") SharedPreferences prefs;
|
@Inject
|
||||||
|
SessionManager sessionManager;
|
||||||
|
@Inject
|
||||||
|
@Named("default_preferences")
|
||||||
|
SharedPreferences prefs;
|
||||||
|
|
||||||
private ViewPager pager;
|
private ViewPager pager;
|
||||||
private Boolean editable;
|
private Boolean editable;
|
||||||
|
|
@ -118,7 +124,14 @@ public class MediaDetailPagerFragment extends DaggerFragment implements ViewPage
|
||||||
Intent viewIntent = new Intent();
|
Intent viewIntent = new Intent();
|
||||||
viewIntent.setAction(ACTION_VIEW);
|
viewIntent.setAction(ACTION_VIEW);
|
||||||
viewIntent.setData(m.getFilePageTitle().getMobileUri());
|
viewIntent.setData(m.getFilePageTitle().getMobileUri());
|
||||||
startActivity(viewIntent);
|
//check if web browser available
|
||||||
|
if(viewIntent.resolveActivity(getActivity().getPackageManager()) != null){
|
||||||
|
startActivity(viewIntent);
|
||||||
|
} else {
|
||||||
|
Toast toast = Toast.makeText(getContext(), getString(R.string.no_web_browser), LENGTH_SHORT);
|
||||||
|
toast.show();
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
case R.id.menu_download_current_image:
|
case R.id.menu_download_current_image:
|
||||||
// Download
|
// Download
|
||||||
|
|
@ -168,13 +181,19 @@ public class MediaDetailPagerFragment extends DaggerFragment implements ViewPage
|
||||||
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 && !(ContextCompat.checkSelfPermission(getContext(), READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED)) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
|
||||||
|
ContextCompat.checkSelfPermission(getContext(), READ_EXTERNAL_STORAGE)
|
||||||
|
!= PERMISSION_GRANTED
|
||||||
|
&& getView() != null) {
|
||||||
Snackbar.make(getView(), R.string.read_storage_permission_rationale,
|
Snackbar.make(getView(), R.string.read_storage_permission_rationale,
|
||||||
Snackbar.LENGTH_INDEFINITE).setAction(R.string.ok,
|
Snackbar.LENGTH_INDEFINITE).setAction(R.string.ok,
|
||||||
view -> ActivityCompat.requestPermissions(getActivity(),
|
view -> ActivityCompat.requestPermissions(getActivity(),
|
||||||
new String[]{READ_EXTERNAL_STORAGE}, 1)).show();
|
new String[]{READ_EXTERNAL_STORAGE}, 1)).show();
|
||||||
} else {
|
} else {
|
||||||
((DownloadManager) getActivity().getSystemService(DOWNLOAD_SERVICE)).enqueue(req);
|
DownloadManager systemService = (DownloadManager) getActivity().getSystemService(DOWNLOAD_SERVICE);
|
||||||
|
if (systemService != null) {
|
||||||
|
systemService.enqueue(req);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -12,26 +11,26 @@ import android.text.TextUtils;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import dagger.android.AndroidInjection;
|
|
||||||
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 fr.free.nrw.commons.modifications.ModifierSequenceDao.Table.TABLE_NAME;
|
import static fr.free.nrw.commons.modifications.ModifierSequenceDao.Table.TABLE_NAME;
|
||||||
|
|
||||||
public class ModificationsContentProvider extends ContentProvider {
|
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";
|
||||||
public 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) {
|
||||||
|
|
@ -40,12 +39,6 @@ public class ModificationsContentProvider extends ContentProvider {
|
||||||
|
|
||||||
@Inject DBOpenHelper dbOpenHelper;
|
@Inject DBOpenHelper dbOpenHelper;
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCreate() {
|
|
||||||
AndroidInjection.inject(this);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@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();
|
||||||
|
|
@ -77,7 +70,7 @@ public class ModificationsContentProvider extends ContentProvider {
|
||||||
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 = dbOpenHelper.getWritableDatabase();
|
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||||
long id = 0;
|
long id;
|
||||||
switch (uriType) {
|
switch (uriType) {
|
||||||
case MODIFICATIONS:
|
case MODIFICATIONS:
|
||||||
id = sqlDB.insert(TABLE_NAME, null, contentValues);
|
id = sqlDB.insert(TABLE_NAME, null, contentValues);
|
||||||
|
|
@ -139,7 +132,7 @@ public class ModificationsContentProvider extends ContentProvider {
|
||||||
*/
|
*/
|
||||||
int uriType = uriMatcher.match(uri);
|
int uriType = uriMatcher.match(uri);
|
||||||
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||||
int rowsUpdated = 0;
|
int rowsUpdated;
|
||||||
switch (uriType) {
|
switch (uriType) {
|
||||||
case MODIFICATIONS:
|
case MODIFICATIONS:
|
||||||
rowsUpdated = sqlDB.update(TABLE_NAME,
|
rowsUpdated = sqlDB.update(TABLE_NAME,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -16,16 +13,21 @@ import java.io.IOException;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
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.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 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);
|
||||||
|
|
@ -34,7 +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!
|
||||||
((CommonsApplication)getContext().getApplicationContext()).injector().inject(this);
|
ApplicationlessInjection
|
||||||
|
.getInstance(getContext()
|
||||||
|
.getApplicationContext())
|
||||||
|
.getCommonsApplicationComponent()
|
||||||
|
.inject(this);
|
||||||
|
|
||||||
Cursor allModifications;
|
Cursor allModifications;
|
||||||
try {
|
try {
|
||||||
|
|
@ -49,16 +55,7 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String authCookie;
|
String authCookie = sessionManager.getAuthCookie();
|
||||||
try {
|
|
||||||
authCookie = AccountManager.get(getContext()).blockingGetAuthToken(account, "", false);
|
|
||||||
} catch (OperationCanceledException | AuthenticatorException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
} catch (IOException e) {
|
|
||||||
Timber.d("Could not authenticate :(");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isNullOrWhiteSpace(authCookie)) {
|
if (isNullOrWhiteSpace(authCookie)) {
|
||||||
Timber.d("Could not authenticate :(");
|
Timber.d("Could not authenticate :(");
|
||||||
return;
|
return;
|
||||||
|
|
@ -80,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 = ModifierSequenceDao.fromCursor(allModifications);
|
ModifierSequence sequence = modifierSequenceDao.fromCursor(allModifications);
|
||||||
ModifierSequenceDao dao = new ModifierSequenceDao(contributionsClient);
|
|
||||||
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 = ContributionDao.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 = mwApi.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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,17 +117,17 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||||
try {
|
try {
|
||||||
editResult = mwApi.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 {
|
||||||
dao.delete(sequence);
|
modifierSequenceDao.delete(sequence);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
allModifications.moveToNext();
|
allModifications.moveToNext();
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ public class ModifierSequence {
|
||||||
for (PageModifier modifier: modifiers) {
|
for (PageModifier modifier: modifiers) {
|
||||||
editSummary.append(modifier.getEditSumary()).append(" ");
|
editSummary.append(modifier.getEditSumary()).append(" ");
|
||||||
}
|
}
|
||||||
editSummary.append("Via Commons Mobile App");
|
editSummary.append("Using [[COM:MOA|Commons Mobile App]]");
|
||||||
return editSummary.toString();
|
return editSummary.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,48 +11,59 @@ import org.json.JSONArray;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
import javax.inject.Provider;
|
||||||
|
|
||||||
public class ModifierSequenceDao {
|
public class ModifierSequenceDao {
|
||||||
|
|
||||||
private final ContentProviderClient client;
|
private final Provider<ContentProviderClient> clientProvider;
|
||||||
|
|
||||||
public ModifierSequenceDao(ContentProviderClient client) {
|
@Inject
|
||||||
this.client = client;
|
public ModifierSequenceDao(@Named("modification") Provider<ContentProviderClient> clientProvider) {
|
||||||
}
|
this.clientProvider = clientProvider;
|
||||||
|
|
||||||
public static ModifierSequence fromCursor(Cursor cursor) {
|
|
||||||
// Hardcoding column positions!
|
|
||||||
ModifierSequence ms = null;
|
|
||||||
try {
|
|
||||||
ms = new ModifierSequence(Uri.parse(cursor.getString(1)),
|
|
||||||
new JSONObject(cursor.getString(2)));
|
|
||||||
} catch (JSONException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
ms.setContentUri( ModificationsContentProvider.uriForId(cursor.getInt(0)));
|
|
||||||
|
|
||||||
return ms;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void save(ModifierSequence sequence) {
|
public void save(ModifierSequence sequence) {
|
||||||
|
ContentProviderClient db = clientProvider.get();
|
||||||
try {
|
try {
|
||||||
if (sequence.getContentUri() == null) {
|
if (sequence.getContentUri() == null) {
|
||||||
sequence.setContentUri(client.insert(ModificationsContentProvider.BASE_URI, toContentValues(sequence)));
|
sequence.setContentUri(db.insert(ModificationsContentProvider.BASE_URI, toContentValues(sequence)));
|
||||||
} else {
|
} else {
|
||||||
client.update(sequence.getContentUri(), toContentValues(sequence), null, null);
|
db.update(sequence.getContentUri(), toContentValues(sequence), null, null);
|
||||||
}
|
}
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
|
} finally {
|
||||||
|
db.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void delete(ModifierSequence sequence) {
|
public void delete(ModifierSequence sequence) {
|
||||||
|
ContentProviderClient db = clientProvider.get();
|
||||||
try {
|
try {
|
||||||
client.delete(sequence.getContentUri(), null, null);
|
db.delete(sequence.getContentUri(), null, null);
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
|
} finally {
|
||||||
|
db.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ModifierSequence fromCursor(Cursor cursor) {
|
||||||
|
// Hardcoding column positions!
|
||||||
|
ModifierSequence ms;
|
||||||
|
try {
|
||||||
|
ms = new ModifierSequence(Uri.parse(cursor.getString(cursor.getColumnIndex(Table.COLUMN_MEDIA_URI))),
|
||||||
|
new JSONObject(cursor.getString(cursor.getColumnIndex(Table.COLUMN_DATA))));
|
||||||
|
} catch (JSONException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
ms.setContentUri( ModificationsContentProvider.uriForId(cursor.getInt(cursor.getColumnIndex(Table.COLUMN_ID))));
|
||||||
|
|
||||||
|
return ms;
|
||||||
|
}
|
||||||
|
|
||||||
private JSONObject toJSON(ModifierSequence sequence) {
|
private JSONObject toJSON(ModifierSequence sequence) {
|
||||||
JSONObject data = new JSONObject();
|
JSONObject data = new JSONObject();
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -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,6 +23,8 @@ 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;
|
||||||
|
|
@ -36,11 +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.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
|
||||||
*/
|
*/
|
||||||
|
|
@ -50,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
|
||||||
|
|
@ -75,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());
|
||||||
}
|
}
|
||||||
|
|
@ -92,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());
|
||||||
}
|
}
|
||||||
|
|
@ -122,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";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -137,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();
|
||||||
|
|
@ -166,6 +205,14 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
||||||
.getNodes("/api/query/pages/page/imageinfo").size() > 0;
|
.getNodes("/api/query/pages/page/imageinfo").size() > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean pageExists(String pageName) throws IOException {
|
||||||
|
return Double.parseDouble( api.action("query")
|
||||||
|
.param("titles", pageName)
|
||||||
|
.get()
|
||||||
|
.getString("/api/query/pages/page/@_idx")) != -1;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Nullable
|
@Nullable
|
||||||
public String edit(String editToken, String processedPageContent, String filename, String summary) throws IOException {
|
public String edit(String editToken, String processedPageContent, String filename, String summary) throws IOException {
|
||||||
|
|
@ -178,6 +225,31 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
||||||
.getString("/api/edit/@result");
|
.getString("/api/edit/@result");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public String appendEdit(String editToken, String processedPageContent, String filename, String summary) throws IOException {
|
||||||
|
return api.action("edit")
|
||||||
|
.param("title", filename)
|
||||||
|
.param("token", editToken)
|
||||||
|
.param("appendtext", processedPageContent)
|
||||||
|
.param("summary", summary)
|
||||||
|
.post()
|
||||||
|
.getString("/api/edit/@result");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public String prependEdit(String editToken, String processedPageContent, String filename, String summary) throws IOException {
|
||||||
|
return api.action("edit")
|
||||||
|
.param("title", filename)
|
||||||
|
.param("token", editToken)
|
||||||
|
.param("prependtext", processedPageContent)
|
||||||
|
.param("summary", summary)
|
||||||
|
.post()
|
||||||
|
.getString("/api/edit/@result");
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String findThumbnailByFilename(String filename) throws IOException {
|
public String findThumbnailByFilename(String filename) throws IOException {
|
||||||
return api.action("query")
|
return api.action("query")
|
||||||
|
|
@ -353,6 +425,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")
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -24,6 +28,8 @@ public interface MediaWikiApi {
|
||||||
|
|
||||||
boolean fileExistsWithName(String fileName) throws IOException;
|
boolean fileExistsWithName(String fileName) throws IOException;
|
||||||
|
|
||||||
|
boolean pageExists(String pageName) throws IOException;
|
||||||
|
|
||||||
String findThumbnailByFilename(String filename) throws IOException;
|
String findThumbnailByFilename(String filename) throws IOException;
|
||||||
|
|
||||||
boolean logEvents(LogBuilder[] logBuilders);
|
boolean logEvents(LogBuilder[] logBuilders);
|
||||||
|
|
@ -34,6 +40,12 @@ public interface MediaWikiApi {
|
||||||
@Nullable
|
@Nullable
|
||||||
String edit(String editToken, String processedPageContent, String filename, String summary) throws IOException;
|
String edit(String editToken, String processedPageContent, String filename, String summary) throws IOException;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
String prependEdit(String editToken, String processedPageContent, String filename, String summary) throws IOException;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
String appendEdit(String editToken, String processedPageContent, String filename, String summary) throws IOException;
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
MediaResult fetchMediaByFilename(String filename) throws IOException;
|
MediaResult fetchMediaByFilename(String filename) throws IOException;
|
||||||
|
|
||||||
|
|
@ -43,6 +55,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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
package fr.free.nrw.commons.nearby;
|
||||||
|
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.support.v4.app.Fragment;
|
||||||
|
import android.support.v4.content.ContextCompat;
|
||||||
|
import android.support.v7.app.AlertDialog;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.R;
|
||||||
|
import fr.free.nrw.commons.contributions.ContributionController;
|
||||||
|
|
||||||
|
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
|
||||||
|
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
|
||||||
|
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
|
||||||
|
|
||||||
|
class DirectUpload {
|
||||||
|
|
||||||
|
private ContributionController controller;
|
||||||
|
private Fragment fragment;
|
||||||
|
|
||||||
|
DirectUpload(Fragment fragment, ContributionController controller) {
|
||||||
|
this.fragment = fragment;
|
||||||
|
this.controller = controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
void initiateCameraUpload() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
if (ContextCompat.checkSelfPermission(fragment.getActivity(), WRITE_EXTERNAL_STORAGE) != PERMISSION_GRANTED) {
|
||||||
|
if (fragment.getActivity().shouldShowRequestPermissionRationale(WRITE_EXTERNAL_STORAGE)) {
|
||||||
|
new AlertDialog.Builder(fragment.getActivity())
|
||||||
|
.setMessage(fragment.getActivity().getString(R.string.write_storage_permission_rationale))
|
||||||
|
.setPositiveButton("OK", (dialog, which) -> {
|
||||||
|
fragment.getActivity().requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, 3);
|
||||||
|
dialog.dismiss();
|
||||||
|
})
|
||||||
|
.setNegativeButton("Cancel", null)
|
||||||
|
.create()
|
||||||
|
.show();
|
||||||
|
} else {
|
||||||
|
fragment.getActivity().requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, 3);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
controller.startCameraCapture();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
controller.startCameraCapture();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void initiateGalleryUpload() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
if (ContextCompat.checkSelfPermission(fragment.getActivity(), READ_EXTERNAL_STORAGE) != PERMISSION_GRANTED) {
|
||||||
|
if (fragment.getActivity().shouldShowRequestPermissionRationale(READ_EXTERNAL_STORAGE)) {
|
||||||
|
new AlertDialog.Builder(fragment.getActivity())
|
||||||
|
.setMessage(fragment.getActivity().getString(R.string.read_storage_permission_rationale))
|
||||||
|
.setPositiveButton("OK", (dialog, which) -> {
|
||||||
|
fragment.getActivity().requestPermissions(new String[]{READ_EXTERNAL_STORAGE}, 1);
|
||||||
|
dialog.dismiss();
|
||||||
|
})
|
||||||
|
.setNegativeButton("Cancel", null)
|
||||||
|
.create()
|
||||||
|
.show();
|
||||||
|
} else {
|
||||||
|
fragment.getActivity().requestPermissions(new String[]{READ_EXTERNAL_STORAGE},
|
||||||
|
1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
controller.startGalleryPick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
controller.startGalleryPick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,21 +1,20 @@
|
||||||
package fr.free.nrw.commons.nearby;
|
package fr.free.nrw.commons.nearby;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
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.design.widget.BottomSheetBehavior;
|
||||||
|
|
||||||
import android.support.v4.app.FragmentTransaction;
|
import android.support.v4.app.FragmentTransaction;
|
||||||
import android.support.v7.app.AlertDialog;
|
import android.support.v7.app.AlertDialog;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
|
@ -28,30 +27,37 @@ import javax.inject.Inject;
|
||||||
|
|
||||||
import butterknife.BindView;
|
import butterknife.BindView;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
|
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.location.LatLng;
|
import fr.free.nrw.commons.location.LatLng;
|
||||||
import fr.free.nrw.commons.location.LocationServiceManager;
|
import fr.free.nrw.commons.location.LocationServiceManager;
|
||||||
import fr.free.nrw.commons.location.LocationUpdateListener;
|
import fr.free.nrw.commons.location.LocationUpdateListener;
|
||||||
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
||||||
import fr.free.nrw.commons.utils.UriSerializer;
|
import fr.free.nrw.commons.utils.UriSerializer;
|
||||||
|
|
||||||
import fr.free.nrw.commons.utils.ViewUtil;
|
import fr.free.nrw.commons.utils.ViewUtil;
|
||||||
import io.reactivex.Observable;
|
import io.reactivex.Observable;
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.disposables.Disposable;
|
import io.reactivex.disposables.Disposable;
|
||||||
import io.reactivex.schedulers.Schedulers;
|
import io.reactivex.schedulers.Schedulers;
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
import static fr.free.nrw.commons.location.LocationServiceManager.LOCATION_REQUEST;
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
|
||||||
public class NearbyActivity extends NavigationBaseActivity implements LocationUpdateListener {
|
public class NearbyActivity extends NavigationBaseActivity implements LocationUpdateListener {
|
||||||
|
|
||||||
private static final int LOCATION_REQUEST = 1;
|
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;
|
||||||
|
|
||||||
|
@BindView(R.id.bottom_sheet)
|
||||||
|
LinearLayout bottomSheet;
|
||||||
|
@BindView(R.id.bottom_sheet_details)
|
||||||
|
LinearLayout bottomSheetDetails;
|
||||||
|
@BindView(R.id.transparentView)
|
||||||
|
View transparentView;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
LocationServiceManager locationManager;
|
LocationServiceManager locationManager;
|
||||||
@Inject
|
@Inject
|
||||||
|
|
@ -59,28 +65,57 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
|
||||||
|
|
||||||
private LatLng curLatLang;
|
private LatLng curLatLang;
|
||||||
private Bundle bundle;
|
private Bundle bundle;
|
||||||
private SharedPreferences sharedPreferences;
|
|
||||||
private NearbyActivityMode viewMode;
|
|
||||||
private Disposable placesDisposable;
|
private Disposable placesDisposable;
|
||||||
private boolean lockNearbyView; //Determines if the nearby places needs to be refreshed
|
private boolean lockNearbyView; //Determines if the nearby places needs to be refreshed
|
||||||
|
private BottomSheetBehavior bottomSheetBehavior; // Behavior for list bottom sheet
|
||||||
|
private BottomSheetBehavior bottomSheetBehaviorForDetails; // Behavior for details bottom sheet
|
||||||
|
private NearbyMapFragment nearbyMapFragment;
|
||||||
|
private NearbyListFragment nearbyListFragment;
|
||||||
|
private static final String TAG_RETAINED_MAP_FRAGMENT = NearbyMapFragment.class.getSimpleName();
|
||||||
|
private static final String TAG_RETAINED_LIST_FRAGMENT = NearbyListFragment.class.getSimpleName();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
|
|
||||||
setContentView(R.layout.activity_nearby);
|
setContentView(R.layout.activity_nearby);
|
||||||
ButterKnife.bind(this);
|
ButterKnife.bind(this);
|
||||||
|
resumeFragment();
|
||||||
bundle = new Bundle();
|
bundle = new Bundle();
|
||||||
|
|
||||||
|
initBottomSheetBehaviour();
|
||||||
initDrawer();
|
initDrawer();
|
||||||
initViewState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initViewState() {
|
private void resumeFragment() {
|
||||||
if (sharedPreferences.getBoolean(MAP_LAST_USED_PREFERENCE, false)) {
|
// Find the retained fragment on activity restarts
|
||||||
viewMode = NearbyActivityMode.MAP;
|
nearbyMapFragment = getMapFragment();
|
||||||
} else {
|
nearbyListFragment = getListFragment();
|
||||||
viewMode = NearbyActivityMode.LIST;
|
}
|
||||||
}
|
|
||||||
|
private void initBottomSheetBehaviour() {
|
||||||
|
|
||||||
|
transparentView.setAlpha(0);
|
||||||
|
|
||||||
|
bottomSheet.getLayoutParams().height = getWindowManager()
|
||||||
|
.getDefaultDisplay().getHeight() / 16 * 9;
|
||||||
|
bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet);
|
||||||
|
// TODO initProperBottomSheetBehavior();
|
||||||
|
bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStateChanged(View bottomSheet, int newState) {
|
||||||
|
prepareViewsForSheetPosition(newState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSlide(View bottomSheet, float slideOffset) {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
|
bottomSheetBehaviorForDetails = BottomSheetBehavior.from(bottomSheetDetails);
|
||||||
|
bottomSheetBehaviorForDetails.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -88,11 +123,6 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
|
||||||
MenuInflater inflater = getMenuInflater();
|
MenuInflater inflater = getMenuInflater();
|
||||||
inflater.inflate(R.menu.menu_nearby, menu);
|
inflater.inflate(R.menu.menu_nearby, menu);
|
||||||
|
|
||||||
if (viewMode.isMap()) {
|
|
||||||
MenuItem item = menu.findItem(R.id.action_toggle_view);
|
|
||||||
item.setIcon(viewMode.getIcon());
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.onCreateOptionsMenu(menu);
|
return super.onCreateOptionsMenu(menu);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,14 +130,9 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
// Handle item selection
|
// Handle item selection
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
case R.id.action_refresh:
|
case R.id.action_display_list:
|
||||||
lockNearbyView(false);
|
bottomSheetBehaviorForDetails.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
refreshView(true);
|
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
|
||||||
return true;
|
|
||||||
case R.id.action_toggle_view:
|
|
||||||
viewMode = viewMode.toggle();
|
|
||||||
item.setIcon(viewMode.getIcon());
|
|
||||||
toggleView();
|
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
|
|
@ -125,7 +150,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
|
||||||
switch (requestCode) {
|
switch (requestCode) {
|
||||||
case LOCATION_REQUEST: {
|
case LOCATION_REQUEST: {
|
||||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
refreshView(false);
|
refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED);
|
||||||
} else {
|
} else {
|
||||||
//If permission not granted, go to page that says Nearby Places cannot be displayed
|
//If permission not granted, go to page that says Nearby Places cannot be displayed
|
||||||
hideProgressBar();
|
hideProgressBar();
|
||||||
|
|
@ -180,7 +205,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
|
||||||
private void checkLocationPermission() {
|
private void checkLocationPermission() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
if (locationManager.isLocationPermissionGranted()) {
|
if (locationManager.isLocationPermissionGranted()) {
|
||||||
refreshView(false);
|
refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED);
|
||||||
} else {
|
} else {
|
||||||
// Should we show an explanation?
|
// Should we show an explanation?
|
||||||
if (locationManager.isPermissionExplanationRequired(this)) {
|
if (locationManager.isPermissionExplanationRequired(this)) {
|
||||||
|
|
@ -206,7 +231,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
refreshView(false);
|
refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -215,23 +240,15 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
|
||||||
super.onActivityResult(requestCode, resultCode, data);
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
if (requestCode == 1) {
|
if (requestCode == 1) {
|
||||||
Timber.d("User is back from Settings page");
|
Timber.d("User is back from Settings page");
|
||||||
refreshView(false);
|
refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void toggleView() {
|
|
||||||
if (viewMode.isMap()) {
|
|
||||||
setMapFragment();
|
|
||||||
} else {
|
|
||||||
setListFragment();
|
|
||||||
}
|
|
||||||
sharedPreferences.edit().putBoolean(MAP_LAST_USED_PREFERENCE, viewMode.isMap()).apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onStart() {
|
protected void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
locationManager.addLocationListener(this);
|
locationManager.addLocationListener(this);
|
||||||
|
locationManager.registerLocationManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -256,21 +273,34 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
|
||||||
checkGps();
|
checkGps();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
// this means that this activity will not be recreated now, user is leaving it
|
||||||
|
// or the activity is otherwise finishing
|
||||||
|
if(isFinishing()) {
|
||||||
|
// we will not need this fragment anymore, this may also be a good place to signal
|
||||||
|
// to the retained fragment object to perform its own cleanup.
|
||||||
|
removeMapFragment();
|
||||||
|
removeListFragment();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method should be the single point to load/refresh nearby places
|
* This method should be the single point to load/refresh nearby places
|
||||||
*
|
*
|
||||||
* @param isHardRefresh
|
* @param locationChangeType defines if location shanged significantly or slightly
|
||||||
*/
|
*/
|
||||||
private void refreshView(boolean isHardRefresh) {
|
private void refreshView(LocationServiceManager.LocationChangeType locationChangeType) {
|
||||||
if (lockNearbyView) {
|
if (lockNearbyView) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
locationManager.registerLocationManager();
|
locationManager.registerLocationManager();
|
||||||
LatLng lastLocation = locationManager.getLastLocation();
|
LatLng lastLocation = locationManager.getLastLocation();
|
||||||
|
|
||||||
if (curLatLang != null && curLatLang.equals(lastLocation)) { //refresh view only if location has changed
|
if (curLatLang != null && curLatLang.equals(lastLocation)) { //refresh view only if location has changed
|
||||||
if (isHardRefresh) {
|
|
||||||
ViewUtil.showLongToast(this, R.string.nearby_location_has_not_changed);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
curLatLang = lastLocation;
|
curLatLang = lastLocation;
|
||||||
|
|
@ -280,40 +310,56 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
progressBar.setVisibility(View.VISIBLE);
|
if (locationChangeType
|
||||||
placesDisposable = Observable.fromCallable(() -> nearbyController
|
.equals(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED)) {
|
||||||
.loadAttractionsFromLocation(curLatLang, this))
|
progressBar.setVisibility(View.VISIBLE);
|
||||||
.subscribeOn(Schedulers.io())
|
placesDisposable = Observable.fromCallable(() -> nearbyController
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.loadAttractionsFromLocation(curLatLang))
|
||||||
.subscribe(this::populatePlaces);
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(this::populatePlaces);
|
||||||
|
} else if (locationChangeType
|
||||||
|
.equals(LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED)) {
|
||||||
|
Gson gson = new GsonBuilder()
|
||||||
|
.registerTypeAdapter(Uri.class, new UriSerializer())
|
||||||
|
.create();
|
||||||
|
String gsonCurLatLng = gson.toJson(curLatLang);
|
||||||
|
bundle.putString("CurLatLng", gsonCurLatLng);
|
||||||
|
updateMapFragment(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void populatePlaces(List<Place> placeList) {
|
private void populatePlaces(NearbyController.NearbyPlacesInfo nearbyPlacesInfo) {
|
||||||
|
List<Place> placeList = nearbyPlacesInfo.placeList;
|
||||||
|
LatLng[] boundaryCoordinates = nearbyPlacesInfo.boundaryCoordinates;
|
||||||
Gson gson = new GsonBuilder()
|
Gson gson = new GsonBuilder()
|
||||||
.registerTypeAdapter(Uri.class, new UriSerializer())
|
.registerTypeAdapter(Uri.class, new UriSerializer())
|
||||||
.create();
|
.create();
|
||||||
String gsonPlaceList = gson.toJson(placeList);
|
String gsonPlaceList = gson.toJson(placeList);
|
||||||
String gsonCurLatLng = gson.toJson(curLatLang);
|
String gsonCurLatLng = gson.toJson(curLatLang);
|
||||||
|
String gsonBoundaryCoordinates = gson.toJson(boundaryCoordinates);
|
||||||
|
|
||||||
if (placeList.size() == 0) {
|
if (placeList.size() == 0) {
|
||||||
int duration = Toast.LENGTH_SHORT;
|
ViewUtil.showSnackbar(findViewById(R.id.container), R.string.no_nearby);
|
||||||
Toast toast = Toast.makeText(this, R.string.no_nearby, duration);
|
|
||||||
toast.show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bundle.clear();
|
bundle.clear();
|
||||||
bundle.putString("PlaceList", gsonPlaceList);
|
bundle.putString("PlaceList", gsonPlaceList);
|
||||||
bundle.putString("CurLatLng", gsonCurLatLng);
|
bundle.putString("CurLatLng", gsonCurLatLng);
|
||||||
|
bundle.putString("BoundaryCoord", gsonBoundaryCoordinates);
|
||||||
|
|
||||||
lockNearbyView(true);
|
// First time to init fragments
|
||||||
// Begin the transaction
|
if (nearbyMapFragment == null) {
|
||||||
if (viewMode.isMap()) {
|
lockNearbyView(true);
|
||||||
setMapFragment();
|
setMapFragment();
|
||||||
} else {
|
|
||||||
setListFragment();
|
setListFragment();
|
||||||
|
hideProgressBar();
|
||||||
|
lockNearbyView(false);
|
||||||
|
} else {
|
||||||
|
// There are fragments, just update the map and list
|
||||||
|
updateMapFragment(false);
|
||||||
|
updateListFragment();
|
||||||
}
|
}
|
||||||
|
|
||||||
hideProgressBar();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void lockNearbyView(boolean lock) {
|
private void lockNearbyView(boolean lock) {
|
||||||
|
|
@ -334,14 +380,92 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private NearbyMapFragment getMapFragment() {
|
||||||
|
return (NearbyMapFragment) getSupportFragmentManager().findFragmentByTag(TAG_RETAINED_MAP_FRAGMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeMapFragment() {
|
||||||
|
if (nearbyMapFragment != null) {
|
||||||
|
android.support.v4.app.FragmentManager fm = getSupportFragmentManager();
|
||||||
|
fm.beginTransaction().remove(nearbyMapFragment).commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private NearbyListFragment getListFragment() {
|
||||||
|
return (NearbyListFragment) getSupportFragmentManager().findFragmentByTag(TAG_RETAINED_LIST_FRAGMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeListFragment() {
|
||||||
|
if (nearbyListFragment != null) {
|
||||||
|
android.support.v4.app.FragmentManager fm = getSupportFragmentManager();
|
||||||
|
fm.beginTransaction().remove(nearbyListFragment).commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateMapFragment(boolean isSlightUpdate) {
|
||||||
|
/*
|
||||||
|
* Significant update means updating nearby place markers. Slightly update means only
|
||||||
|
* updating current location marker and camera target.
|
||||||
|
* We update our map Significantly on each 1000 meter change, but we can't never know
|
||||||
|
* the frequency of nearby places. Thus we check if we are close to the boundaries of
|
||||||
|
* our nearby markers, we update our map Significantly.
|
||||||
|
* */
|
||||||
|
|
||||||
|
NearbyMapFragment nearbyMapFragment = getMapFragment();
|
||||||
|
|
||||||
|
if (nearbyMapFragment != null && curLatLang != null) {
|
||||||
|
hideProgressBar(); // In case it is visible (this happens, not an impossible case)
|
||||||
|
/*
|
||||||
|
* If we are close to nearby places boundaries, we need a significant update to
|
||||||
|
* get new nearby places. Check order is south, north, west, east
|
||||||
|
* */
|
||||||
|
if (nearbyMapFragment.boundaryCoordinates != null
|
||||||
|
&& (curLatLang.getLatitude() <= nearbyMapFragment.boundaryCoordinates[0].getLatitude()
|
||||||
|
|| curLatLang.getLatitude() >= nearbyMapFragment.boundaryCoordinates[1].getLatitude()
|
||||||
|
|| curLatLang.getLongitude() <= nearbyMapFragment.boundaryCoordinates[2].getLongitude()
|
||||||
|
|| curLatLang.getLongitude() >= nearbyMapFragment.boundaryCoordinates[3].getLongitude())) {
|
||||||
|
// populate places
|
||||||
|
placesDisposable = Observable.fromCallable(() -> nearbyController
|
||||||
|
.loadAttractionsFromLocation(curLatLang))
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(this::populatePlaces);
|
||||||
|
nearbyMapFragment.setArguments(bundle);
|
||||||
|
nearbyMapFragment.updateMapSignificantly();
|
||||||
|
updateListFragment();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSlightUpdate) {
|
||||||
|
nearbyMapFragment.setArguments(bundle);
|
||||||
|
nearbyMapFragment.updateMapSlightly();
|
||||||
|
} else {
|
||||||
|
nearbyMapFragment.setArguments(bundle);
|
||||||
|
nearbyMapFragment.updateMapSignificantly();
|
||||||
|
updateListFragment();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lockNearbyView(true);
|
||||||
|
setMapFragment();
|
||||||
|
setListFragment();
|
||||||
|
hideProgressBar();
|
||||||
|
lockNearbyView(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateListFragment() {
|
||||||
|
nearbyListFragment.setArguments(bundle);
|
||||||
|
nearbyListFragment.updateNearbyListSignificantly();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls fragment for map view.
|
* Calls fragment for map view.
|
||||||
*/
|
*/
|
||||||
private void setMapFragment() {
|
private void setMapFragment() {
|
||||||
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
|
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
|
||||||
Fragment fragment = new NearbyMapFragment();
|
nearbyMapFragment = new NearbyMapFragment();
|
||||||
fragment.setArguments(bundle);
|
nearbyMapFragment.setArguments(bundle);
|
||||||
fragmentTransaction.replace(R.id.container, fragment, fragment.getClass().getSimpleName());
|
fragmentTransaction.replace(R.id.container, nearbyMapFragment, TAG_RETAINED_MAP_FRAGMENT);
|
||||||
fragmentTransaction.commitAllowingStateLoss();
|
fragmentTransaction.commitAllowingStateLoss();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -350,14 +474,25 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
|
||||||
*/
|
*/
|
||||||
private void setListFragment() {
|
private void setListFragment() {
|
||||||
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
|
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
|
||||||
Fragment fragment = new NearbyListFragment();
|
nearbyListFragment = new NearbyListFragment();
|
||||||
fragment.setArguments(bundle);
|
nearbyListFragment.setArguments(bundle);
|
||||||
fragmentTransaction.replace(R.id.container, fragment, fragment.getClass().getSimpleName());
|
fragmentTransaction.replace(R.id.container_sheet, nearbyListFragment, TAG_RETAINED_LIST_FRAGMENT);
|
||||||
|
initBottomSheetBehaviour();
|
||||||
|
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
fragmentTransaction.commitAllowingStateLoss();
|
fragmentTransaction.commitAllowingStateLoss();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLocationChanged(LatLng latLng) {
|
public void onLocationChangedSignificantly(LatLng latLng) {
|
||||||
refreshView(false);
|
refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLocationChangedSlightly(LatLng latLng) {
|
||||||
|
refreshView(LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void prepareViewsForSheetPosition(int bottomSheetState) {
|
||||||
|
// TODO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
package fr.free.nrw.commons.nearby;
|
|
||||||
|
|
||||||
import android.support.annotation.DrawableRes;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
|
|
||||||
enum NearbyActivityMode {
|
|
||||||
MAP(R.drawable.ic_list_white_24dp),
|
|
||||||
LIST(R.drawable.ic_map_white_24dp);
|
|
||||||
|
|
||||||
@DrawableRes
|
|
||||||
private final int icon;
|
|
||||||
|
|
||||||
NearbyActivityMode(int icon) {
|
|
||||||
this.icon = icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
@DrawableRes
|
|
||||||
public int getIcon() {
|
|
||||||
return icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
public NearbyActivityMode toggle() {
|
|
||||||
return isMap() ? LIST : MAP;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isMap() {
|
|
||||||
return MAP.equals(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package fr.free.nrw.commons.nearby;
|
package fr.free.nrw.commons.nearby;
|
||||||
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
|
import android.support.v4.app.Fragment;
|
||||||
|
|
||||||
import com.pedrogomez.renderers.ListAdapteeCollection;
|
import com.pedrogomez.renderers.ListAdapteeCollection;
|
||||||
import com.pedrogomez.renderers.RVRendererAdapter;
|
import com.pedrogomez.renderers.RVRendererAdapter;
|
||||||
|
|
@ -9,18 +10,32 @@ import com.pedrogomez.renderers.RendererBuilder;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
class NearbyAdapterFactory {
|
import fr.free.nrw.commons.contributions.ContributionController;
|
||||||
private PlaceRenderer.PlaceClickedListener listener;
|
|
||||||
|
|
||||||
NearbyAdapterFactory(@NonNull PlaceRenderer.PlaceClickedListener listener) {
|
class NearbyAdapterFactory {
|
||||||
this.listener = listener;
|
|
||||||
|
private Fragment fragment;
|
||||||
|
private ContributionController controller;
|
||||||
|
|
||||||
|
NearbyAdapterFactory(){
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
NearbyAdapterFactory(Fragment fragment, ContributionController controller) {
|
||||||
|
this.fragment = fragment;
|
||||||
|
this.controller = controller;
|
||||||
}
|
}
|
||||||
|
|
||||||
public RVRendererAdapter<Place> create(List<Place> placeList) {
|
public RVRendererAdapter<Place> create(List<Place> placeList) {
|
||||||
RendererBuilder<Place> builder = new RendererBuilder<Place>()
|
RendererBuilder<Place> builder = new RendererBuilder<Place>()
|
||||||
.bind(Place.class, new PlaceRenderer(listener));
|
.bind(Place.class, new PlaceRenderer(fragment, controller));
|
||||||
ListAdapteeCollection<Place> collection = new ListAdapteeCollection<>(
|
ListAdapteeCollection<Place> collection = new ListAdapteeCollection<>(
|
||||||
placeList != null ? placeList : Collections.<Place>emptyList());
|
placeList != null ? placeList : Collections.emptyList());
|
||||||
return new RVRendererAdapter<>(builder, collection);
|
return new RVRendererAdapter<>(builder, collection);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public void updateAdapterData(List<Place> newPlaceList, RVRendererAdapter<Place> rendererAdapter) {
|
||||||
|
rendererAdapter.notifyDataSetChanged();
|
||||||
|
rendererAdapter.diffUpdate(newPlaceList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -37,7 +37,7 @@ public class NearbyBaseMarker extends BaseMarkerOptions<NearbyMarker, NearbyBase
|
||||||
.registerTypeAdapter(Uri.class, new UriDeserializer())
|
.registerTypeAdapter(Uri.class, new UriDeserializer())
|
||||||
.create();
|
.create();
|
||||||
|
|
||||||
position((LatLng) in.readParcelable(LatLng.class.getClassLoader()));
|
position(in.readParcelable(LatLng.class.getClassLoader()));
|
||||||
snippet(in.readString());
|
snippet(in.readString());
|
||||||
String iconId = in.readString();
|
String iconId = in.readString();
|
||||||
Bitmap iconBitmap = in.readParcelable(Bitmap.class.getClassLoader());
|
Bitmap iconBitmap = in.readParcelable(Bitmap.class.getClassLoader());
|
||||||
|
|
|
||||||
|
|
@ -39,23 +39,44 @@ public class NearbyController {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepares Place list to make their distance information update later.
|
* Prepares Place list to make their distance information update later.
|
||||||
|
*
|
||||||
* @param curLatLng current location for user
|
* @param curLatLng current location for user
|
||||||
* @param context context
|
* @return NearbyPlacesInfo a variable holds Place list without distance information
|
||||||
* @return Place list without distance information
|
* and boundary coordinates of current Place List
|
||||||
*/
|
*/
|
||||||
public List<Place> loadAttractionsFromLocation(LatLng curLatLng, Context context) {
|
public NearbyPlacesInfo loadAttractionsFromLocation(LatLng curLatLng) {
|
||||||
|
|
||||||
Timber.d("Loading attractions near %s", curLatLng);
|
Timber.d("Loading attractions near %s", curLatLng);
|
||||||
|
NearbyPlacesInfo nearbyPlacesInfo = new NearbyPlacesInfo();
|
||||||
|
|
||||||
if (curLatLng == null) {
|
if (curLatLng == null) {
|
||||||
return Collections.emptyList();
|
return null;
|
||||||
}
|
}
|
||||||
List<Place> places = prefs.getBoolean("useWikidata", true)
|
List<Place> places = nearbyPlaces.getFromWikidataQuery(curLatLng, Locale.getDefault().getLanguage());
|
||||||
? nearbyPlaces.getFromWikidataQuery(curLatLng, Locale.getDefault().getLanguage())
|
|
||||||
: nearbyPlaces.getFromWikiNeedsPictures();
|
LatLng[] boundaryCoordinates = {places.get(0).location, // south
|
||||||
|
places.get(0).location, // north
|
||||||
|
places.get(0).location, // west
|
||||||
|
places.get(0).location};// east, init with a random location
|
||||||
|
|
||||||
if (curLatLng != null) {
|
if (curLatLng != null) {
|
||||||
Timber.d("Sorting places by distance...");
|
Timber.d("Sorting places by distance...");
|
||||||
final Map<Place, Double> distances = new HashMap<>();
|
final Map<Place, Double> distances = new HashMap<>();
|
||||||
for (Place place: places) {
|
for (Place place: places) {
|
||||||
distances.put(place, computeDistanceBetween(place.location, curLatLng));
|
distances.put(place, computeDistanceBetween(place.location, curLatLng));
|
||||||
|
// Find boundaries with basic find max approach
|
||||||
|
if (place.location.getLatitude() < boundaryCoordinates[0].getLatitude()) {
|
||||||
|
boundaryCoordinates[0] = place.location;
|
||||||
|
}
|
||||||
|
if (place.location.getLatitude() > boundaryCoordinates[1].getLatitude()) {
|
||||||
|
boundaryCoordinates[1] = place.location;
|
||||||
|
}
|
||||||
|
if (place.location.getLongitude() < boundaryCoordinates[2].getLongitude()) {
|
||||||
|
boundaryCoordinates[2] = place.location;
|
||||||
|
}
|
||||||
|
if (place.location.getLongitude() > boundaryCoordinates[3].getLongitude()) {
|
||||||
|
boundaryCoordinates[3] = place.location;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Collections.sort(places,
|
Collections.sort(places,
|
||||||
(lhs, rhs) -> {
|
(lhs, rhs) -> {
|
||||||
|
|
@ -65,11 +86,14 @@ public class NearbyController {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return places;
|
nearbyPlacesInfo.placeList = places;
|
||||||
|
nearbyPlacesInfo.boundaryCoordinates = boundaryCoordinates;
|
||||||
|
return nearbyPlacesInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads attractions from location for list view, we need to return Place data type.
|
* Loads attractions from location for list view, we need to return Place data type.
|
||||||
|
*
|
||||||
* @param curLatLng users current location
|
* @param curLatLng users current location
|
||||||
* @param placeList list of nearby places in Place data type
|
* @param placeList list of nearby places in Place data type
|
||||||
* @return Place list that holds nearby places
|
* @return Place list that holds nearby places
|
||||||
|
|
@ -78,7 +102,7 @@ public class NearbyController {
|
||||||
LatLng curLatLng,
|
LatLng curLatLng,
|
||||||
List<Place> placeList) {
|
List<Place> placeList) {
|
||||||
placeList = placeList.subList(0, Math.min(placeList.size(), MAX_RESULTS));
|
placeList = placeList.subList(0, Math.min(placeList.size(), MAX_RESULTS));
|
||||||
for (Place place: placeList) {
|
for (Place place : placeList) {
|
||||||
String distance = formatDistanceBetween(curLatLng, place.location);
|
String distance = formatDistanceBetween(curLatLng, place.location);
|
||||||
place.setDistance(distance);
|
place.setDistance(distance);
|
||||||
}
|
}
|
||||||
|
|
@ -86,7 +110,8 @@ public class NearbyController {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*Loads attractions from location for map view, we need to return BaseMarkerOption data type.
|
* Loads attractions from location for map view, we need to return BaseMarkerOption data type.
|
||||||
|
*
|
||||||
* @param curLatLng users current location
|
* @param curLatLng users current location
|
||||||
* @param placeList list of nearby places in Place data type
|
* @param placeList list of nearby places in Place data type
|
||||||
* @return BaseMarkerOptions list that holds nearby places
|
* @return BaseMarkerOptions list that holds nearby places
|
||||||
|
|
@ -103,27 +128,34 @@ public class NearbyController {
|
||||||
|
|
||||||
placeList = placeList.subList(0, Math.min(placeList.size(), MAX_RESULTS));
|
placeList = placeList.subList(0, Math.min(placeList.size(), MAX_RESULTS));
|
||||||
|
|
||||||
Bitmap icon = UiUtils.getBitmap(
|
VectorDrawableCompat vectorDrawable = VectorDrawableCompat.create(
|
||||||
VectorDrawableCompat.create(
|
context.getResources(), R.drawable.ic_custom_map_marker, context.getTheme()
|
||||||
context.getResources(), R.drawable.ic_custom_map_marker, context.getTheme()
|
);
|
||||||
));
|
if (vectorDrawable != null) {
|
||||||
|
Bitmap icon = UiUtils.getBitmap(vectorDrawable);
|
||||||
|
|
||||||
for (Place place: placeList) {
|
for (Place place : placeList) {
|
||||||
String distance = formatDistanceBetween(curLatLng, place.location);
|
String distance = formatDistanceBetween(curLatLng, place.location);
|
||||||
place.setDistance(distance);
|
place.setDistance(distance);
|
||||||
|
|
||||||
NearbyBaseMarker nearbyBaseMarker = new NearbyBaseMarker();
|
NearbyBaseMarker nearbyBaseMarker = new NearbyBaseMarker();
|
||||||
nearbyBaseMarker.title(place.name);
|
nearbyBaseMarker.title(place.name);
|
||||||
nearbyBaseMarker.position(
|
nearbyBaseMarker.position(
|
||||||
new com.mapbox.mapboxsdk.geometry.LatLng(
|
new com.mapbox.mapboxsdk.geometry.LatLng(
|
||||||
place.location.getLatitude(),
|
place.location.getLatitude(),
|
||||||
place.location.getLongitude()));
|
place.location.getLongitude()));
|
||||||
nearbyBaseMarker.place(place);
|
nearbyBaseMarker.place(place);
|
||||||
nearbyBaseMarker.icon(IconFactory.getInstance(context)
|
nearbyBaseMarker.icon(IconFactory.getInstance(context)
|
||||||
.fromBitmap(icon));
|
.fromBitmap(icon));
|
||||||
|
|
||||||
baseMarkerOptions.add(nearbyBaseMarker);
|
baseMarkerOptions.add(nearbyBaseMarker);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return baseMarkerOptions;
|
return baseMarkerOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class NearbyPlacesInfo {
|
||||||
|
List<Place> placeList; // List of nearby places
|
||||||
|
LatLng[] boundaryCoordinates; // Corners of nearby area
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,152 +0,0 @@
|
||||||
package fr.free.nrw.commons.nearby;
|
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.support.v4.app.FragmentActivity;
|
|
||||||
import android.support.v7.widget.PopupMenu;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import butterknife.BindView;
|
|
||||||
import butterknife.ButterKnife;
|
|
||||||
import butterknife.OnClick;
|
|
||||||
import butterknife.Unbinder;
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.location.LatLng;
|
|
||||||
import fr.free.nrw.commons.ui.widget.OverlayDialog;
|
|
||||||
import fr.free.nrw.commons.utils.DialogUtil;
|
|
||||||
|
|
||||||
public class NearbyInfoDialog extends OverlayDialog {
|
|
||||||
|
|
||||||
private final static String ARG_TITLE = "placeTitle";
|
|
||||||
private final static String ARG_DESC = "placeDesc";
|
|
||||||
private final static String ARG_LATITUDE = "latitude";
|
|
||||||
private final static String ARG_LONGITUDE = "longitude";
|
|
||||||
private final static String ARG_SITE_LINK = "sitelink";
|
|
||||||
|
|
||||||
@BindView(R.id.link_preview_title) TextView placeTitle;
|
|
||||||
@BindView(R.id.link_preview_extract) TextView placeDescription;
|
|
||||||
@BindView(R.id.link_preview_go_button) TextView goToButton;
|
|
||||||
@BindView(R.id.link_preview_overflow_button) ImageView overflowButton;
|
|
||||||
|
|
||||||
private Unbinder unbinder;
|
|
||||||
private LatLng location;
|
|
||||||
private Sitelinks sitelinks;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
|
||||||
View view = inflater.inflate(R.layout.dialog_nearby_info, container, false);
|
|
||||||
unbinder = ButterKnife.bind(this, view);
|
|
||||||
initUi();
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initUi() {
|
|
||||||
Bundle bundle = getArguments();
|
|
||||||
placeTitle.setText(bundle.getString(ARG_TITLE));
|
|
||||||
placeDescription.setText(bundle.getString(ARG_DESC));
|
|
||||||
location = new LatLng(bundle.getDouble(ARG_LATITUDE), bundle.getDouble(ARG_LONGITUDE), 0);
|
|
||||||
getArticleLink(bundle);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void getArticleLink(Bundle bundle) {
|
|
||||||
this.sitelinks = bundle.getParcelable(ARG_SITE_LINK);
|
|
||||||
|
|
||||||
if (sitelinks == null || Uri.EMPTY.equals(sitelinks.getWikipediaLink())) {
|
|
||||||
goToButton.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
overflowButton.setVisibility(showMenu() ? View.VISIBLE : View.GONE);
|
|
||||||
|
|
||||||
overflowButton.setOnClickListener(v -> popupMenuListener());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void popupMenuListener() {
|
|
||||||
PopupMenu popupMenu = new PopupMenu(getActivity(), overflowButton);
|
|
||||||
popupMenu.inflate(R.menu.nearby_info_dialog_options);
|
|
||||||
|
|
||||||
MenuItem commonsArticle = popupMenu.getMenu()
|
|
||||||
.findItem(R.id.nearby_info_menu_commons_article);
|
|
||||||
MenuItem wikiDataArticle = popupMenu.getMenu()
|
|
||||||
.findItem(R.id.nearby_info_menu_wikidata_article);
|
|
||||||
|
|
||||||
commonsArticle.setEnabled(!sitelinks.getCommonsLink().equals(Uri.EMPTY));
|
|
||||||
wikiDataArticle.setEnabled(!sitelinks.getWikidataLink().equals(Uri.EMPTY));
|
|
||||||
|
|
||||||
popupMenu.setOnMenuItemClickListener(menuListener);
|
|
||||||
popupMenu.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean showMenu() {
|
|
||||||
return !sitelinks.getCommonsLink().equals(Uri.EMPTY)
|
|
||||||
|| !sitelinks.getWikidataLink().equals(Uri.EMPTY);
|
|
||||||
}
|
|
||||||
|
|
||||||
private final PopupMenu.OnMenuItemClickListener menuListener = new PopupMenu
|
|
||||||
.OnMenuItemClickListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onMenuItemClick(MenuItem item) {
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
case R.id.nearby_info_menu_commons_article:
|
|
||||||
openWebView(sitelinks.getCommonsLink());
|
|
||||||
return true;
|
|
||||||
case R.id.nearby_info_menu_wikidata_article:
|
|
||||||
openWebView(sitelinks.getWikidataLink());
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public static void showYourself(FragmentActivity fragmentActivity, Place place) {
|
|
||||||
NearbyInfoDialog mDialog = new NearbyInfoDialog();
|
|
||||||
Bundle bundle = new Bundle();
|
|
||||||
bundle.putString(ARG_TITLE, place.name);
|
|
||||||
bundle.putString(ARG_DESC, place.getDescription().getText());
|
|
||||||
bundle.putDouble(ARG_LATITUDE, place.location.getLatitude());
|
|
||||||
bundle.putDouble(ARG_LONGITUDE, place.location.getLongitude());
|
|
||||||
bundle.putParcelable(ARG_SITE_LINK, place.siteLinks);
|
|
||||||
mDialog.setArguments(bundle);
|
|
||||||
DialogUtil.showSafely(fragmentActivity, mDialog);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroyView() {
|
|
||||||
super.onDestroyView();
|
|
||||||
unbinder.unbind();
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnClick(R.id.link_preview_directions_button)
|
|
||||||
void onDirectionsClick() {
|
|
||||||
//Open map app at given position
|
|
||||||
Uri gmmIntentUri = Uri.parse(
|
|
||||||
"geo:0,0?q=" + location.getLatitude() + "," + location.getLongitude());
|
|
||||||
Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri);
|
|
||||||
|
|
||||||
if (mapIntent.resolveActivity(getActivity().getPackageManager()) != null) {
|
|
||||||
startActivity(mapIntent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnClick(R.id.link_preview_go_button)
|
|
||||||
void onReadArticleClick() {
|
|
||||||
openWebView(sitelinks.getWikipediaLink());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void openWebView(Uri link) {
|
|
||||||
Intent browserIntent = new Intent(Intent.ACTION_VIEW, link);
|
|
||||||
startActivity(browserIntent);
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnClick(R.id.emptyLayout)
|
|
||||||
void onCloseClicked() {
|
|
||||||
dismissAllowingStateLoss();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
package fr.free.nrw.commons.nearby;
|
package fr.free.nrw.commons.nearby;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
import android.support.v7.widget.LinearLayoutManager;
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
|
|
@ -11,17 +15,23 @@ import android.view.ViewGroup;
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.GsonBuilder;
|
import com.google.gson.GsonBuilder;
|
||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
import com.pedrogomez.renderers.RVRendererAdapter;
|
||||||
|
|
||||||
import java.lang.reflect.Type;
|
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 dagger.android.support.DaggerFragment;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
|
import fr.free.nrw.commons.contributions.ContributionController;
|
||||||
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;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
import static android.app.Activity.RESULT_OK;
|
||||||
|
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
|
||||||
|
|
||||||
public class NearbyListFragment extends DaggerFragment {
|
public class NearbyListFragment extends DaggerFragment {
|
||||||
private static final Type LIST_TYPE = new TypeToken<List<Place>>() {
|
private static final Type LIST_TYPE = new TypeToken<List<Place>>() {
|
||||||
}.getType();
|
}.getType();
|
||||||
|
|
@ -33,6 +43,7 @@ public class NearbyListFragment extends DaggerFragment {
|
||||||
|
|
||||||
private NearbyAdapterFactory adapterFactory;
|
private NearbyAdapterFactory adapterFactory;
|
||||||
private RecyclerView recyclerView;
|
private RecyclerView recyclerView;
|
||||||
|
private ContributionController controller;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
|
@ -40,15 +51,23 @@ public class NearbyListFragment extends DaggerFragment {
|
||||||
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,
|
||||||
Bundle savedInstanceState) {
|
Bundle savedInstanceState) {
|
||||||
Timber.d("NearbyListFragment created");
|
Timber.d("NearbyListFragment created");
|
||||||
View view = inflater.inflate(R.layout.fragment_nearby, container, false);
|
View view = inflater.inflate(R.layout.fragment_nearby, container, false);
|
||||||
recyclerView = (RecyclerView) view.findViewById(R.id.listView);
|
recyclerView = view.findViewById(R.id.listView);
|
||||||
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||||
adapterFactory = new NearbyAdapterFactory(place -> NearbyInfoDialog.showYourself(getActivity(), place));
|
|
||||||
|
controller = new ContributionController(this);
|
||||||
|
adapterFactory = new NearbyAdapterFactory(this, controller);
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,9 +75,19 @@ public class NearbyListFragment extends DaggerFragment {
|
||||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||||
// Check that this is the first time view is created,
|
// Check that this is the first time view is created,
|
||||||
// to avoid double list when screen orientation changed
|
// to avoid double list when screen orientation changed
|
||||||
|
Bundle bundle = this.getArguments();
|
||||||
|
recyclerView.setAdapter(adapterFactory.create(getPlaceListFromBundle(bundle)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateNearbyListSignificantly() {
|
||||||
|
Bundle bundle = this.getArguments();
|
||||||
|
adapterFactory.updateAdapterData(getPlaceListFromBundle(bundle),
|
||||||
|
(RVRendererAdapter<Place>) recyclerView.getAdapter());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Place> getPlaceListFromBundle(Bundle bundle) {
|
||||||
List<Place> placeList = Collections.emptyList();
|
List<Place> placeList = Collections.emptyList();
|
||||||
|
|
||||||
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);
|
||||||
|
|
@ -69,6 +98,46 @@ public class NearbyListFragment extends DaggerFragment {
|
||||||
placeList = NearbyController.loadAttractionsFromLocationToPlaces(curLatLng, placeList);
|
placeList = NearbyController.loadAttractionsFromLocationToPlaces(curLatLng, placeList);
|
||||||
}
|
}
|
||||||
|
|
||||||
recyclerView.setAdapter(adapterFactory.create(placeList));
|
return placeList;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@Override
|
||||||
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||||
|
Timber.d("onRequestPermissionsResult: req code = " + " perm = " + permissions + " grant =" + grantResults);
|
||||||
|
|
||||||
|
switch (requestCode) {
|
||||||
|
// 1 = "Read external storage" allowed when gallery selected
|
||||||
|
case 1: {
|
||||||
|
if (grantResults.length > 0 && grantResults[0] == PERMISSION_GRANTED) {
|
||||||
|
Timber.d("Call controller.startGalleryPick()");
|
||||||
|
controller.startGalleryPick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// 3 = "Write external storage" allowed when camera selected
|
||||||
|
case 3: {
|
||||||
|
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
Timber.d("Call controller.startCameraCapture()");
|
||||||
|
controller.startCameraCapture();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
|
|
||||||
|
if (resultCode == RESULT_OK) {
|
||||||
|
Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
|
||||||
|
requestCode, resultCode, data);
|
||||||
|
controller.handleImagePicked(requestCode, data, true);
|
||||||
|
} else {
|
||||||
|
Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
|
||||||
|
requestCode, resultCode, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,37 +1,118 @@
|
||||||
package fr.free.nrw.commons.nearby;
|
package fr.free.nrw.commons.nearby;
|
||||||
|
|
||||||
|
import android.animation.ObjectAnimator;
|
||||||
|
import android.animation.TypeEvaluator;
|
||||||
|
import android.animation.ValueAnimator;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
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.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.design.widget.BottomSheetBehavior;
|
||||||
|
import android.support.design.widget.CoordinatorLayout;
|
||||||
|
import android.support.design.widget.FloatingActionButton;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.KeyEvent;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.view.animation.Animation;
|
||||||
|
import android.view.animation.AnimationUtils;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.GsonBuilder;
|
import com.google.gson.GsonBuilder;
|
||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
import com.mapbox.mapboxsdk.Mapbox;
|
import com.mapbox.mapboxsdk.Mapbox;
|
||||||
|
import com.mapbox.mapboxsdk.annotations.Icon;
|
||||||
|
import com.mapbox.mapboxsdk.annotations.IconFactory;
|
||||||
|
import com.mapbox.mapboxsdk.annotations.Marker;
|
||||||
import com.mapbox.mapboxsdk.annotations.MarkerOptions;
|
import com.mapbox.mapboxsdk.annotations.MarkerOptions;
|
||||||
import com.mapbox.mapboxsdk.annotations.PolygonOptions;
|
import com.mapbox.mapboxsdk.annotations.PolygonOptions;
|
||||||
import com.mapbox.mapboxsdk.camera.CameraPosition;
|
import com.mapbox.mapboxsdk.camera.CameraPosition;
|
||||||
|
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
|
||||||
import com.mapbox.mapboxsdk.constants.Style;
|
import com.mapbox.mapboxsdk.constants.Style;
|
||||||
import com.mapbox.mapboxsdk.geometry.LatLng;
|
import com.mapbox.mapboxsdk.geometry.LatLng;
|
||||||
import com.mapbox.mapboxsdk.maps.MapView;
|
import com.mapbox.mapboxsdk.maps.MapView;
|
||||||
import com.mapbox.mapboxsdk.maps.MapboxMap;
|
import com.mapbox.mapboxsdk.maps.MapboxMap;
|
||||||
import com.mapbox.mapboxsdk.maps.MapboxMapOptions;
|
import com.mapbox.mapboxsdk.maps.MapboxMapOptions;
|
||||||
|
import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
|
||||||
import com.mapbox.services.android.telemetry.MapboxTelemetry;
|
import com.mapbox.services.android.telemetry.MapboxTelemetry;
|
||||||
|
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import fr.free.nrw.commons.R;
|
import javax.inject.Inject;
|
||||||
import fr.free.nrw.commons.utils.UriDeserializer;
|
import javax.inject.Named;
|
||||||
|
|
||||||
|
import dagger.android.support.DaggerFragment;
|
||||||
|
import fr.free.nrw.commons.R;
|
||||||
|
import fr.free.nrw.commons.contributions.ContributionController;
|
||||||
|
import fr.free.nrw.commons.utils.UriDeserializer;
|
||||||
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
import static android.app.Activity.RESULT_OK;
|
||||||
|
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
|
||||||
|
|
||||||
|
public class NearbyMapFragment extends DaggerFragment {
|
||||||
|
|
||||||
public class NearbyMapFragment extends android.support.v4.app.Fragment {
|
|
||||||
private MapView mapView;
|
private MapView mapView;
|
||||||
private List<NearbyBaseMarker> baseMarkerOptions;
|
private List<NearbyBaseMarker> baseMarkerOptions;
|
||||||
private fr.free.nrw.commons.location.LatLng curLatLng;
|
private fr.free.nrw.commons.location.LatLng curLatLng;
|
||||||
|
public fr.free.nrw.commons.location.LatLng[] boundaryCoordinates;
|
||||||
|
|
||||||
|
private View bottomSheetList;
|
||||||
|
private View bottomSheetDetails;
|
||||||
|
|
||||||
|
private BottomSheetBehavior bottomSheetListBehavior;
|
||||||
|
private BottomSheetBehavior bottomSheetDetailsBehavior;
|
||||||
|
private LinearLayout wikipediaButton;
|
||||||
|
private LinearLayout wikidataButton;
|
||||||
|
private LinearLayout directionsButton;
|
||||||
|
private LinearLayout commonsButton;
|
||||||
|
private FloatingActionButton fabPlus;
|
||||||
|
private FloatingActionButton fabCamera;
|
||||||
|
private FloatingActionButton fabGallery;
|
||||||
|
private FloatingActionButton fabRecenter;
|
||||||
|
private View transparentView;
|
||||||
|
private TextView description;
|
||||||
|
private TextView title;
|
||||||
|
private TextView distance;
|
||||||
|
private ImageView icon;
|
||||||
|
|
||||||
|
private TextView wikipediaButtonText;
|
||||||
|
private TextView wikidataButtonText;
|
||||||
|
private TextView commonsButtonText;
|
||||||
|
private TextView directionsButtonText;
|
||||||
|
|
||||||
|
private boolean isFabOpen = false;
|
||||||
|
private Animation rotate_backward;
|
||||||
|
private Animation fab_close;
|
||||||
|
private Animation fab_open;
|
||||||
|
private Animation rotate_forward;
|
||||||
|
private ContributionController controller;
|
||||||
|
|
||||||
|
private Place place;
|
||||||
|
private Marker selected;
|
||||||
|
private Marker currentLocationMarker;
|
||||||
|
private MapboxMap mapboxMap;
|
||||||
|
private PolygonOptions currentLocationPolygonOptions;
|
||||||
|
|
||||||
|
private boolean isBottomListSheetExpanded;
|
||||||
|
private final double CAMERA_TARGET_SHIFT_FACTOR = 0.06;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@Named("prefs")
|
||||||
|
SharedPreferences prefs;
|
||||||
|
@Inject
|
||||||
|
@Named("direct_nearby_upload_prefs")
|
||||||
|
SharedPreferences directPrefs;
|
||||||
|
|
||||||
public NearbyMapFragment() {
|
public NearbyMapFragment() {
|
||||||
}
|
}
|
||||||
|
|
@ -46,18 +127,24 @@ public class NearbyMapFragment extends android.support.v4.app.Fragment {
|
||||||
if (bundle != null) {
|
if (bundle != null) {
|
||||||
String gsonPlaceList = bundle.getString("PlaceList");
|
String gsonPlaceList = bundle.getString("PlaceList");
|
||||||
String gsonLatLng = bundle.getString("CurLatLng");
|
String gsonLatLng = bundle.getString("CurLatLng");
|
||||||
Type listType = new TypeToken<List<Place>>() {}.getType();
|
Type listType = new TypeToken<List<Place>>() {
|
||||||
|
}.getType();
|
||||||
|
String gsonBoundaryCoordinates = bundle.getString("BoundaryCoord");
|
||||||
List<Place> placeList = gson.fromJson(gsonPlaceList, listType);
|
List<Place> placeList = gson.fromJson(gsonPlaceList, listType);
|
||||||
Type curLatLngType = new TypeToken<fr.free.nrw.commons.location.LatLng>() {}.getType();
|
Type curLatLngType = new TypeToken<fr.free.nrw.commons.location.LatLng>() {
|
||||||
|
}.getType();
|
||||||
|
Type gsonBoundaryCoordinatesType = new TypeToken<fr.free.nrw.commons.location.LatLng[]>() {}.getType();
|
||||||
curLatLng = gson.fromJson(gsonLatLng, curLatLngType);
|
curLatLng = gson.fromJson(gsonLatLng, curLatLngType);
|
||||||
baseMarkerOptions = NearbyController
|
baseMarkerOptions = NearbyController
|
||||||
.loadAttractionsFromLocationToBaseMarkerOptions(curLatLng,
|
.loadAttractionsFromLocationToBaseMarkerOptions(curLatLng,
|
||||||
placeList,
|
placeList,
|
||||||
getActivity());
|
getActivity());
|
||||||
|
boundaryCoordinates = gson.fromJson(gsonBoundaryCoordinates, gsonBoundaryCoordinatesType);
|
||||||
}
|
}
|
||||||
Mapbox.getInstance(getActivity(),
|
Mapbox.getInstance(getActivity(),
|
||||||
getString(R.string.mapbox_commons_app_token));
|
getString(R.string.mapbox_commons_app_token));
|
||||||
MapboxTelemetry.getInstance().setTelemetryEnabled(false);
|
MapboxTelemetry.getInstance().setTelemetryEnabled(false);
|
||||||
|
setRetainInstance(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -73,9 +160,257 @@ public class NearbyMapFragment extends android.support.v4.app.Fragment {
|
||||||
return mapView;
|
return mapView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
this.getView().setFocusableInTouchMode(true);
|
||||||
|
this.getView().requestFocus();
|
||||||
|
this.getView().setOnKeyListener((v, keyCode, event) -> {
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||||
|
if (bottomSheetDetailsBehavior.getState() == BottomSheetBehavior
|
||||||
|
.STATE_EXPANDED) {
|
||||||
|
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||||
|
return true;
|
||||||
|
} else if (bottomSheetDetailsBehavior.getState() == BottomSheetBehavior
|
||||||
|
.STATE_COLLAPSED) {
|
||||||
|
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
|
mapView.getMapAsync(MapboxMap::deselectMarkers);
|
||||||
|
selected = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateMapSlightly() {
|
||||||
|
// Get arguments from bundle for new location
|
||||||
|
Bundle bundle = this.getArguments();
|
||||||
|
if (mapboxMap != null) {
|
||||||
|
Gson gson = new GsonBuilder()
|
||||||
|
.registerTypeAdapter(Uri.class, new UriDeserializer())
|
||||||
|
.create();
|
||||||
|
if (bundle != null) {
|
||||||
|
String gsonLatLng = bundle.getString("CurLatLng");
|
||||||
|
Type curLatLngType = new TypeToken<fr.free.nrw.commons.location.LatLng>() {}.getType();
|
||||||
|
curLatLng = gson.fromJson(gsonLatLng, curLatLngType);
|
||||||
|
}
|
||||||
|
updateMapToTrackPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateMapSignificantly() {
|
||||||
|
|
||||||
|
Bundle bundle = this.getArguments();
|
||||||
|
if (mapboxMap != null) {
|
||||||
|
if (bundle != null) {
|
||||||
|
Gson gson = new GsonBuilder()
|
||||||
|
.registerTypeAdapter(Uri.class, new UriDeserializer())
|
||||||
|
.create();
|
||||||
|
|
||||||
|
String gsonPlaceList = bundle.getString("PlaceList");
|
||||||
|
String gsonLatLng = bundle.getString("CurLatLng");
|
||||||
|
String gsonBoundaryCoordinates = bundle.getString("BoundaryCoord");
|
||||||
|
Type listType = new TypeToken<List<Place>>() {}.getType();
|
||||||
|
List<Place> placeList = gson.fromJson(gsonPlaceList, listType);
|
||||||
|
Type curLatLngType = new TypeToken<fr.free.nrw.commons.location.LatLng>() {}.getType();
|
||||||
|
Type gsonBoundaryCoordinatesType = new TypeToken<fr.free.nrw.commons.location.LatLng[]>() {}.getType();
|
||||||
|
curLatLng = gson.fromJson(gsonLatLng, curLatLngType);
|
||||||
|
baseMarkerOptions = NearbyController
|
||||||
|
.loadAttractionsFromLocationToBaseMarkerOptions(curLatLng,
|
||||||
|
placeList,
|
||||||
|
getActivity());
|
||||||
|
boundaryCoordinates = gson.fromJson(gsonBoundaryCoordinates, gsonBoundaryCoordinatesType);
|
||||||
|
}
|
||||||
|
mapboxMap.clear();
|
||||||
|
addCurrentLocationMarker(mapboxMap);
|
||||||
|
updateMapToTrackPosition();
|
||||||
|
addNearbyMarkerstoMapBoxMap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only update current position marker and camera view
|
||||||
|
private void updateMapToTrackPosition() {
|
||||||
|
|
||||||
|
if (currentLocationMarker != null) {
|
||||||
|
LatLng curMapBoxLatLng = new LatLng(curLatLng.getLatitude(),curLatLng.getLongitude());
|
||||||
|
ValueAnimator markerAnimator = ObjectAnimator.ofObject(currentLocationMarker, "position",
|
||||||
|
new LatLngEvaluator(), currentLocationMarker.getPosition(),
|
||||||
|
curMapBoxLatLng);
|
||||||
|
markerAnimator.setDuration(1000);
|
||||||
|
markerAnimator.start();
|
||||||
|
|
||||||
|
List<LatLng> circle = createCircleArray(curLatLng.getLatitude(), curLatLng.getLongitude(),
|
||||||
|
curLatLng.getAccuracy() * 2, 100);
|
||||||
|
if (currentLocationPolygonOptions != null){
|
||||||
|
mapboxMap.removePolygon(currentLocationPolygonOptions.getPolygon());
|
||||||
|
currentLocationPolygonOptions = new PolygonOptions()
|
||||||
|
.addAll(circle)
|
||||||
|
.strokeColor(Color.parseColor("#55000000"))
|
||||||
|
.fillColor(Color.parseColor("#11000000"));
|
||||||
|
mapboxMap.addPolygon(currentLocationPolygonOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make camera to follow user on location change
|
||||||
|
CameraPosition position = new CameraPosition.Builder()
|
||||||
|
.target(isBottomListSheetExpanded ?
|
||||||
|
new LatLng(curMapBoxLatLng.getLatitude()- CAMERA_TARGET_SHIFT_FACTOR,
|
||||||
|
curMapBoxLatLng.getLongitude())
|
||||||
|
: curMapBoxLatLng ) // Sets the new camera position
|
||||||
|
.zoom(mapboxMap.getCameraPosition().zoom) // Same zoom level
|
||||||
|
.build();
|
||||||
|
|
||||||
|
mapboxMap.animateCamera(CameraUpdateFactory
|
||||||
|
.newCameraPosition(position), 1000);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateMapCameraAccordingToBottomSheet(boolean isBottomListSheetExpanded) {
|
||||||
|
CameraPosition position;
|
||||||
|
this.isBottomListSheetExpanded = isBottomListSheetExpanded;
|
||||||
|
if (mapboxMap != null && curLatLng != null) {
|
||||||
|
if (isBottomListSheetExpanded) {
|
||||||
|
// Make camera to follow user on location change
|
||||||
|
position = new CameraPosition.Builder()
|
||||||
|
.target(new LatLng(curLatLng.getLatitude() - CAMERA_TARGET_SHIFT_FACTOR,
|
||||||
|
curLatLng.getLongitude())) // Sets the new camera target above
|
||||||
|
// current to make it visible when sheet is expanded
|
||||||
|
.zoom(11) // Same zoom level
|
||||||
|
.build();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Make camera to follow user on location change
|
||||||
|
position = new CameraPosition.Builder()
|
||||||
|
.target(new LatLng(curLatLng.getLatitude(),
|
||||||
|
curLatLng.getLongitude())) // Sets the new camera target to curLatLng
|
||||||
|
.zoom(mapboxMap.getCameraPosition().zoom) // Same zoom level
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
mapboxMap.animateCamera(CameraUpdateFactory
|
||||||
|
.newCameraPosition(position), 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initViews() {
|
||||||
|
bottomSheetList = getActivity().findViewById(R.id.bottom_sheet);
|
||||||
|
bottomSheetListBehavior = BottomSheetBehavior.from(bottomSheetList);
|
||||||
|
bottomSheetDetails = getActivity().findViewById(R.id.bottom_sheet_details);
|
||||||
|
bottomSheetDetailsBehavior = BottomSheetBehavior.from(bottomSheetDetails);
|
||||||
|
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
|
bottomSheetDetails.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
fabPlus = getActivity().findViewById(R.id.fab_plus);
|
||||||
|
fabCamera = getActivity().findViewById(R.id.fab_camera);
|
||||||
|
fabGallery = getActivity().findViewById(R.id.fab_galery);
|
||||||
|
fabRecenter = getActivity().findViewById(R.id.fab_recenter);
|
||||||
|
|
||||||
|
fab_open = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_open);
|
||||||
|
fab_close = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_close);
|
||||||
|
rotate_forward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_forward);
|
||||||
|
rotate_backward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_backward);
|
||||||
|
|
||||||
|
transparentView = getActivity().findViewById(R.id.transparentView);
|
||||||
|
|
||||||
|
description = getActivity().findViewById(R.id.description);
|
||||||
|
title = getActivity().findViewById(R.id.title);
|
||||||
|
distance = getActivity().findViewById(R.id.category);
|
||||||
|
icon = getActivity().findViewById(R.id.icon);
|
||||||
|
|
||||||
|
wikidataButton = getActivity().findViewById(R.id.wikidataButton);
|
||||||
|
wikipediaButton = getActivity().findViewById(R.id.wikipediaButton);
|
||||||
|
directionsButton = getActivity().findViewById(R.id.directionsButton);
|
||||||
|
commonsButton = getActivity().findViewById(R.id.commonsButton);
|
||||||
|
|
||||||
|
wikidataButtonText = getActivity().findViewById(R.id.wikidataButtonText);
|
||||||
|
wikipediaButtonText = getActivity().findViewById(R.id.wikipediaButtonText);
|
||||||
|
directionsButtonText = getActivity().findViewById(R.id.directionsButtonText);
|
||||||
|
commonsButtonText = getActivity().findViewById(R.id.commonsButtonText);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setListeners() {
|
||||||
|
fabPlus.setOnClickListener(view -> animateFAB(isFabOpen));
|
||||||
|
|
||||||
|
bottomSheetDetails.setOnClickListener(view -> {
|
||||||
|
if (bottomSheetDetailsBehavior.getState() == BottomSheetBehavior.STATE_COLLAPSED) {
|
||||||
|
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
|
||||||
|
} else {
|
||||||
|
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fabRecenter.setOnClickListener(view -> {
|
||||||
|
if (curLatLng != null) {
|
||||||
|
mapView.getMapAsync(mapboxMap -> {
|
||||||
|
CameraPosition position = new CameraPosition.Builder()
|
||||||
|
.target(new LatLng(curLatLng.getLatitude(), curLatLng.getLongitude())) // Sets the new camera position
|
||||||
|
.zoom(11) // Sets the zoom
|
||||||
|
.build(); // Creates a CameraPosition from the builder
|
||||||
|
|
||||||
|
mapboxMap.animateCamera(CameraUpdateFactory
|
||||||
|
.newCameraPosition(position), 1000);
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bottomSheetDetailsBehavior.setBottomSheetCallback(new BottomSheetBehavior
|
||||||
|
.BottomSheetCallback() {
|
||||||
|
@Override
|
||||||
|
public void onStateChanged(@NonNull View bottomSheet, int newState) {
|
||||||
|
prepareViewsForSheetPosition(newState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSlide(@NonNull View bottomSheet, float slideOffset) {
|
||||||
|
if (slideOffset >= 0) {
|
||||||
|
transparentView.setAlpha(slideOffset);
|
||||||
|
if (slideOffset == 1) {
|
||||||
|
transparentView.setClickable(true);
|
||||||
|
} else if (slideOffset == 0) {
|
||||||
|
transparentView.setClickable(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bottomSheetListBehavior.setBottomSheetCallback(new BottomSheetBehavior
|
||||||
|
.BottomSheetCallback() {
|
||||||
|
@Override
|
||||||
|
public void onStateChanged(@NonNull View bottomSheet, int newState) {
|
||||||
|
if (newState == BottomSheetBehavior.STATE_EXPANDED) {
|
||||||
|
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
|
updateMapCameraAccordingToBottomSheet(true);
|
||||||
|
} else {
|
||||||
|
updateMapCameraAccordingToBottomSheet(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSlide(@NonNull View bottomSheet, float slideOffset) {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove texts if it doesnt fit
|
||||||
|
if (wikipediaButtonText.getLineCount() > 1
|
||||||
|
|| wikidataButtonText.getLineCount() > 1
|
||||||
|
|| commonsButtonText.getLineCount() > 1
|
||||||
|
|| directionsButtonText.getLineCount() > 1) {
|
||||||
|
wikipediaButtonText.setVisibility(View.GONE);
|
||||||
|
wikidataButtonText.setVisibility(View.GONE);
|
||||||
|
commonsButtonText.setVisibility(View.GONE);
|
||||||
|
directionsButtonText.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
|
@ -84,21 +419,13 @@ public class NearbyMapFragment extends android.support.v4.app.Fragment {
|
||||||
// create map
|
// create map
|
||||||
mapView = new MapView(getActivity(), options);
|
mapView = new MapView(getActivity(), options);
|
||||||
mapView.onCreate(savedInstanceState);
|
mapView.onCreate(savedInstanceState);
|
||||||
mapView.getMapAsync(mapboxMap -> {
|
mapView.getMapAsync(new OnMapReadyCallback() {
|
||||||
mapboxMap.addMarkers(baseMarkerOptions);
|
@Override
|
||||||
|
public void onMapReady(MapboxMap mapboxMap) {
|
||||||
mapboxMap.setOnMarkerClickListener(marker -> {
|
NearbyMapFragment.this.mapboxMap = mapboxMap;
|
||||||
if (marker instanceof NearbyMarker) {
|
updateMapSignificantly();
|
||||||
NearbyMarker nearbyMarker = (NearbyMarker) marker;
|
}
|
||||||
Place place = nearbyMarker.getNearbyBaseMarker().getPlace();
|
|
||||||
NearbyInfoDialog.showYourself(getActivity(), place);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
addCurrentLocationMarker(mapboxMap);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
mapView.setStyleUrl("asset://mapstyle.json");
|
mapView.setStyleUrl("asset://mapstyle.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,23 +434,67 @@ public class NearbyMapFragment extends android.support.v4.app.Fragment {
|
||||||
* circle which uses the accuracy * 2, to draw a circle
|
* circle which uses the accuracy * 2, to draw a circle
|
||||||
* which represents the user's position with an accuracy
|
* which represents the user's position with an accuracy
|
||||||
* of 95%.
|
* of 95%.
|
||||||
|
*
|
||||||
|
* Should be called only on creation of mapboxMap, there
|
||||||
|
* is other method to update markers location with users
|
||||||
|
* move.
|
||||||
*/
|
*/
|
||||||
private void addCurrentLocationMarker(MapboxMap mapboxMap) {
|
private void addCurrentLocationMarker(MapboxMap mapboxMap) {
|
||||||
MarkerOptions currentLocationMarker = new MarkerOptions()
|
if (currentLocationMarker != null) {
|
||||||
|
currentLocationMarker.remove(); // Remove previous marker, we are not Hansel and Gretel
|
||||||
|
}
|
||||||
|
|
||||||
|
Icon icon = IconFactory.getInstance(getContext()).fromResource(R.drawable.current_location_marker);
|
||||||
|
|
||||||
|
MarkerOptions currentLocationMarkerOptions = new MarkerOptions()
|
||||||
.position(new LatLng(curLatLng.getLatitude(), curLatLng.getLongitude()));
|
.position(new LatLng(curLatLng.getLatitude(), curLatLng.getLongitude()));
|
||||||
mapboxMap.addMarker(currentLocationMarker);
|
currentLocationMarkerOptions.setIcon(icon); // Set custom icon
|
||||||
|
|
||||||
|
currentLocationMarker = mapboxMap.addMarker(currentLocationMarkerOptions);
|
||||||
|
|
||||||
List<LatLng> circle = createCircleArray(curLatLng.getLatitude(), curLatLng.getLongitude(),
|
List<LatLng> circle = createCircleArray(curLatLng.getLatitude(), curLatLng.getLongitude(),
|
||||||
curLatLng.getAccuracy() * 2, 100);
|
curLatLng.getAccuracy() * 2, 100);
|
||||||
|
|
||||||
mapboxMap.addPolygon(
|
currentLocationPolygonOptions = new PolygonOptions()
|
||||||
new PolygonOptions()
|
.addAll(circle)
|
||||||
.addAll(circle)
|
.strokeColor(Color.parseColor("#55000000"))
|
||||||
.strokeColor(Color.parseColor("#55000000"))
|
.fillColor(Color.parseColor("#11000000"));
|
||||||
.fillColor(Color.parseColor("#11000000"))
|
mapboxMap.addPolygon(currentLocationPolygonOptions);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void addNearbyMarkerstoMapBoxMap() {
|
||||||
|
|
||||||
|
mapboxMap.addMarkers(baseMarkerOptions);
|
||||||
|
mapboxMap.setOnInfoWindowCloseListener(marker -> {
|
||||||
|
if (marker == selected) {
|
||||||
|
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mapView.getMapAsync(mapboxMap -> {
|
||||||
|
mapboxMap.addMarkers(baseMarkerOptions);
|
||||||
|
fabRecenter.setVisibility(View.VISIBLE);
|
||||||
|
mapboxMap.setOnInfoWindowCloseListener(marker -> {
|
||||||
|
if (marker == selected) {
|
||||||
|
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mapboxMap.setOnMarkerClickListener(marker -> {
|
||||||
|
if (marker instanceof NearbyMarker) {
|
||||||
|
this.selected = marker;
|
||||||
|
NearbyMarker nearbyMarker = (NearbyMarker) marker;
|
||||||
|
Place place = nearbyMarker.getNearbyBaseMarker().getPlace();
|
||||||
|
passInfoToSheet(place);
|
||||||
|
bottomSheetListBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
|
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a series of points that create a circle on the map.
|
* Creates a series of points that create a circle on the map.
|
||||||
* Takes the center latitude, center longitude of the circle,
|
* Takes the center latitude, center longitude of the circle,
|
||||||
|
|
@ -145,10 +516,230 @@ public class NearbyMapFragment extends android.support.v4.app.Fragment {
|
||||||
double nodeLatitude = centerLat + radiusLat * Math.sin(theta);
|
double nodeLatitude = centerLat + radiusLat * Math.sin(theta);
|
||||||
circle.add(new LatLng(nodeLatitude, nodeLongitude));
|
circle.add(new LatLng(nodeLatitude, nodeLongitude));
|
||||||
}
|
}
|
||||||
|
|
||||||
return circle;
|
return circle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void prepareViewsForSheetPosition(int bottomSheetState) {
|
||||||
|
|
||||||
|
switch (bottomSheetState) {
|
||||||
|
case (BottomSheetBehavior.STATE_COLLAPSED):
|
||||||
|
closeFabs(isFabOpen);
|
||||||
|
if (!fabPlus.isShown()) showFAB();
|
||||||
|
this.getView().requestFocus();
|
||||||
|
break;
|
||||||
|
case (BottomSheetBehavior.STATE_EXPANDED):
|
||||||
|
this.getView().requestFocus();
|
||||||
|
break;
|
||||||
|
case (BottomSheetBehavior.STATE_HIDDEN):
|
||||||
|
mapView.getMapAsync(MapboxMap::deselectMarkers);
|
||||||
|
transparentView.setClickable(false);
|
||||||
|
transparentView.setAlpha(0);
|
||||||
|
closeFabs(isFabOpen);
|
||||||
|
hideFAB();
|
||||||
|
this.getView().requestFocus();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideFAB() {
|
||||||
|
|
||||||
|
removeAnchorFromFABs(fabPlus);
|
||||||
|
fabPlus.hide();
|
||||||
|
|
||||||
|
removeAnchorFromFABs(fabCamera);
|
||||||
|
fabCamera.hide();
|
||||||
|
|
||||||
|
removeAnchorFromFABs(fabGallery);
|
||||||
|
fabGallery.hide();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* We are not able to hide FABs without removing anchors, this method removes anchors
|
||||||
|
* */
|
||||||
|
private void removeAnchorFromFABs(FloatingActionButton floatingActionButton) {
|
||||||
|
//get rid of anchors
|
||||||
|
//Somehow this was the only way https://stackoverflow.com/questions/32732932
|
||||||
|
// /floatingactionbutton-visible-for-sometime-even-if-visibility-is-set-to-gone
|
||||||
|
CoordinatorLayout.LayoutParams param = (CoordinatorLayout.LayoutParams) floatingActionButton
|
||||||
|
.getLayoutParams();
|
||||||
|
param.setAnchorId(View.NO_ID);
|
||||||
|
// If we don't set them to zero, then they become visible for a moment on upper left side
|
||||||
|
param.width = 0;
|
||||||
|
param.height = 0;
|
||||||
|
floatingActionButton.setLayoutParams(param);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showFAB() {
|
||||||
|
|
||||||
|
addAnchorToBigFABs(fabPlus, getActivity().findViewById(R.id.bottom_sheet_details).getId());
|
||||||
|
fabPlus.show();
|
||||||
|
|
||||||
|
addAnchorToSmallFABs(fabGallery, getActivity().findViewById(R.id.empty_view).getId());
|
||||||
|
|
||||||
|
addAnchorToSmallFABs(fabCamera, getActivity().findViewById(R.id.empty_view1).getId());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add amnchors back before making them visible again.
|
||||||
|
* */
|
||||||
|
private void addAnchorToBigFABs(FloatingActionButton floatingActionButton, int anchorID) {
|
||||||
|
CoordinatorLayout.LayoutParams params = new CoordinatorLayout.LayoutParams
|
||||||
|
(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
params.setAnchorId(anchorID);
|
||||||
|
params.anchorGravity = Gravity.TOP|Gravity.RIGHT|Gravity.END;
|
||||||
|
floatingActionButton.setLayoutParams(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add amnchors back before making them visible again. Big and small fabs have different anchor
|
||||||
|
* gravities, therefore the are two methods.
|
||||||
|
* */
|
||||||
|
private void addAnchorToSmallFABs(FloatingActionButton floatingActionButton, int anchorID) {
|
||||||
|
CoordinatorLayout.LayoutParams params = new CoordinatorLayout.LayoutParams
|
||||||
|
(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
params.setAnchorId(anchorID);
|
||||||
|
params.anchorGravity = Gravity.CENTER_HORIZONTAL;
|
||||||
|
floatingActionButton.setLayoutParams(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void passInfoToSheet(Place place) {
|
||||||
|
this.place = place;
|
||||||
|
wikipediaButton.setEnabled(place.hasWikipediaLink());
|
||||||
|
wikipediaButton.setOnClickListener(view -> openWebView(place.siteLinks.getWikipediaLink()));
|
||||||
|
|
||||||
|
wikidataButton.setEnabled(place.hasWikidataLink());
|
||||||
|
wikidataButton.setOnClickListener(view -> openWebView(place.siteLinks.getWikidataLink()));
|
||||||
|
|
||||||
|
directionsButton.setOnClickListener(view -> {
|
||||||
|
//Open map app at given position
|
||||||
|
Intent mapIntent = new Intent(Intent.ACTION_VIEW, place.location.getGmmIntentUri());
|
||||||
|
if (mapIntent.resolveActivity(getActivity().getPackageManager()) != null) {
|
||||||
|
startActivity(mapIntent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
commonsButton.setEnabled(place.hasCommonsLink());
|
||||||
|
commonsButton.setOnClickListener(view -> openWebView(place.siteLinks.getCommonsLink()));
|
||||||
|
|
||||||
|
icon.setImageResource(place.getLabel().getIcon());
|
||||||
|
|
||||||
|
title.setText(place.name);
|
||||||
|
distance.setText(place.distance);
|
||||||
|
description.setText(place.getLongDescription());
|
||||||
|
title.setText(place.name.toString());
|
||||||
|
distance.setText(place.distance.toString());
|
||||||
|
|
||||||
|
fabCamera.setOnClickListener(view -> {
|
||||||
|
if (fabCamera.isShown()) {
|
||||||
|
Timber.d("Camera button tapped. Image title: " + place.getName() + "Image desc: " + place.getLongDescription());
|
||||||
|
controller = new ContributionController(this);
|
||||||
|
|
||||||
|
DirectUpload directUpload = new DirectUpload(this, controller);
|
||||||
|
storeSharedPrefs();
|
||||||
|
directUpload.initiateCameraUpload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fabGallery.setOnClickListener(view -> {
|
||||||
|
if (fabGallery.isShown()) {
|
||||||
|
Timber.d("Gallery button tapped. Image title: " + place.getName() + "Image desc: " + place.getLongDescription());
|
||||||
|
controller = new ContributionController(this);
|
||||||
|
|
||||||
|
DirectUpload directUpload = new DirectUpload(this, controller);
|
||||||
|
storeSharedPrefs();
|
||||||
|
directUpload.initiateGalleryUpload();
|
||||||
|
|
||||||
|
//TODO: App crashes after image upload completes
|
||||||
|
//TODO: Handle onRequestPermissionsResult
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void storeSharedPrefs() {
|
||||||
|
SharedPreferences.Editor editor = directPrefs.edit();
|
||||||
|
editor.putString("Title", place.getName());
|
||||||
|
editor.putString("Desc", place.getLongDescription());
|
||||||
|
editor.putString("Category", place.getCategory());
|
||||||
|
editor.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||||
|
Timber.d("onRequestPermissionsResult: req code = " + " perm = " + permissions + " grant =" + grantResults);
|
||||||
|
|
||||||
|
switch (requestCode) {
|
||||||
|
// 1 = "Read external storage" allowed when gallery selected
|
||||||
|
case 1: {
|
||||||
|
if (grantResults.length > 0 && grantResults[0] == PERMISSION_GRANTED) {
|
||||||
|
Timber.d("Call controller.startGalleryPick()");
|
||||||
|
controller.startGalleryPick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// 3 = "Write external storage" allowed when camera selected
|
||||||
|
case 3: {
|
||||||
|
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
Timber.d("Call controller.startCameraCapture()");
|
||||||
|
controller.startCameraCapture();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
|
|
||||||
|
if (resultCode == RESULT_OK) {
|
||||||
|
Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
|
||||||
|
requestCode, resultCode, data);
|
||||||
|
controller.handleImagePicked(requestCode, data, true);
|
||||||
|
} else {
|
||||||
|
Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
|
||||||
|
requestCode, resultCode, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openWebView(Uri link) {
|
||||||
|
Intent browserIntent = new Intent(Intent.ACTION_VIEW, link);
|
||||||
|
startActivity(browserIntent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void animateFAB(boolean isFabOpen) {
|
||||||
|
this.isFabOpen = !isFabOpen;
|
||||||
|
if (fabPlus.isShown()){
|
||||||
|
if (isFabOpen) {
|
||||||
|
fabPlus.startAnimation(rotate_backward);
|
||||||
|
fabCamera.startAnimation(fab_close);
|
||||||
|
fabGallery.startAnimation(fab_close);
|
||||||
|
fabCamera.hide();
|
||||||
|
fabGallery.hide();
|
||||||
|
} else {
|
||||||
|
fabPlus.startAnimation(rotate_forward);
|
||||||
|
fabCamera.startAnimation(fab_open);
|
||||||
|
fabGallery.startAnimation(fab_open);
|
||||||
|
fabCamera.show();
|
||||||
|
fabGallery.show();
|
||||||
|
}
|
||||||
|
this.isFabOpen=!isFabOpen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeFabs ( boolean isFabOpen){
|
||||||
|
if (isFabOpen) {
|
||||||
|
fabPlus.startAnimation(rotate_backward);
|
||||||
|
fabCamera.startAnimation(fab_close);
|
||||||
|
fabGallery.startAnimation(fab_close);
|
||||||
|
fabCamera.hide();
|
||||||
|
fabGallery.hide();
|
||||||
|
this.isFabOpen = !isFabOpen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStart() {
|
public void onStart() {
|
||||||
if (mapView != null) {
|
if (mapView != null) {
|
||||||
|
|
@ -167,10 +758,14 @@ public class NearbyMapFragment extends android.support.v4.app.Fragment {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
|
super.onResume();
|
||||||
if (mapView != null) {
|
if (mapView != null) {
|
||||||
mapView.onResume();
|
mapView.onResume();
|
||||||
}
|
}
|
||||||
super.onResume();
|
initViews();
|
||||||
|
setListeners();
|
||||||
|
transparentView.setClickable(false);
|
||||||
|
transparentView.setAlpha(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -188,4 +783,19 @@ public class NearbyMapFragment extends android.support.v4.app.Fragment {
|
||||||
}
|
}
|
||||||
super.onDestroyView();
|
super.onDestroyView();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class LatLngEvaluator implements TypeEvaluator<LatLng> {
|
||||||
|
// Method is used to interpolate the marker animation.
|
||||||
|
private LatLng latLng = new LatLng();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LatLng evaluate(float fraction, LatLng startValue, LatLng endValue) {
|
||||||
|
latLng.setLatitude(startValue.getLatitude()
|
||||||
|
+ ((endValue.getLatitude() - startValue.getLatitude()) * fraction));
|
||||||
|
latLng.setLongitude(startValue.getLongitude()
|
||||||
|
+ ((endValue.getLongitude() - startValue.getLongitude()) * fraction));
|
||||||
|
return latLng;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package fr.free.nrw.commons.nearby;
|
package fr.free.nrw.commons.nearby;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.StrictMode;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
@ -9,6 +8,7 @@ import java.io.InputStreamReader;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.net.URLConnection;
|
import java.net.URLConnection;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
@ -30,11 +30,10 @@ public class NearbyPlaces {
|
||||||
private static final Uri WIKIDATA_QUERY_UI_URL = Uri.parse("https://query.wikidata.org/");
|
private static final Uri WIKIDATA_QUERY_UI_URL = Uri.parse("https://query.wikidata.org/");
|
||||||
private final String wikidataQuery;
|
private final String wikidataQuery;
|
||||||
private double radius = INITIAL_RADIUS;
|
private double radius = INITIAL_RADIUS;
|
||||||
private List<Place> places;
|
|
||||||
|
|
||||||
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);
|
||||||
|
|
@ -102,13 +101,17 @@ public class NearbyPlaces {
|
||||||
}
|
}
|
||||||
|
|
||||||
String[] fields = line.split("\t");
|
String[] fields = line.split("\t");
|
||||||
|
Timber.v("Fields: " + Arrays.toString(fields));
|
||||||
String point = fields[0];
|
String point = fields[0];
|
||||||
|
String wikiDataLink = Utils.stripLocalizedString(fields[1]);
|
||||||
String name = Utils.stripLocalizedString(fields[2]);
|
String name = Utils.stripLocalizedString(fields[2]);
|
||||||
String type = Utils.stripLocalizedString(fields[4]);
|
String type = Utils.stripLocalizedString(fields[4]);
|
||||||
|
String icon = fields[5];
|
||||||
String wikipediaSitelink = Utils.stripLocalizedString(fields[7]);
|
String wikipediaSitelink = Utils.stripLocalizedString(fields[7]);
|
||||||
String commonsSitelink = Utils.stripLocalizedString(fields[8]);
|
String commonsSitelink = Utils.stripLocalizedString(fields[8]);
|
||||||
String wikiDataLink = Utils.stripLocalizedString(fields[1]);
|
String category = Utils.stripLocalizedString(fields[9]);
|
||||||
String icon = fields[5];
|
|
||||||
|
Timber.v("Name: " + name + ", type: " + type + ", category: " + category + ", wikipediaSitelink: " + wikipediaSitelink + ", commonsSitelink: " + commonsSitelink);
|
||||||
|
|
||||||
double latitude;
|
double latitude;
|
||||||
double longitude;
|
double longitude;
|
||||||
|
|
@ -126,10 +129,11 @@ 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),
|
||||||
|
category,
|
||||||
new Sitelinks.Builder()
|
new Sitelinks.Builder()
|
||||||
.setWikipediaLink(wikipediaSitelink)
|
.setWikipediaLink(wikipediaSitelink)
|
||||||
.setCommonsLink(commonsSitelink)
|
.setCommonsLink(commonsSitelink)
|
||||||
|
|
@ -141,66 +145,4 @@ public class NearbyPlaces {
|
||||||
|
|
||||||
return places;
|
return places;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Place> getFromWikiNeedsPictures() {
|
|
||||||
if (places != null) {
|
|
||||||
return places;
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
places = new ArrayList<>();
|
|
||||||
StrictMode.ThreadPolicy policy
|
|
||||||
= new StrictMode.ThreadPolicy.Builder().permitAll().build();
|
|
||||||
StrictMode.setThreadPolicy(policy);
|
|
||||||
|
|
||||||
URL file = new URL("https://tools.wmflabs.org/wiki-needs-pictures/data/data.csv");
|
|
||||||
|
|
||||||
BufferedReader in = new BufferedReader(new InputStreamReader(file.openStream()));
|
|
||||||
|
|
||||||
boolean firstLine = true;
|
|
||||||
String line;
|
|
||||||
Timber.d("Reading from CSV file...");
|
|
||||||
|
|
||||||
while ((line = in.readLine()) != null) {
|
|
||||||
|
|
||||||
// Skip CSV header.
|
|
||||||
if (firstLine) {
|
|
||||||
firstLine = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
String[] fields = line.split(",");
|
|
||||||
String name = Utils.stripLocalizedString(fields[0]);
|
|
||||||
|
|
||||||
double latitude;
|
|
||||||
double longitude;
|
|
||||||
try {
|
|
||||||
latitude = Double.parseDouble(fields[1]);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
latitude = 0;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
longitude = Double.parseDouble(fields[2]);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
longitude = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
String type = fields[3];
|
|
||||||
|
|
||||||
places.add(new Place(
|
|
||||||
name,
|
|
||||||
Place.Description.fromText(type), // list
|
|
||||||
type, // details
|
|
||||||
null,
|
|
||||||
new LatLng(latitude, longitude, 0),
|
|
||||||
new Sitelinks.Builder().build()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
in.close();
|
|
||||||
|
|
||||||
} catch (IOException e) {
|
|
||||||
Timber.d(e.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return places;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,31 @@
|
||||||
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.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
import dagger.android.support.DaggerFragment;
|
import dagger.android.support.AndroidSupportInjection;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tells user that Nearby Places cannot be displayed if location permissions are denied
|
* Tells user that Nearby Places cannot be displayed if location permissions are denied
|
||||||
*/
|
*/
|
||||||
public class NoPermissionsFragment extends DaggerFragment {
|
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,10 +13,11 @@ 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;
|
||||||
|
private final String category;
|
||||||
|
|
||||||
public Bitmap image;
|
public Bitmap image;
|
||||||
public Bitmap secondaryImage;
|
public Bitmap secondaryImage;
|
||||||
|
|
@ -24,24 +25,43 @@ 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, String category, 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.category = category;
|
||||||
this.siteLinks = siteLinks;
|
this.siteLinks = siteLinks;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Description getDescription() {
|
public String getName() { return name; }
|
||||||
return description;
|
|
||||||
|
public Label getLabel() {
|
||||||
|
return label;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getLongDescription() { return longDescription; }
|
||||||
|
|
||||||
|
public String getCategory() {return category; }
|
||||||
|
|
||||||
public void setDistance(String distance) {
|
public void setDistance(String distance) {
|
||||||
this.distance = distance;
|
this.distance = distance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean hasWikipediaLink() {
|
||||||
|
return !(siteLinks == null || Uri.EMPTY.equals(siteLinks.getWikipediaLink()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasWikidataLink() {
|
||||||
|
return !(siteLinks == null || Uri.EMPTY.equals(siteLinks.getWikidataLink()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasCommonsLink() {
|
||||||
|
return !(siteLinks == null || Uri.EMPTY.equals(siteLinks.getCommonsLink()));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (o instanceof Place) {
|
if (o instanceof Place) {
|
||||||
|
|
@ -67,10 +87,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 +113,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 +139,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,79 @@
|
||||||
package fr.free.nrw.commons.nearby;
|
package fr.free.nrw.commons.nearby;
|
||||||
|
|
||||||
import android.support.annotation.NonNull;
|
import android.content.Intent;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.support.v4.app.Fragment;
|
||||||
|
import android.support.transition.TransitionManager;
|
||||||
|
import android.support.v7.widget.PopupMenu;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import com.pedrogomez.renderers.Renderer;
|
import com.pedrogomez.renderers.Renderer;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
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.contributions.ContributionController;
|
||||||
|
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||||
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
public class PlaceRenderer extends Renderer<Place> {
|
||||||
|
|
||||||
class PlaceRenderer extends Renderer<Place> {
|
|
||||||
@BindView(R.id.tvName) TextView tvName;
|
@BindView(R.id.tvName) TextView tvName;
|
||||||
@BindView(R.id.tvDesc) TextView tvDesc;
|
@BindView(R.id.tvDesc) TextView tvDesc;
|
||||||
@BindView(R.id.distance) TextView distance;
|
@BindView(R.id.distance) TextView distance;
|
||||||
@BindView(R.id.icon) ImageView icon;
|
@BindView(R.id.icon) ImageView icon;
|
||||||
private final PlaceClickedListener listener;
|
@BindView(R.id.buttonLayout) LinearLayout buttonLayout;
|
||||||
|
@BindView(R.id.cameraButton) LinearLayout cameraButton;
|
||||||
|
|
||||||
PlaceRenderer(@NonNull PlaceClickedListener listener) {
|
@BindView(R.id.galleryButton) LinearLayout galleryButton;
|
||||||
this.listener = listener;
|
@BindView(R.id.directionsButton) LinearLayout directionsButton;
|
||||||
|
@BindView(R.id.iconOverflow) LinearLayout iconOverflow;
|
||||||
|
@BindView(R.id.cameraButtonText) TextView cameraButtonText;
|
||||||
|
@BindView(R.id.galleryButtonText) TextView galleryButtonText;
|
||||||
|
|
||||||
|
@BindView(R.id.directionsButtonText) TextView directionsButtonText;
|
||||||
|
@BindView(R.id.iconOverflowText) TextView iconOverflowText;
|
||||||
|
|
||||||
|
private View view;
|
||||||
|
private static ArrayList<LinearLayout> openedItems;
|
||||||
|
private Place place;
|
||||||
|
|
||||||
|
private Fragment fragment;
|
||||||
|
private ContributionController controller;
|
||||||
|
|
||||||
|
|
||||||
|
@Inject @Named("prefs") SharedPreferences prefs;
|
||||||
|
@Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs;
|
||||||
|
|
||||||
|
public PlaceRenderer(){
|
||||||
|
openedItems = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlaceRenderer(Fragment fragment, ContributionController controller) {
|
||||||
|
this.fragment = fragment;
|
||||||
|
this.controller = controller;
|
||||||
|
openedItems = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected View inflate(LayoutInflater layoutInflater, ViewGroup viewGroup) {
|
protected View inflate(LayoutInflater layoutInflater, ViewGroup viewGroup) {
|
||||||
return layoutInflater.inflate(R.layout.item_place, viewGroup, false);
|
view = layoutInflater.inflate(R.layout.item_place, viewGroup, false);
|
||||||
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -36,23 +83,129 @@ class PlaceRenderer extends Renderer<Place> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void hookListeners(View view) {
|
protected void hookListeners(View view) {
|
||||||
view.setOnClickListener(v -> listener.placeClicked(getContent()));
|
|
||||||
|
final View.OnClickListener listener = view12 -> {
|
||||||
|
Log.d("Renderer", "clicked");
|
||||||
|
TransitionManager.beginDelayedTransition(buttonLayout);
|
||||||
|
|
||||||
|
if(buttonLayout.isShown()){
|
||||||
|
closeLayout(buttonLayout);
|
||||||
|
}else {
|
||||||
|
openLayout(buttonLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
view.setOnClickListener(listener);
|
||||||
|
view.requestFocus();
|
||||||
|
view.setOnFocusChangeListener((view1, hasFocus) -> {
|
||||||
|
if (!hasFocus && buttonLayout.isShown()) {
|
||||||
|
closeLayout(buttonLayout);
|
||||||
|
} else if (hasFocus && !buttonLayout.isShown()) {
|
||||||
|
listener.onClick(view1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cameraButton.setOnClickListener(view2 -> {
|
||||||
|
Timber.d("Camera button tapped. Image title: " + place.getName() + "Image desc: " + place.getLongDescription());
|
||||||
|
DirectUpload directUpload = new DirectUpload(fragment, controller);
|
||||||
|
storeSharedPrefs();
|
||||||
|
directUpload.initiateCameraUpload();
|
||||||
|
});
|
||||||
|
|
||||||
|
galleryButton.setOnClickListener(view3 -> {
|
||||||
|
Timber.d("Gallery button tapped. Image title: " + place.getName() + "Image desc: " + place.getLongDescription());
|
||||||
|
DirectUpload directUpload = new DirectUpload(fragment, controller);
|
||||||
|
storeSharedPrefs();
|
||||||
|
directUpload.initiateGalleryUpload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void storeSharedPrefs() {
|
||||||
|
SharedPreferences.Editor editor = directPrefs.edit();
|
||||||
|
Timber.d("directPrefs stored");
|
||||||
|
editor.putString("Title", place.getName());
|
||||||
|
editor.putString("Desc", place.getLongDescription());
|
||||||
|
editor.putString("Category", place.getCategory());
|
||||||
|
editor.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeLayout(LinearLayout buttonLayout){
|
||||||
|
buttonLayout.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openLayout(LinearLayout buttonLayout){
|
||||||
|
buttonLayout.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void render() {
|
public void render() {
|
||||||
Place place = getContent();
|
ApplicationlessInjection.getInstance(getContext().getApplicationContext())
|
||||||
|
.getCommonsApplicationComponent().inject(this);
|
||||||
|
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.setVisibility(View.INVISIBLE);
|
||||||
}
|
}
|
||||||
tvDesc.setText(descriptionText);
|
tvDesc.setText(descriptionText);
|
||||||
distance.setText(place.distance);
|
distance.setText(place.distance);
|
||||||
icon.setImageResource(place.getDescription().getIcon());
|
icon.setImageResource(place.getLabel().getIcon());
|
||||||
|
|
||||||
|
directionsButton.setOnClickListener(view -> {
|
||||||
|
//Open map app at given position
|
||||||
|
Intent mapIntent = new Intent(Intent.ACTION_VIEW, place.location.getGmmIntentUri());
|
||||||
|
if (mapIntent.resolveActivity(view.getContext().getPackageManager()) != null) {
|
||||||
|
view.getContext().startActivity(mapIntent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
iconOverflow.setVisibility(showMenu() ? View.VISIBLE : View.GONE);
|
||||||
|
iconOverflow.setOnClickListener(v -> popupMenuListener());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PlaceClickedListener {
|
private void popupMenuListener() {
|
||||||
void placeClicked(Place place);
|
PopupMenu popupMenu = new PopupMenu(view.getContext(), iconOverflow);
|
||||||
|
popupMenu.inflate(R.menu.nearby_info_dialog_options);
|
||||||
|
|
||||||
|
MenuItem commonsArticle = popupMenu.getMenu()
|
||||||
|
.findItem(R.id.nearby_info_menu_commons_article);
|
||||||
|
MenuItem wikiDataArticle = popupMenu.getMenu()
|
||||||
|
.findItem(R.id.nearby_info_menu_wikidata_article);
|
||||||
|
MenuItem wikipediaArticle = popupMenu.getMenu()
|
||||||
|
.findItem(R.id.nearby_info_menu_wikipedia_article);
|
||||||
|
|
||||||
|
commonsArticle.setEnabled(place.hasCommonsLink());
|
||||||
|
wikiDataArticle.setEnabled(place.hasWikidataLink());
|
||||||
|
wikipediaArticle.setEnabled(place.hasWikipediaLink());
|
||||||
|
|
||||||
|
popupMenu.setOnMenuItemClickListener(item -> {
|
||||||
|
switch (item.getItemId()) {
|
||||||
|
case R.id.nearby_info_menu_commons_article:
|
||||||
|
openWebView(place.siteLinks.getCommonsLink());
|
||||||
|
return true;
|
||||||
|
case R.id.nearby_info_menu_wikidata_article:
|
||||||
|
openWebView(place.siteLinks.getWikidataLink());
|
||||||
|
return true;
|
||||||
|
case R.id.nearby_info_menu_wikipedia_article:
|
||||||
|
openWebView(place.siteLinks.getWikipediaLink());
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
popupMenu.show();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private void openWebView(Uri link) {
|
||||||
|
Intent browserIntent = new Intent(Intent.ACTION_VIEW, link);
|
||||||
|
view.getContext().startActivity(browserIntent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean showMenu() {
|
||||||
|
return place.hasCommonsLink() || place.hasWikidataLink();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,20 +7,15 @@ package fr.free.nrw.commons.notification;
|
||||||
public class Notification {
|
public class Notification {
|
||||||
public NotificationType notificationType;
|
public NotificationType notificationType;
|
||||||
public String notificationText;
|
public String notificationText;
|
||||||
|
public String date;
|
||||||
|
public String description;
|
||||||
|
public String link;
|
||||||
|
|
||||||
|
public Notification(NotificationType notificationType, String notificationText, String date, String description, String link) {
|
||||||
Notification (NotificationType notificationType, String notificationText) {
|
|
||||||
this.notificationType = notificationType;
|
this.notificationType = notificationType;
|
||||||
this.notificationText = notificationText;
|
this.notificationText = notificationText;
|
||||||
}
|
this.date = date;
|
||||||
|
this.description = description;
|
||||||
|
this.link = link;
|
||||||
public enum NotificationType {
|
|
||||||
/* Added for test purposes, needs to be rescheduled after implementing
|
|
||||||
fetching notifications from server */
|
|
||||||
edit,
|
|
||||||
mention,
|
|
||||||
message,
|
|
||||||
block;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,32 @@
|
||||||
package fr.free.nrw.commons.notification;
|
package fr.free.nrw.commons.notification;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.FragmentManager;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.v7.widget.DividerItemDecoration;
|
||||||
import android.support.v7.widget.LinearLayoutManager;
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.pedrogomez.renderers.RVRendererAdapter;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import butterknife.BindView;
|
import butterknife.BindView;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
import butterknife.Optional;
|
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
||||||
|
import io.reactivex.Observable;
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.schedulers.Schedulers;
|
||||||
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
import static android.widget.Toast.LENGTH_SHORT;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by root on 18.12.2017.
|
* Created by root on 18.12.2017.
|
||||||
|
|
@ -18,33 +35,83 @@ import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
||||||
public class NotificationActivity extends NavigationBaseActivity {
|
public class NotificationActivity extends NavigationBaseActivity {
|
||||||
NotificationAdapterFactory notificationAdapterFactory;
|
NotificationAdapterFactory notificationAdapterFactory;
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@BindView(R.id.listView) RecyclerView recyclerView;
|
@BindView(R.id.listView) RecyclerView recyclerView;
|
||||||
|
|
||||||
|
@Inject NotificationController controller;
|
||||||
|
|
||||||
|
private static final String TAG_NOTIFICATION_WORKER_FRAGMENT = "NotificationWorkerFragment";
|
||||||
|
private NotificationWorkerFragment mNotificationWorkerFragment;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_notification);
|
setContentView(R.layout.activity_notification);
|
||||||
ButterKnife.bind(this);
|
ButterKnife.bind(this);
|
||||||
|
mNotificationWorkerFragment = (NotificationWorkerFragment) getFragmentManager()
|
||||||
|
.findFragmentByTag(TAG_NOTIFICATION_WORKER_FRAGMENT);
|
||||||
initListView();
|
initListView();
|
||||||
addNotifications();
|
|
||||||
initDrawer();
|
initDrawer();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initListView() {
|
private void initListView() {
|
||||||
recyclerView = findViewById(R.id.listView);
|
|
||||||
recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||||
notificationAdapterFactory = new NotificationAdapterFactory(new NotificationRenderer.NotificationClicked() {
|
DividerItemDecoration itemDecor = new DividerItemDecoration(recyclerView.getContext(), DividerItemDecoration.VERTICAL);
|
||||||
@Override
|
recyclerView.addItemDecoration(itemDecor);
|
||||||
public void notificationClicked(Notification notification) {
|
addNotifications();
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("CheckResult")
|
||||||
private void addNotifications() {
|
private void addNotifications() {
|
||||||
|
Timber.d("Add notifications");
|
||||||
|
|
||||||
recyclerView.setAdapter(notificationAdapterFactory.create(NotificationController.loadNotifications()));
|
if(mNotificationWorkerFragment == null){
|
||||||
|
Observable.fromCallable(() -> controller.getNotifications())
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(notificationList -> {
|
||||||
|
Timber.d("Number of notifications is %d", notificationList.size());
|
||||||
|
initializeAndSetNotificationList(notificationList);
|
||||||
|
setAdapter(notificationList);
|
||||||
|
}, throwable -> Timber.e(throwable, "Error occurred while loading notifications"));
|
||||||
|
} else {
|
||||||
|
setAdapter(mNotificationWorkerFragment.getNotificationList());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private void handleUrl(String url) {
|
||||||
|
if (url == null || url.equals("")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Intent browser = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||||
|
//check if web browser available
|
||||||
|
if(browser.resolveActivity(this.getPackageManager()) != null){
|
||||||
|
startActivity(browser);
|
||||||
|
} else {
|
||||||
|
Toast toast = Toast.makeText(this, getString(R.string.no_web_browser), LENGTH_SHORT);
|
||||||
|
toast.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
|
||||||
|
context.startActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeAndSetNotificationList(List<Notification> notificationList){
|
||||||
|
FragmentManager fm = getFragmentManager();
|
||||||
|
mNotificationWorkerFragment = new NotificationWorkerFragment();
|
||||||
|
fm.beginTransaction().add(mNotificationWorkerFragment, TAG_NOTIFICATION_WORKER_FRAGMENT)
|
||||||
|
.commit();
|
||||||
|
mNotificationWorkerFragment.setNotificationList(notificationList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,23 +1,39 @@
|
||||||
package fr.free.nrw.commons.notification;
|
package fr.free.nrw.commons.notification;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
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.
|
* Created by root on 19.12.2017.
|
||||||
*/
|
*/
|
||||||
|
@Singleton
|
||||||
public class NotificationController {
|
public class NotificationController {
|
||||||
|
|
||||||
public static List<Notification> loadNotifications() {
|
private MediaWikiApi mediaWikiApi;
|
||||||
List<Notification> notifications = new ArrayList<>();
|
private SessionManager sessionManager;
|
||||||
notifications.add(new Notification(Notification.NotificationType.message, "notification 1"));
|
|
||||||
notifications.add(new Notification(Notification.NotificationType.edit, "notification 2"));
|
@Inject
|
||||||
notifications.add(new Notification(Notification.NotificationType.mention, "notification 3"));
|
public NotificationController(MediaWikiApi mediaWikiApi, SessionManager sessionManager) {
|
||||||
notifications.add(new Notification(Notification.NotificationType.message, "notification 4"));
|
this.mediaWikiApi = mediaWikiApi;
|
||||||
notifications.add(new Notification(Notification.NotificationType.edit, "notification 5"));
|
this.sessionManager = sessionManager;
|
||||||
notifications.add(new Notification(Notification.NotificationType.mention, "notification 6"));
|
}
|
||||||
notifications.add(new Notification(Notification.NotificationType.message, "notification 7"));
|
|
||||||
return notifications;
|
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<>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
package fr.free.nrw.commons.notification;
|
package fr.free.nrw.commons.notification;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.borjabravo.readmoretextview.ReadMoreTextView;
|
||||||
import com.pedrogomez.renderers.Renderer;
|
import com.pedrogomez.renderers.Renderer;
|
||||||
|
|
||||||
import butterknife.BindView;
|
import butterknife.BindView;
|
||||||
|
|
@ -17,8 +19,8 @@ import fr.free.nrw.commons.R;
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class NotificationRenderer extends Renderer<Notification> {
|
public class NotificationRenderer extends Renderer<Notification> {
|
||||||
@BindView(R.id.title) TextView title;
|
@BindView(R.id.title) ReadMoreTextView title;
|
||||||
@BindView(R.id.description) TextView description;
|
@BindView(R.id.description) ReadMoreTextView description;
|
||||||
@BindView(R.id.time) TextView time;
|
@BindView(R.id.time) TextView time;
|
||||||
@BindView(R.id.icon) ImageView icon;
|
@BindView(R.id.icon) ImageView icon;
|
||||||
private NotificationClicked listener;
|
private NotificationClicked listener;
|
||||||
|
|
@ -45,23 +47,21 @@ public class NotificationRenderer extends Renderer<Notification> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void render() {
|
public void render() {
|
||||||
Notification notification = getContent();
|
Notification notification = getContent();
|
||||||
title.setText(notification.notificationText);
|
StringBuilder str = new StringBuilder(notification.notificationText);
|
||||||
time.setText("3d");
|
str.append(" " );
|
||||||
description.setText("Example notification description");
|
title.setText(str);
|
||||||
switch (notification.notificationType) {
|
time.setText(notification.date);
|
||||||
case edit:
|
StringBuilder desc = new StringBuilder(notification.description);
|
||||||
icon.setImageResource(R.drawable.ic_edit_black_24dp);
|
desc.append(" ");
|
||||||
break;
|
description.setText(desc);
|
||||||
case message:
|
switch (notification.notificationType) {
|
||||||
icon.setImageResource(R.drawable.ic_message_black_24dp);
|
case THANK_YOU_EDIT:
|
||||||
break;
|
icon.setImageResource(R.drawable.ic_edit_black_24dp);
|
||||||
case mention:
|
break;
|
||||||
icon.setImageResource(R.drawable.ic_chat_bubble_black_24px);
|
default:
|
||||||
break;
|
icon.setImageResource(R.drawable.round_icon_unknown);
|
||||||
default:
|
}
|
||||||
icon.setImageResource(R.drawable.round_icon_unknown);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface NotificationClicked{
|
public interface NotificationClicked{
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package fr.free.nrw.commons.notification;
|
||||||
|
|
||||||
|
import android.app.Fragment;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by knightshade on 25/2/18.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class NotificationWorkerFragment extends Fragment {
|
||||||
|
private List<Notification> notificationList;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setRetainInstance(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNotificationList(List<Notification> notificationList){
|
||||||
|
this.notificationList = notificationList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Notification> getNotificationList(){
|
||||||
|
return notificationList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,14 +3,17 @@ package fr.free.nrw.commons.settings;
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
import android.content.ActivityNotFoundException;
|
import android.content.ActivityNotFoundException;
|
||||||
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
|
import android.content.pm.ResolveInfo;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.CheckBoxPreference;
|
import android.preference.SwitchPreference;
|
||||||
import android.preference.EditTextPreference;
|
import android.preference.EditTextPreference;
|
||||||
import android.preference.ListPreference;
|
import android.preference.ListPreference;
|
||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
|
|
@ -21,15 +24,17 @@ import android.support.v4.content.FileProvider;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
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 {
|
||||||
|
|
@ -40,8 +45,11 @@ public class SettingsFragment extends PreferenceFragment {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
AndroidInjection.inject(this);
|
|
||||||
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);
|
||||||
|
|
@ -56,7 +64,7 @@ public class SettingsFragment extends PreferenceFragment {
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
CheckBoxPreference themePreference = (CheckBoxPreference) findPreference("theme");
|
SwitchPreference themePreference = (SwitchPreference) findPreference("theme");
|
||||||
themePreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
themePreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
getActivity().recreate();
|
getActivity().recreate();
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -67,7 +75,12 @@ public class SettingsFragment extends PreferenceFragment {
|
||||||
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;
|
||||||
|
try {
|
||||||
|
value = Integer.parseInt(newValue.toString());
|
||||||
|
} catch(Exception e) {
|
||||||
|
value = 100; //Default number
|
||||||
|
}
|
||||||
final SharedPreferences.Editor editor = prefs.edit();
|
final SharedPreferences.Editor editor = prefs.edit();
|
||||||
if (value > 500) {
|
if (value > 500) {
|
||||||
new AlertDialog.Builder(getActivity())
|
new AlertDialog.Builder(getActivity())
|
||||||
|
|
@ -81,9 +94,9 @@ public class SettingsFragment extends PreferenceFragment {
|
||||||
uploadLimit.setSummary(500 + "");
|
uploadLimit.setSummary(500 + "");
|
||||||
uploadLimit.setText(500 + "");
|
uploadLimit.setText(500 + "");
|
||||||
} else {
|
} else {
|
||||||
editor.putInt(Prefs.UPLOADS_SHOWING, Integer.parseInt(newValue.toString()));
|
editor.putInt(Prefs.UPLOADS_SHOWING, value);
|
||||||
editor.putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED,true);
|
editor.putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED,true);
|
||||||
uploadLimit.setSummary(newValue.toString());
|
uploadLimit.setSummary(String.valueOf(value));
|
||||||
}
|
}
|
||||||
editor.apply();
|
editor.apply();
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -133,19 +146,24 @@ public class SettingsFragment extends PreferenceFragment {
|
||||||
appLogsFile
|
appLogsFile
|
||||||
);
|
);
|
||||||
|
|
||||||
Intent feedbackIntent = new Intent(Intent.ACTION_SEND);
|
//initialize the emailSelectorIntent
|
||||||
feedbackIntent.setType("message/rfc822");
|
Intent emailSelectorIntent = new Intent(Intent.ACTION_SENDTO);
|
||||||
feedbackIntent.putExtra(Intent.EXTRA_EMAIL,
|
emailSelectorIntent.setData(Uri.parse("mailto:"));
|
||||||
new String[]{CommonsApplication.LOGS_PRIVATE_EMAIL});
|
//initialize the emailIntent
|
||||||
feedbackIntent.putExtra(Intent.EXTRA_SUBJECT,
|
final Intent emailIntent = new Intent(Intent.ACTION_SEND);
|
||||||
String.format(CommonsApplication.FEEDBACK_EMAIL_SUBJECT,
|
emailIntent.putExtra(Intent.EXTRA_EMAIL, new String[]{CommonsApplication.FEEDBACK_EMAIL});
|
||||||
BuildConfig.VERSION_NAME));
|
emailIntent.putExtra(Intent.EXTRA_SUBJECT, String.format(CommonsApplication.FEEDBACK_EMAIL_SUBJECT, BuildConfig.VERSION_NAME));
|
||||||
feedbackIntent.putExtra(Intent.EXTRA_STREAM,appLogsFilePath);
|
emailIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
emailIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||||
|
emailIntent.setSelector( emailSelectorIntent );
|
||||||
|
//adding the attachment to the intent
|
||||||
|
emailIntent.putExtra(Intent.EXTRA_STREAM, appLogsFilePath);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
startActivity(feedbackIntent);
|
startActivity(Intent.createChooser(emailIntent, "Send mail.."));
|
||||||
} catch (ActivityNotFoundException e) {
|
} catch (ActivityNotFoundException e) {
|
||||||
Toast.makeText(getActivity(), R.string.no_email_client, Toast.LENGTH_SHORT).show();
|
Toast.makeText(getActivity(), R.string.no_email_client, Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
|
||||||
import dagger.android.support.DaggerAppCompatActivity;
|
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
|
import fr.free.nrw.commons.di.CommonsDaggerAppCompatActivity;
|
||||||
|
|
||||||
public abstract class BaseActivity extends DaggerAppCompatActivity {
|
public abstract class BaseActivity extends CommonsDaggerAppCompatActivity {
|
||||||
boolean currentTheme;
|
boolean currentTheme;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import android.accounts.AccountManager;
|
||||||
import android.content.ActivityNotFoundException;
|
import android.content.ActivityNotFoundException;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.design.widget.NavigationView;
|
import android.support.design.widget.NavigationView;
|
||||||
import android.support.v4.widget.DrawerLayout;
|
import android.support.v4.widget.DrawerLayout;
|
||||||
|
|
@ -22,6 +23,7 @@ import fr.free.nrw.commons.AboutActivity;
|
||||||
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.WelcomeActivity;
|
import fr.free.nrw.commons.WelcomeActivity;
|
||||||
import fr.free.nrw.commons.auth.AccountUtil;
|
import fr.free.nrw.commons.auth.AccountUtil;
|
||||||
import fr.free.nrw.commons.auth.LoginActivity;
|
import fr.free.nrw.commons.auth.LoginActivity;
|
||||||
|
|
@ -87,8 +89,11 @@ public abstract class NavigationBaseActivity extends BaseActivity
|
||||||
|
|
||||||
private void setDrawerPaneWidth() {
|
private void setDrawerPaneWidth() {
|
||||||
ViewGroup.LayoutParams params = navigationView.getLayoutParams();
|
ViewGroup.LayoutParams params = navigationView.getLayoutParams();
|
||||||
// set width to lowerBound of 80% of the screen size
|
// set width to lowerBound of 70% of the screen size in portrait mode
|
||||||
params.width = (getResources().getDisplayMetrics().widthPixels * 70) / 100;
|
// set width to lowerBound of 50% of the screen size in landscape mode
|
||||||
|
int percentageWidth = getResources().getInteger(R.integer.drawer_width);
|
||||||
|
|
||||||
|
params.width = (getResources().getDisplayMetrics().widthPixels * percentageWidth) / 100;
|
||||||
navigationView.setLayoutParams(params);
|
navigationView.setLayoutParams(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -120,8 +125,9 @@ public abstract class NavigationBaseActivity extends BaseActivity
|
||||||
return true;
|
return true;
|
||||||
case R.id.action_feedback:
|
case R.id.action_feedback:
|
||||||
drawerLayout.closeDrawer(navigationView);
|
drawerLayout.closeDrawer(navigationView);
|
||||||
Intent feedbackIntent = new Intent(Intent.ACTION_SEND);
|
Intent feedbackIntent = new Intent(Intent.ACTION_SENDTO);
|
||||||
feedbackIntent.setType("message/rfc822");
|
feedbackIntent.setType("message/rfc822");
|
||||||
|
feedbackIntent.setData(Uri.parse("mailto:"));
|
||||||
feedbackIntent.putExtra(Intent.EXTRA_EMAIL,
|
feedbackIntent.putExtra(Intent.EXTRA_EMAIL,
|
||||||
new String[]{CommonsApplication.FEEDBACK_EMAIL});
|
new String[]{CommonsApplication.FEEDBACK_EMAIL});
|
||||||
feedbackIntent.putExtra(Intent.EXTRA_SUBJECT,
|
feedbackIntent.putExtra(Intent.EXTRA_SUBJECT,
|
||||||
|
|
@ -147,7 +153,7 @@ public abstract class NavigationBaseActivity extends BaseActivity
|
||||||
return true;
|
return true;
|
||||||
case R.id.action_notifications:
|
case R.id.action_notifications:
|
||||||
drawerLayout.closeDrawer(navigationView);
|
drawerLayout.closeDrawer(navigationView);
|
||||||
startActivityWithFlags(this, NotificationActivity.class, Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
|
NotificationActivity.startYourself(this);
|
||||||
return true;
|
return true;
|
||||||
case R.id.action_featured_images:
|
case R.id.action_featured_images:
|
||||||
drawerLayout.closeDrawer(navigationView);
|
drawerLayout.closeDrawer(navigationView);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package fr.free.nrw.commons.ui.widget;
|
package fr.free.nrw.commons.ui.widget;
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* Created by mikel on 07/08/2017.
|
*Created by mikel on 07/08/2017.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
@ -20,20 +20,22 @@ import fr.free.nrw.commons.utils.UiUtils;
|
||||||
* a text view compatible with older versions of the platform
|
* a text view compatible with older versions of the platform
|
||||||
*/
|
*/
|
||||||
public class CompatTextView extends AppCompatTextView {
|
public class CompatTextView extends AppCompatTextView {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new instance of CompatTextView
|
* Constructs a new instance of CompatTextView
|
||||||
|
*
|
||||||
* @param context the view context
|
* @param context the view context
|
||||||
*/
|
*/
|
||||||
public CompatTextView(Context context) {
|
public CompatTextView(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
init(null);
|
init(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new instance of CompatTextView
|
* Constructs a new instance of CompatTextView
|
||||||
|
*
|
||||||
* @param context the view context
|
* @param context the view context
|
||||||
* @param attrs the set of attributes for the view
|
* @param attrs the set of attributes for the view
|
||||||
*/
|
*/
|
||||||
public CompatTextView(Context context, AttributeSet attrs) {
|
public CompatTextView(Context context, AttributeSet attrs) {
|
||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
|
|
@ -42,6 +44,7 @@ public class CompatTextView extends AppCompatTextView {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new instance of CompatTextView
|
* Constructs a new instance of CompatTextView
|
||||||
|
*
|
||||||
* @param context
|
* @param context
|
||||||
* @param attrs
|
* @param attrs
|
||||||
* @param defStyleAttr
|
* @param defStyleAttr
|
||||||
|
|
@ -53,6 +56,7 @@ public class CompatTextView extends AppCompatTextView {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* initializes the view
|
* initializes the view
|
||||||
|
*
|
||||||
* @param attrs the attribute set of the view, which can be null
|
* @param attrs the attribute set of the view, which can be null
|
||||||
*/
|
*/
|
||||||
private void init(@Nullable AttributeSet attrs) {
|
private void init(@Nullable AttributeSet attrs) {
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ public abstract class OverlayDialog extends DialogFragment {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* creates a DialogFragment with the correct style and theme
|
* creates a DialogFragment with the correct style and theme
|
||||||
* @param savedInstanceState
|
* @param savedInstanceState bundle re-constructed from a previous saved state
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
package fr.free.nrw.commons.upload;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.BitmapRegionDecoder;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.utils.ImageUtils;
|
||||||
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by bluesir9 on 16/9/17.
|
||||||
|
*
|
||||||
|
* <p>Responsible for checking if the picture that the user is trying to upload is useful or not. Will attempt to filter
|
||||||
|
* away completely black,fuzzy/blurry pictures(for now).
|
||||||
|
*
|
||||||
|
* <p>todo: Detect selfies?
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class DetectUnwantedPicturesAsync extends AsyncTask<Void, Void, ImageUtils.Result> {
|
||||||
|
|
||||||
|
interface Callback {
|
||||||
|
void onResult(ImageUtils.Result result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Callback callback;
|
||||||
|
private final String imageMediaFilePath;
|
||||||
|
|
||||||
|
DetectUnwantedPicturesAsync(String imageMediaFilePath, Callback callback) {
|
||||||
|
this.callback = callback;
|
||||||
|
this.imageMediaFilePath = imageMediaFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ImageUtils.Result doInBackground(Void... voids) {
|
||||||
|
try {
|
||||||
|
Timber.d("FilePath: " + imageMediaFilePath);
|
||||||
|
if (imageMediaFilePath == null) {
|
||||||
|
return ImageUtils.Result.IMAGE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(imageMediaFilePath,false);
|
||||||
|
|
||||||
|
return ImageUtils.checkIfImageIsTooDark(decoder);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
Timber.e(ioe, "IO Exception");
|
||||||
|
return ImageUtils.Result.IMAGE_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(ImageUtils.Result result) {
|
||||||
|
super.onPostExecute(result);
|
||||||
|
//callback to UI so that it can take necessary decision based on the result obtained
|
||||||
|
callback.onResult(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
package fr.free.nrw.commons.upload;
|
package fr.free.nrw.commons.upload;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.support.v7.app.AlertDialog;
|
import android.support.v7.app.AlertDialog;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
|
||||||
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;
|
||||||
|
|
@ -28,12 +30,14 @@ public class ExistingFileAsync extends AsyncTask<Void, Void, Boolean> {
|
||||||
DUPLICATE_CANCELLED
|
DUPLICATE_CANCELLED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final WeakReference<Activity> activity;
|
||||||
private final MediaWikiApi api;
|
private final MediaWikiApi api;
|
||||||
private final String fileSha1;
|
private final String fileSha1;
|
||||||
private final Context context;
|
private final WeakReference<Context> context;
|
||||||
private final Callback callback;
|
private final Callback callback;
|
||||||
|
|
||||||
public ExistingFileAsync(String fileSha1, Context context, Callback callback, MediaWikiApi mwApi) {
|
public ExistingFileAsync(WeakReference<Activity> activity, String fileSha1, WeakReference<Context> context, Callback callback, MediaWikiApi mwApi) {
|
||||||
|
this.activity = activity;
|
||||||
this.fileSha1 = fileSha1;
|
this.fileSha1 = fileSha1;
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
|
|
@ -69,19 +73,21 @@ public class ExistingFileAsync extends AsyncTask<Void, Void, Boolean> {
|
||||||
// If file exists, display warning to user.
|
// If file exists, display warning to user.
|
||||||
// Use soft warning for now (user able to choose to proceed) until have determined that implementation works without bugs
|
// Use soft warning for now (user able to choose to proceed) until have determined that implementation works without bugs
|
||||||
if (fileExists) {
|
if (fileExists) {
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
AlertDialog.Builder builder = new AlertDialog.Builder(context.get());
|
||||||
builder.setMessage(R.string.file_exists)
|
builder.setMessage(R.string.file_exists)
|
||||||
.setTitle(R.string.warning);
|
.setTitle(R.string.warning);
|
||||||
builder.setPositiveButton(R.string.no, (dialog, id) -> {
|
builder.setPositiveButton(R.string.no, (dialog, id) -> {
|
||||||
//Go back to ContributionsActivity
|
//Go back to ContributionsActivity
|
||||||
Intent intent = new Intent(context, ContributionsActivity.class);
|
Intent intent = new Intent(context.get(), ContributionsActivity.class);
|
||||||
context.startActivity(intent);
|
context.get().startActivity(intent);
|
||||||
callback.onResult(Result.DUPLICATE_CANCELLED);
|
callback.onResult(Result.DUPLICATE_CANCELLED);
|
||||||
});
|
});
|
||||||
builder.setNegativeButton(R.string.yes, (dialog, id) -> callback.onResult(Result.DUPLICATE_PROCEED));
|
builder.setNegativeButton(R.string.yes, (dialog, id) -> callback.onResult(Result.DUPLICATE_PROCEED));
|
||||||
|
|
||||||
AlertDialog dialog = builder.create();
|
AlertDialog dialog = builder.create();
|
||||||
dialog.show();
|
if (!activity.get().isFinishing()) {
|
||||||
|
dialog.show();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
callback.onResult(Result.NO_DUPLICATE);
|
callback.onResult(Result.NO_DUPLICATE);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,20 +3,25 @@ package fr.free.nrw.commons.upload;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.ContentUris;
|
import android.content.ContentUris;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
|
import android.preference.PreferenceManager;
|
||||||
import android.provider.DocumentsContract;
|
import android.provider.DocumentsContract;
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.io.FileDescriptor;
|
import java.io.FileDescriptor;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.channels.FileChannel;
|
import java.nio.channels.FileChannel;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
|
@ -28,7 +33,7 @@ public class FileUtils {
|
||||||
* other file-based ContentProviders.
|
* other file-based ContentProviders.
|
||||||
*
|
*
|
||||||
* @param context The context.
|
* @param context The context.
|
||||||
* @param uri The Uri to query.
|
* @param uri The Uri to query.
|
||||||
* @author paulburke
|
* @author paulburke
|
||||||
*/
|
*/
|
||||||
// Can be safely suppressed, checks for isKitKat before running isDocumentUri
|
// Can be safely suppressed, checks for isKitKat before running isDocumentUri
|
||||||
|
|
@ -36,6 +41,7 @@ public class FileUtils {
|
||||||
@Nullable
|
@Nullable
|
||||||
public static String getPath(Context context, Uri uri) {
|
public static String getPath(Context context, Uri uri) {
|
||||||
|
|
||||||
|
String returnPath = null;
|
||||||
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
|
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
|
||||||
|
|
||||||
// DocumentProvider
|
// DocumentProvider
|
||||||
|
|
@ -47,31 +53,34 @@ public class FileUtils {
|
||||||
final String type = split[0];
|
final String type = split[0];
|
||||||
|
|
||||||
if ("primary".equalsIgnoreCase(type)) {
|
if ("primary".equalsIgnoreCase(type)) {
|
||||||
return Environment.getExternalStorageDirectory() + "/" + split[1];
|
returnPath = 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);
|
returnPath = 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];
|
||||||
|
|
||||||
Uri contentUri = null;
|
Uri contentUri = null;
|
||||||
if ("image".equals(type)) {
|
switch (type) {
|
||||||
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
|
case "image":
|
||||||
} else if ("video".equals(type)) {
|
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
|
||||||
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
|
break;
|
||||||
} else if ("audio".equals(type)) {
|
case "video":
|
||||||
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
|
||||||
|
break;
|
||||||
|
case "audio":
|
||||||
|
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
final String selection = "_id=?";
|
final String selection = "_id=?";
|
||||||
|
|
@ -79,16 +88,55 @@ public class FileUtils {
|
||||||
split[1]
|
split[1]
|
||||||
};
|
};
|
||||||
|
|
||||||
return getDataColumn(context, contentUri, selection, selectionArgs);
|
returnPath = getDataColumn(context, contentUri, selection, selectionArgs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// MediaStore (and general)
|
// MediaStore (and general)
|
||||||
else if ("content".equalsIgnoreCase(uri.getScheme())) {
|
else if ("content".equalsIgnoreCase(uri.getScheme())) {
|
||||||
return getDataColumn(context, uri, null, null);
|
returnPath = getDataColumn(context, uri, null, null);
|
||||||
}
|
}
|
||||||
// File
|
// File
|
||||||
else if ("file".equalsIgnoreCase(uri.getScheme())) {
|
else if ("file".equalsIgnoreCase(uri.getScheme())) {
|
||||||
return uri.getPath();
|
returnPath = uri.getPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(returnPath == null) {
|
||||||
|
//fetching path may fail depending on the source URI and all hope is lost
|
||||||
|
//so we will create and use a copy of the file, which seems to work
|
||||||
|
String copyPath = null;
|
||||||
|
try {
|
||||||
|
ParcelFileDescriptor descriptor
|
||||||
|
= context.getContentResolver().openFileDescriptor(uri, "r");
|
||||||
|
if (descriptor != null) {
|
||||||
|
|
||||||
|
SharedPreferences sharedPref = PreferenceManager
|
||||||
|
.getDefaultSharedPreferences(context);
|
||||||
|
boolean useExtStorage = sharedPref.getBoolean("useExternalStorage", true);
|
||||||
|
if (useExtStorage) {
|
||||||
|
copyPath = Environment.getExternalStorageDirectory().toString()
|
||||||
|
+ "/CommonsApp/" + new Date().getTime() + ".jpg";
|
||||||
|
File newFile = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp");
|
||||||
|
newFile.mkdir();
|
||||||
|
FileUtils.copy(
|
||||||
|
descriptor.getFileDescriptor(),
|
||||||
|
copyPath);
|
||||||
|
Timber.d("Filepath (copied): %s", copyPath);
|
||||||
|
return copyPath;
|
||||||
|
}
|
||||||
|
copyPath = context.getCacheDir().getAbsolutePath()
|
||||||
|
+ "/" + new Date().getTime() + ".jpg";
|
||||||
|
FileUtils.copy(
|
||||||
|
descriptor.getFileDescriptor(),
|
||||||
|
copyPath);
|
||||||
|
Timber.d("Filepath (copied): %s", copyPath);
|
||||||
|
return copyPath;
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
Timber.w(e, "Error in file " + copyPath);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return returnPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -109,7 +157,7 @@ public class FileUtils {
|
||||||
String[] selectionArgs) {
|
String[] selectionArgs) {
|
||||||
|
|
||||||
Cursor cursor = null;
|
Cursor cursor = null;
|
||||||
final String column = "_data";
|
final String column = MediaStore.Images.ImageColumns.DATA;
|
||||||
final String[] projection = {
|
final String[] projection = {
|
||||||
column
|
column
|
||||||
};
|
};
|
||||||
|
|
@ -163,7 +211,8 @@ public class FileUtils {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copy content from source file to destination file.
|
* Copy content from source file to destination file.
|
||||||
* @param source stream copied from
|
*
|
||||||
|
* @param source stream copied from
|
||||||
* @param destination stream copied to
|
* @param destination stream copied to
|
||||||
* @throws IOException thrown when failing to read source or opening destination file
|
* @throws IOException thrown when failing to read source or opening destination file
|
||||||
*/
|
*/
|
||||||
|
|
@ -176,7 +225,8 @@ public class FileUtils {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copy content from source file to destination file.
|
* Copy content from source file to destination file.
|
||||||
* @param source file descriptor copied from
|
*
|
||||||
|
* @param source file descriptor copied from
|
||||||
* @param destination file path copied to
|
* @param destination file path copied to
|
||||||
* @throws IOException thrown when failing to read source or opening destination file
|
* @throws IOException thrown when failing to read source or opening destination file
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -113,11 +113,11 @@ public class GPSExtractor {
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
public String getCoords(boolean useGPS) {
|
public String getCoords(boolean useGPS) {
|
||||||
String latitude = "";
|
String latitude;
|
||||||
String longitude = "";
|
String longitude;
|
||||||
String latitude_ref = "";
|
String latitudeRef;
|
||||||
String longitude_ref = "";
|
String longitudeRef;
|
||||||
String decimalCoords = "";
|
String decimalCoords;
|
||||||
|
|
||||||
//If image has no EXIF data and user has enabled GPS setting, get user's location
|
//If image has no EXIF data and user has enabled GPS setting, get user's location
|
||||||
if (exif == null || exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE) == null) {
|
if (exif == null || exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE) == null) {
|
||||||
|
|
@ -150,15 +150,15 @@ public class GPSExtractor {
|
||||||
Timber.d("EXIF data has location info");
|
Timber.d("EXIF data has location info");
|
||||||
|
|
||||||
latitude = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE);
|
latitude = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE);
|
||||||
latitude_ref = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF);
|
latitudeRef = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF);
|
||||||
longitude = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE);
|
longitude = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE);
|
||||||
longitude_ref = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF);
|
longitudeRef = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF);
|
||||||
|
|
||||||
if (latitude!=null && latitude_ref!=null && longitude!=null && longitude_ref!=null) {
|
if (latitude!=null && latitudeRef!=null && longitude!=null && longitudeRef!=null) {
|
||||||
Timber.d("Latitude: %s %s", latitude, latitude_ref);
|
Timber.d("Latitude: %s %s", latitude, latitudeRef);
|
||||||
Timber.d("Longitude: %s %s", longitude, longitude_ref);
|
Timber.d("Longitude: %s %s", longitude, longitudeRef);
|
||||||
|
|
||||||
decimalCoords = getDecimalCoords(latitude, latitude_ref, longitude, longitude_ref);
|
decimalCoords = getDecimalCoords(latitude, latitudeRef, longitude, longitudeRef);
|
||||||
return decimalCoords;
|
return decimalCoords;
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package fr.free.nrw.commons.upload;
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
import android.app.ProgressDialog;
|
import android.app.ProgressDialog;
|
||||||
import android.content.ContentProviderClient;
|
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
|
@ -12,7 +11,9 @@ import android.database.DataSetObserver;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v4.app.ActivityCompat;
|
import android.support.v4.app.ActivityCompat;
|
||||||
import android.support.v4.app.FragmentManager;
|
import android.support.v4.app.FragmentManager;
|
||||||
import android.support.v4.content.ContextCompat;
|
import android.support.v4.content.ContextCompat;
|
||||||
|
|
@ -22,6 +23,7 @@ import android.view.inputmethod.InputMethodManager;
|
||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|
@ -52,10 +54,17 @@ public class MultipleShareActivity extends AuthenticatedActivity
|
||||||
MultipleUploadListFragment.OnMultipleUploadInitiatedHandler,
|
MultipleUploadListFragment.OnMultipleUploadInitiatedHandler,
|
||||||
OnCategoriesSaveHandler {
|
OnCategoriesSaveHandler {
|
||||||
|
|
||||||
@Inject MediaWikiApi mwApi;
|
@Inject
|
||||||
@Inject SessionManager sessionManager;
|
MediaWikiApi mwApi;
|
||||||
@Inject UploadController uploadController;
|
@Inject
|
||||||
@Inject @Named("default_preferences") SharedPreferences prefs;
|
SessionManager sessionManager;
|
||||||
|
@Inject
|
||||||
|
UploadController uploadController;
|
||||||
|
@Inject
|
||||||
|
ModifierSequenceDao modifierSequenceDao;
|
||||||
|
@Inject
|
||||||
|
@Named("default_preferences")
|
||||||
|
SharedPreferences prefs;
|
||||||
|
|
||||||
private ArrayList<Contribution> photosList = null;
|
private ArrayList<Contribution> photosList = null;
|
||||||
|
|
||||||
|
|
@ -63,6 +72,8 @@ public class MultipleShareActivity extends AuthenticatedActivity
|
||||||
private MediaDetailPagerFragment mediaDetails;
|
private MediaDetailPagerFragment mediaDetails;
|
||||||
private CategorizationFragment categorizationFragment;
|
private CategorizationFragment categorizationFragment;
|
||||||
|
|
||||||
|
private boolean locationPermitted = false;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Media getMediaAtPosition(int i) {
|
public Media getMediaAtPosition(int i) {
|
||||||
return photosList.get(i);
|
return photosList.get(i);
|
||||||
|
|
@ -166,19 +177,18 @@ public class MultipleShareActivity extends AuthenticatedActivity
|
||||||
@Override
|
@Override
|
||||||
public void onCategoriesSave(List<String> categories) {
|
public void onCategoriesSave(List<String> categories) {
|
||||||
if (categories.size() > 0) {
|
if (categories.size() > 0) {
|
||||||
ModifierSequenceDao dao = new ModifierSequenceDao(getContentResolver().acquireContentProviderClient(ModificationsContentProvider.AUTHORITY));
|
|
||||||
for (Contribution contribution : photosList) {
|
for (Contribution contribution : photosList) {
|
||||||
ModifierSequence categoriesSequence = new ModifierSequence(contribution.getContentUri());
|
ModifierSequence categoriesSequence = new ModifierSequence(contribution.getContentUri());
|
||||||
|
|
||||||
categoriesSequence.queueModifier(new CategoryModifier(categories.toArray(new String[]{})));
|
categoriesSequence.queueModifier(new CategoryModifier(categories.toArray(new String[]{})));
|
||||||
categoriesSequence.queueModifier(new TemplateRemoveModifier("Uncategorized"));
|
categoriesSequence.queueModifier(new TemplateRemoveModifier("Uncategorized"));
|
||||||
|
|
||||||
dao.save(categoriesSequence);
|
modifierSequenceDao.save(categoriesSequence);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// FIXME: Make sure that the content provider is up
|
// FIXME: Make sure that the content provider is up
|
||||||
// This is the wrong place for it, but bleh - better than not having it turned on by default for people who don't go throughl ogin
|
// This is the wrong place for it, but bleh - better than not having it turned on by default for people who don't go throughl ogin
|
||||||
ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(), ModificationsContentProvider.AUTHORITY, true); // Enable sync by default!
|
ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(), ModificationsContentProvider.MODIFICATIONS_AUTHORITY, true); // Enable sync by default!
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -208,6 +218,14 @@ public class MultipleShareActivity extends AuthenticatedActivity
|
||||||
|
|
||||||
getSupportFragmentManager().addOnBackStackChangedListener(this);
|
getSupportFragmentManager().addOnBackStackChangedListener(this);
|
||||||
requestAuthToken();
|
requestAuthToken();
|
||||||
|
|
||||||
|
//TODO: 15/10/17 should location permission be explicitly requested if not provided?
|
||||||
|
//check if location permission is enabled
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
if (ContextCompat.checkSelfPermission(this,Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
locationPermitted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -241,7 +259,7 @@ public class MultipleShareActivity extends AuthenticatedActivity
|
||||||
mwApi.setAuthCookie(authCookie);
|
mwApi.setAuthCookie(authCookie);
|
||||||
Intent intent = getIntent();
|
Intent intent = getIntent();
|
||||||
|
|
||||||
if (intent.getAction().equals(Intent.ACTION_SEND_MULTIPLE)) {
|
if (Intent.ACTION_SEND_MULTIPLE.equals(intent.getAction())) {
|
||||||
if (photosList == null) {
|
if (photosList == null) {
|
||||||
photosList = new ArrayList<>();
|
photosList = new ArrayList<>();
|
||||||
ArrayList<Uri> urisList = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
|
ArrayList<Uri> urisList = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
|
||||||
|
|
@ -253,6 +271,11 @@ public class MultipleShareActivity extends AuthenticatedActivity
|
||||||
up.setTag("sequence", i);
|
up.setTag("sequence", i);
|
||||||
up.setSource(Contribution.SOURCE_EXTERNAL);
|
up.setSource(Contribution.SOURCE_EXTERNAL);
|
||||||
up.setMultiple(true);
|
up.setMultiple(true);
|
||||||
|
String imageGpsCoordinates = extractImageGpsData(uri);
|
||||||
|
if (imageGpsCoordinates != null) {
|
||||||
|
Timber.d("GPS data for image found!");
|
||||||
|
up.setDecimalCoords(imageGpsCoordinates);
|
||||||
|
}
|
||||||
photosList.add(up);
|
photosList.add(up);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -279,7 +302,49 @@ public class MultipleShareActivity extends AuthenticatedActivity
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBackStackChanged() {
|
public void onBackStackChanged() {
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(mediaDetails != null && mediaDetails.isVisible()) ;
|
getSupportActionBar().setDisplayHomeAsUpEnabled(mediaDetails != null && mediaDetails.isVisible());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will attempt to extract the gps coordinates using exif data or by using the current
|
||||||
|
* location if available for the image who's imageUri has been provided.
|
||||||
|
* @param imageUri The uri of the image who's GPS coordinates data we wish to extract
|
||||||
|
* @return GPS coordinates as a String as is returned by {@link GPSExtractor}
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private String extractImageGpsData(Uri imageUri) {
|
||||||
|
Timber.d("Entering extractImagesGpsData");
|
||||||
|
|
||||||
|
if (imageUri == null) {
|
||||||
|
//now why would you do that???
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
GPSExtractor gpsExtractor = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
ParcelFileDescriptor fd = getContentResolver().openFileDescriptor(imageUri,"r");
|
||||||
|
if (fd != null) {
|
||||||
|
gpsExtractor = new GPSExtractor(fd.getFileDescriptor(),this,prefs);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String filePath = FileUtils.getPath(this,imageUri);
|
||||||
|
if (filePath != null) {
|
||||||
|
gpsExtractor = new GPSExtractor(filePath,this,prefs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gpsExtractor != null) {
|
||||||
|
//get image coordinates from exif data or user location
|
||||||
|
return gpsExtractor.getCoords(locationPermitted);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (FileNotFoundException fnfe) {
|
||||||
|
Timber.w(fnfe);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
package fr.free.nrw.commons.upload;
|
package fr.free.nrw.commons.upload;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Point;
|
import android.graphics.Point;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.graphics.drawable.VectorDrawableCompat;
|
import android.support.graphics.drawable.VectorDrawableCompat;
|
||||||
|
import android.support.v4.app.Fragment;
|
||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.TextWatcher;
|
import android.text.TextWatcher;
|
||||||
import android.util.DisplayMetrics;
|
import android.util.DisplayMetrics;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
|
|
@ -27,12 +30,12 @@ import android.widget.TextView;
|
||||||
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 dagger.android.support.DaggerFragment;
|
import dagger.android.support.AndroidSupportInjection;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.contributions.Contribution;
|
import fr.free.nrw.commons.contributions.Contribution;
|
||||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
||||||
|
|
||||||
public class MultipleUploadListFragment extends DaggerFragment {
|
public class MultipleUploadListFragment extends Fragment {
|
||||||
|
|
||||||
public interface OnMultipleUploadInitiatedHandler {
|
public interface OnMultipleUploadInitiatedHandler {
|
||||||
void OnMultipleUploadInitiated();
|
void OnMultipleUploadInitiated();
|
||||||
|
|
@ -56,6 +59,12 @@ public class MultipleUploadListFragment extends DaggerFragment {
|
||||||
private RelativeLayout overlay;
|
private RelativeLayout overlay;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttach(Context context) {
|
||||||
|
AndroidSupportInjection.inject(this);
|
||||||
|
super.onAttach(context);
|
||||||
|
}
|
||||||
|
|
||||||
private class PhotoDisplayAdapter extends BaseAdapter {
|
private class PhotoDisplayAdapter extends BaseAdapter {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -170,9 +179,21 @@ public class MultipleUploadListFragment extends DaggerFragment {
|
||||||
photosGrid.setColumnWidth(photoSize.x);
|
photosGrid.setColumnWidth(photoSize.x);
|
||||||
|
|
||||||
baseTitle.addTextChangedListener(textWatcher);
|
baseTitle.addTextChangedListener(textWatcher);
|
||||||
|
|
||||||
|
baseTitle.setOnFocusChangeListener((v, hasFocus) -> {
|
||||||
|
if (!hasFocus) {
|
||||||
|
hideKeyboard(v);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void hideKeyboard(View view) {
|
||||||
|
InputMethodManager inputMethodManager =(InputMethodManager)getActivity().getSystemService(Activity.INPUT_METHOD_SERVICE);
|
||||||
|
inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroyView() {
|
public void onDestroyView() {
|
||||||
baseTitle.removeTextChangedListener(textWatcher);
|
baseTitle.removeTextChangedListener(textWatcher);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
package fr.free.nrw.commons.upload;
|
package fr.free.nrw.commons.upload;
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
|
import android.app.Activity;
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
|
|
@ -16,7 +19,9 @@ import android.support.annotation.RequiresApi;
|
||||||
import android.support.design.widget.Snackbar;
|
import android.support.design.widget.Snackbar;
|
||||||
import android.support.graphics.drawable.VectorDrawableCompat;
|
import android.support.graphics.drawable.VectorDrawableCompat;
|
||||||
import android.support.v4.app.ActivityCompat;
|
import android.support.v4.app.ActivityCompat;
|
||||||
|
import android.support.v4.app.FragmentManager;
|
||||||
import android.support.v4.content.ContextCompat;
|
import android.support.v4.content.ContextCompat;
|
||||||
|
import android.support.v7.app.AlertDialog;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
@ -29,6 +34,7 @@ import java.io.File;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
@ -46,11 +52,14 @@ import fr.free.nrw.commons.caching.CacheController;
|
||||||
import fr.free.nrw.commons.category.CategorizationFragment;
|
import fr.free.nrw.commons.category.CategorizationFragment;
|
||||||
import fr.free.nrw.commons.category.OnCategoriesSaveHandler;
|
import fr.free.nrw.commons.category.OnCategoriesSaveHandler;
|
||||||
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.modifications.CategoryModifier;
|
import fr.free.nrw.commons.modifications.CategoryModifier;
|
||||||
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
|
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
|
||||||
import fr.free.nrw.commons.modifications.ModifierSequence;
|
import fr.free.nrw.commons.modifications.ModifierSequence;
|
||||||
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
|
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
|
||||||
import fr.free.nrw.commons.modifications.TemplateRemoveModifier;
|
import fr.free.nrw.commons.modifications.TemplateRemoveModifier;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.utils.ImageUtils;
|
||||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
|
@ -61,10 +70,10 @@ import static fr.free.nrw.commons.upload.ExistingFileAsync.Result.NO_DUPLICATE;
|
||||||
* Activity for the title/desc screen after image is selected. Also starts processing image
|
* Activity for the title/desc screen after image is selected. Also starts processing image
|
||||||
* GPS coordinates or user location (if enabled in Settings) for category suggestions.
|
* GPS coordinates or user location (if enabled in Settings) for category suggestions.
|
||||||
*/
|
*/
|
||||||
public class ShareActivity
|
public class ShareActivity
|
||||||
extends AuthenticatedActivity
|
extends AuthenticatedActivity
|
||||||
implements SingleUploadFragment.OnUploadActionInitiated,
|
implements SingleUploadFragment.OnUploadActionInitiated,
|
||||||
OnCategoriesSaveHandler {
|
OnCategoriesSaveHandler,SimilarImageDialogFragment.onResponse {
|
||||||
|
|
||||||
private static final int REQUEST_PERM_ON_CREATE_STORAGE = 1;
|
private static final int REQUEST_PERM_ON_CREATE_STORAGE = 1;
|
||||||
private static final int REQUEST_PERM_ON_CREATE_LOCATION = 2;
|
private static final int REQUEST_PERM_ON_CREATE_LOCATION = 2;
|
||||||
|
|
@ -72,11 +81,19 @@ public class ShareActivity
|
||||||
private static final int REQUEST_PERM_ON_SUBMIT_STORAGE = 4;
|
private static final int REQUEST_PERM_ON_SUBMIT_STORAGE = 4;
|
||||||
private CategorizationFragment categorizationFragment;
|
private CategorizationFragment categorizationFragment;
|
||||||
|
|
||||||
@Inject MediaWikiApi mwApi;
|
@Inject
|
||||||
@Inject CacheController cacheController;
|
MediaWikiApi mwApi;
|
||||||
@Inject SessionManager sessionManager;
|
@Inject
|
||||||
@Inject UploadController uploadController;
|
CacheController cacheController;
|
||||||
@Inject @Named("default_preferences") SharedPreferences prefs;
|
@Inject
|
||||||
|
SessionManager sessionManager;
|
||||||
|
@Inject
|
||||||
|
UploadController uploadController;
|
||||||
|
@Inject
|
||||||
|
ModifierSequenceDao modifierSequenceDao;
|
||||||
|
@Inject
|
||||||
|
@Named("default_preferences")
|
||||||
|
SharedPreferences prefs;
|
||||||
|
|
||||||
private String source;
|
private String source;
|
||||||
private String mimeType;
|
private String mimeType;
|
||||||
|
|
@ -88,6 +105,7 @@ public class ShareActivity
|
||||||
private boolean cacheFound;
|
private boolean cacheFound;
|
||||||
|
|
||||||
private GPSExtractor imageObj;
|
private GPSExtractor imageObj;
|
||||||
|
private GPSExtractor tempImageObj;
|
||||||
private String decimalCoords;
|
private String decimalCoords;
|
||||||
|
|
||||||
private boolean useNewPermissions = false;
|
private boolean useNewPermissions = false;
|
||||||
|
|
@ -99,6 +117,9 @@ public class ShareActivity
|
||||||
private Snackbar snackbar;
|
private Snackbar snackbar;
|
||||||
private boolean duplicateCheckPassed = false;
|
private boolean duplicateCheckPassed = false;
|
||||||
|
|
||||||
|
private boolean haveCheckedForOtherImages = false;
|
||||||
|
private boolean isNearbyUpload = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when user taps the submit button.
|
* Called when user taps the submit button.
|
||||||
*/
|
*/
|
||||||
|
|
@ -166,13 +187,12 @@ public class ShareActivity
|
||||||
|
|
||||||
categoriesSequence.queueModifier(new CategoryModifier(categories.toArray(new String[]{})));
|
categoriesSequence.queueModifier(new CategoryModifier(categories.toArray(new String[]{})));
|
||||||
categoriesSequence.queueModifier(new TemplateRemoveModifier("Uncategorized"));
|
categoriesSequence.queueModifier(new TemplateRemoveModifier("Uncategorized"));
|
||||||
ModifierSequenceDao dao = new ModifierSequenceDao(getContentResolver().acquireContentProviderClient(ModificationsContentProvider.AUTHORITY));
|
modifierSequenceDao.save(categoriesSequence);
|
||||||
dao.save(categoriesSequence);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Make sure that the content provider is up
|
// FIXME: Make sure that the content provider is up
|
||||||
// This is the wrong place for it, but bleh - better than not having it turned on by default for people who don't go throughl ogin
|
// This is the wrong place for it, but bleh - better than not having it turned on by default for people who don't go throughl ogin
|
||||||
ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(), ModificationsContentProvider.AUTHORITY, true); // Enable sync by default!
|
ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(), ModificationsContentProvider.MODIFICATIONS_AUTHORITY, true); // Enable sync by default!
|
||||||
|
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|
@ -197,6 +217,10 @@ public class ShareActivity
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected boolean isNearbyUpload() {
|
||||||
|
return isNearbyUpload;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
@ -216,13 +240,17 @@ public class ShareActivity
|
||||||
//Receive intent from ContributionController.java when user selects picture to upload
|
//Receive intent from ContributionController.java when user selects picture to upload
|
||||||
Intent intent = getIntent();
|
Intent intent = getIntent();
|
||||||
|
|
||||||
if (intent.getAction().equals(Intent.ACTION_SEND)) {
|
if (Intent.ACTION_SEND.equals(intent.getAction())) {
|
||||||
mediaUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
mediaUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
||||||
if (intent.hasExtra(UploadService.EXTRA_SOURCE)) {
|
if (intent.hasExtra(UploadService.EXTRA_SOURCE)) {
|
||||||
source = intent.getStringExtra(UploadService.EXTRA_SOURCE);
|
source = intent.getStringExtra(UploadService.EXTRA_SOURCE);
|
||||||
} else {
|
} else {
|
||||||
source = Contribution.SOURCE_EXTERNAL;
|
source = Contribution.SOURCE_EXTERNAL;
|
||||||
}
|
}
|
||||||
|
if (intent.hasExtra("isDirectUpload")) {
|
||||||
|
Timber.d("This was initiated by a direct upload from Nearby");
|
||||||
|
isNearbyUpload = true;
|
||||||
|
}
|
||||||
mimeType = intent.getType();
|
mimeType = intent.getType();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -278,7 +306,7 @@ public class ShareActivity
|
||||||
REQUEST_PERM_ON_CREATE_LOCATION);
|
REQUEST_PERM_ON_CREATE_LOCATION);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
performPreuploadProcessingOfFile();
|
performPreUploadProcessingOfFile();
|
||||||
|
|
||||||
|
|
||||||
SingleUploadFragment shareView = (SingleUploadFragment) getSupportFragmentManager().findFragmentByTag("shareView");
|
SingleUploadFragment shareView = (SingleUploadFragment) getSupportFragmentManager().findFragmentByTag("shareView");
|
||||||
|
|
@ -302,7 +330,7 @@ public class ShareActivity
|
||||||
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
backgroundImageView.setImageURI(mediaUri);
|
backgroundImageView.setImageURI(mediaUri);
|
||||||
storagePermitted = true;
|
storagePermitted = true;
|
||||||
performPreuploadProcessingOfFile();
|
performPreUploadProcessingOfFile();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -310,7 +338,7 @@ public class ShareActivity
|
||||||
if (grantResults.length >= 1
|
if (grantResults.length >= 1
|
||||||
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
locationPermitted = true;
|
locationPermitted = true;
|
||||||
performPreuploadProcessingOfFile();
|
performPreUploadProcessingOfFile();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -319,12 +347,12 @@ public class ShareActivity
|
||||||
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
backgroundImageView.setImageURI(mediaUri);
|
backgroundImageView.setImageURI(mediaUri);
|
||||||
storagePermitted = true;
|
storagePermitted = true;
|
||||||
performPreuploadProcessingOfFile();
|
performPreUploadProcessingOfFile();
|
||||||
}
|
}
|
||||||
if (grantResults.length >= 2
|
if (grantResults.length >= 2
|
||||||
&& grantResults[1] == PackageManager.PERMISSION_GRANTED) {
|
&& grantResults[1] == PackageManager.PERMISSION_GRANTED) {
|
||||||
locationPermitted = true;
|
locationPermitted = true;
|
||||||
performPreuploadProcessingOfFile();
|
performPreUploadProcessingOfFile();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -335,7 +363,7 @@ public class ShareActivity
|
||||||
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
//It is OK to call this at both (1) and (4) because if perm had been granted at
|
//It is OK to call this at both (1) and (4) because if perm had been granted at
|
||||||
//snackbar, user should not be prompted at submit button
|
//snackbar, user should not be prompted at submit button
|
||||||
performPreuploadProcessingOfFile();
|
performPreUploadProcessingOfFile();
|
||||||
|
|
||||||
//Uploading only begins if storage permission granted from arrow icon
|
//Uploading only begins if storage permission granted from arrow icon
|
||||||
uploadBegins();
|
uploadBegins();
|
||||||
|
|
@ -346,7 +374,7 @@ public class ShareActivity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void performPreuploadProcessingOfFile() {
|
private void performPreUploadProcessingOfFile() {
|
||||||
if (!useNewPermissions || storagePermitted) {
|
if (!useNewPermissions || storagePermitted) {
|
||||||
if (!duplicateCheckPassed) {
|
if (!duplicateCheckPassed) {
|
||||||
//Test SHA1 of image to see if it matches SHA1 of a file on Commons
|
//Test SHA1 of image to see if it matches SHA1 of a file on Commons
|
||||||
|
|
@ -357,11 +385,21 @@ public class ShareActivity
|
||||||
Timber.d("File SHA1 is: %s", fileSHA1);
|
Timber.d("File SHA1 is: %s", fileSHA1);
|
||||||
|
|
||||||
ExistingFileAsync fileAsyncTask =
|
ExistingFileAsync fileAsyncTask =
|
||||||
new ExistingFileAsync(fileSHA1, this, result -> {
|
new ExistingFileAsync(new WeakReference<Activity>(this), fileSHA1, new WeakReference<Context>(this), result -> {
|
||||||
Timber.d("%s duplicate check: %s", mediaUri.toString(), result);
|
Timber.d("%s duplicate check: %s", mediaUri.toString(), result);
|
||||||
duplicateCheckPassed = (result == DUPLICATE_PROCEED
|
duplicateCheckPassed = (result == DUPLICATE_PROCEED
|
||||||
|| result == NO_DUPLICATE);
|
|| result == NO_DUPLICATE);
|
||||||
}, mwApi);
|
/*
|
||||||
|
TODO: 16/9/17 should we run DetectUnwantedPicturesAsync if DUPLICATE_PROCEED is returned? Since that means
|
||||||
|
we are processing images that are already on server???...
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (duplicateCheckPassed) {
|
||||||
|
//image can be uploaded, so now check if its a useless picture or not
|
||||||
|
performUnwantedPictureDetectionProcess();
|
||||||
|
}
|
||||||
|
|
||||||
|
},mwApi);
|
||||||
fileAsyncTask.execute();
|
fileAsyncTask.execute();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Timber.d(e, "IO Exception: ");
|
Timber.d(e, "IO Exception: ");
|
||||||
|
|
@ -375,6 +413,37 @@ public class ShareActivity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void performUnwantedPictureDetectionProcess() {
|
||||||
|
String imageMediaFilePath = FileUtils.getPath(this,mediaUri);
|
||||||
|
DetectUnwantedPicturesAsync detectUnwantedPicturesAsync = new DetectUnwantedPicturesAsync(imageMediaFilePath, result -> {
|
||||||
|
|
||||||
|
if (result != ImageUtils.Result.IMAGE_OK) {
|
||||||
|
//show appropriate error message
|
||||||
|
String errorMessage = result == ImageUtils.Result.IMAGE_DARK ? getString(R.string.upload_image_too_dark) : getString(R.string.upload_image_blurry);
|
||||||
|
AlertDialog.Builder errorDialogBuilder = new AlertDialog.Builder(this);
|
||||||
|
errorDialogBuilder.setMessage(errorMessage);
|
||||||
|
errorDialogBuilder.setTitle(getString(R.string.warning));
|
||||||
|
errorDialogBuilder.setPositiveButton(getString(R.string.no), (dialogInterface, i) -> {
|
||||||
|
//user does not wish to upload the picture, take them back to ContributionsActivity
|
||||||
|
Intent intent = new Intent(ShareActivity.this, ContributionsActivity.class);
|
||||||
|
dialogInterface.dismiss();
|
||||||
|
startActivity(intent);
|
||||||
|
});
|
||||||
|
errorDialogBuilder.setNegativeButton(getString(R.string.yes), (dialogInterface, i) -> {
|
||||||
|
//user wishes to go ahead with the upload of this picture, just dismiss this dialog
|
||||||
|
dialogInterface.dismiss();
|
||||||
|
});
|
||||||
|
|
||||||
|
AlertDialog errorDialog = errorDialogBuilder.create();
|
||||||
|
if (!isFinishing()) {
|
||||||
|
errorDialog.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
detectUnwantedPicturesAsync.execute();
|
||||||
|
}
|
||||||
|
|
||||||
private Snackbar requestPermissionUsingSnackBar(String rationale,
|
private Snackbar requestPermissionUsingSnackBar(String rationale,
|
||||||
final String[] perms,
|
final String[] perms,
|
||||||
final int code) {
|
final int code) {
|
||||||
|
|
@ -452,13 +521,93 @@ public class ShareActivity
|
||||||
if (imageObj != null) {
|
if (imageObj != null) {
|
||||||
// Gets image coords from exif data or user location
|
// Gets image coords from exif data or user location
|
||||||
decimalCoords = imageObj.getCoords(gpsEnabled);
|
decimalCoords = imageObj.getCoords(gpsEnabled);
|
||||||
useImageCoords();
|
if(decimalCoords==null || !imageObj.imageCoordsExists){
|
||||||
|
// Check if the location is from GPS or EXIF
|
||||||
|
// Find other photos taken around the same time which has gps coordinates
|
||||||
|
Timber.d("EXIF:false");
|
||||||
|
Timber.d("EXIF call"+(imageObj==tempImageObj));
|
||||||
|
if(!haveCheckedForOtherImages)
|
||||||
|
findOtherImages(gpsEnabled);// Do not do repeat the process
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// As the selected image has GPS data in EXIF go ahead with the same.
|
||||||
|
useImageCoords();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (FileNotFoundException e) {
|
} catch (FileNotFoundException e) {
|
||||||
Timber.w("File not found: " + mediaUri, e);
|
Timber.w("File not found: " + mediaUri, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void findOtherImages(boolean gpsEnabled) {
|
||||||
|
Timber.d("filePath"+getPathOfMediaOrCopy());
|
||||||
|
String filePath = getPathOfMediaOrCopy();
|
||||||
|
long timeOfCreation = new File(filePath).lastModified();//Time when the original image was created
|
||||||
|
File folder = new File(filePath.substring(0,filePath.lastIndexOf('/')));
|
||||||
|
File[] files = folder.listFiles();
|
||||||
|
Timber.d("folderTime Number:"+files.length);
|
||||||
|
|
||||||
|
for(File file : files){
|
||||||
|
if(file.lastModified()-timeOfCreation<=(120*1000) && file.lastModified()-timeOfCreation>=-(120*1000)){
|
||||||
|
//Make sure the photos were taken within 20seconds
|
||||||
|
Timber.d("fild date:"+file.lastModified()+ " time of creation"+timeOfCreation);
|
||||||
|
tempImageObj = null;//Temporary GPSExtractor to extract coords from these photos
|
||||||
|
ParcelFileDescriptor descriptor
|
||||||
|
= null;
|
||||||
|
try {
|
||||||
|
descriptor = getContentResolver().openFileDescriptor(Uri.parse(file.getAbsolutePath()), "r");
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
if (descriptor != null) {
|
||||||
|
tempImageObj = new GPSExtractor(descriptor.getFileDescriptor(),this, prefs);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (filePath != null) {
|
||||||
|
tempImageObj = new GPSExtractor(file.getAbsolutePath(), this, prefs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(tempImageObj!=null){
|
||||||
|
Timber.d("not null fild EXIF"+tempImageObj.imageCoordsExists +" coords"+tempImageObj.getCoords(gpsEnabled));
|
||||||
|
if(tempImageObj.getCoords(gpsEnabled)!=null && tempImageObj.imageCoordsExists){
|
||||||
|
// Current image has gps coordinates and it's not current gps locaiton
|
||||||
|
Timber.d("This fild has image coords:"+ file.getAbsolutePath());
|
||||||
|
// Create a dialog fragment for the suggestion
|
||||||
|
FragmentManager fragmentManager = getSupportFragmentManager();
|
||||||
|
SimilarImageDialogFragment newFragment = new SimilarImageDialogFragment();
|
||||||
|
Bundle args = new Bundle();
|
||||||
|
args.putString("originalImagePath",filePath);
|
||||||
|
args.putString("possibleImagePath",file.getAbsolutePath());
|
||||||
|
newFragment.setArguments(args);
|
||||||
|
newFragment.show(fragmentManager, "dialog");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
haveCheckedForOtherImages = true; //Finished checking for other images
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPostiveResponse() {
|
||||||
|
imageObj = tempImageObj;
|
||||||
|
decimalCoords = imageObj.getCoords(false);// Not necessary to use gps as image already ha EXIF data
|
||||||
|
Timber.d("EXIF from tempImageObj");
|
||||||
|
useImageCoords();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNegativeResponse() {
|
||||||
|
Timber.d("EXIF from imageObj");
|
||||||
|
useImageCoords();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initiates retrieval of image coordinates or user coordinates, and caching of coordinates.
|
* Initiates retrieval of image coordinates or user coordinates, and caching of coordinates.
|
||||||
* Then initiates the calls to MediaWiki API through an instance of MwVolleyApi.
|
* Then initiates the calls to MediaWiki API through an instance of MwVolleyApi.
|
||||||
|
|
@ -466,6 +615,7 @@ public class ShareActivity
|
||||||
public void useImageCoords() {
|
public void useImageCoords() {
|
||||||
if (decimalCoords != null) {
|
if (decimalCoords != null) {
|
||||||
Timber.d("Decimal coords of image: %s", decimalCoords);
|
Timber.d("Decimal coords of image: %s", decimalCoords);
|
||||||
|
Timber.d("is EXIF data present:"+imageObj.imageCoordsExists+" from findOther image:"+(imageObj==tempImageObj));
|
||||||
|
|
||||||
// Only set cache for this point if image has coords
|
// Only set cache for this point if image has coords
|
||||||
if (imageObj.imageCoordsExists) {
|
if (imageObj.imageCoordsExists) {
|
||||||
|
|
@ -489,7 +639,10 @@ public class ShareActivity
|
||||||
Timber.d("Cache found, setting categoryList in MwVolleyApi to %s", displayCatList);
|
Timber.d("Cache found, setting categoryList in MwVolleyApi to %s", displayCatList);
|
||||||
MwVolleyApi.setGpsCat(displayCatList);
|
MwVolleyApi.setGpsCat(displayCatList);
|
||||||
}
|
}
|
||||||
|
}else{
|
||||||
|
Timber.d("EXIF: no coords");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
package fr.free.nrw.commons.upload;
|
||||||
|
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.graphics.drawable.VectorDrawableCompat;
|
||||||
|
import android.support.v4.app.DialogFragment;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.Window;
|
||||||
|
import android.widget.Button;
|
||||||
|
|
||||||
|
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
|
||||||
|
import com.facebook.drawee.view.SimpleDraweeView;
|
||||||
|
import com.facebook.imagepipeline.listener.RequestListener;
|
||||||
|
import com.facebook.imagepipeline.listener.RequestLoggingListener;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.R;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by harisanker on 14/2/18.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class SimilarImageDialogFragment extends DialogFragment {
|
||||||
|
SimpleDraweeView originalImage;
|
||||||
|
SimpleDraweeView possibleImage;
|
||||||
|
Button positiveButton;
|
||||||
|
Button negativeButton;
|
||||||
|
onResponse mOnResponse;//Implemented interface from shareActivity
|
||||||
|
Boolean gotResponse = false;
|
||||||
|
public SimilarImageDialogFragment() {
|
||||||
|
}
|
||||||
|
public interface onResponse{
|
||||||
|
public void onPostiveResponse();
|
||||||
|
public void onNegativeResponse();
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||||
|
View view = inflater.inflate(R.layout.fragment_similar_image_dialog, container, false);
|
||||||
|
Set<RequestListener> requestListeners = new HashSet<>();
|
||||||
|
requestListeners.add(new RequestLoggingListener());
|
||||||
|
|
||||||
|
originalImage =(SimpleDraweeView) view.findViewById(R.id.orginalImage);
|
||||||
|
possibleImage =(SimpleDraweeView) view.findViewById(R.id.possibleImage);
|
||||||
|
positiveButton = (Button) view.findViewById(R.id.postive_button);
|
||||||
|
negativeButton = (Button) view.findViewById(R.id.negative_button);
|
||||||
|
|
||||||
|
originalImage.setHierarchy(GenericDraweeHierarchyBuilder
|
||||||
|
.newInstance(getResources())
|
||||||
|
.setPlaceholderImage(VectorDrawableCompat.create(getResources(),
|
||||||
|
R.drawable.ic_image_black_24dp,getContext().getTheme()))
|
||||||
|
.setFailureImage(VectorDrawableCompat.create(getResources(),
|
||||||
|
R.drawable.ic_error_outline_black_24dp, getContext().getTheme()))
|
||||||
|
.build());
|
||||||
|
possibleImage.setHierarchy(GenericDraweeHierarchyBuilder
|
||||||
|
.newInstance(getResources())
|
||||||
|
.setPlaceholderImage(VectorDrawableCompat.create(getResources(),
|
||||||
|
R.drawable.ic_image_black_24dp,getContext().getTheme()))
|
||||||
|
.setFailureImage(VectorDrawableCompat.create(getResources(),
|
||||||
|
R.drawable.ic_error_outline_black_24dp, getContext().getTheme()))
|
||||||
|
.build());
|
||||||
|
|
||||||
|
originalImage.setImageURI(Uri.fromFile(new File(getArguments().getString("originalImagePath"))));
|
||||||
|
possibleImage.setImageURI(Uri.fromFile(new File(getArguments().getString("possibleImagePath"))));
|
||||||
|
|
||||||
|
negativeButton.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View view) {
|
||||||
|
mOnResponse.onNegativeResponse();
|
||||||
|
gotResponse = true;
|
||||||
|
dismiss();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
positiveButton.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View view) {
|
||||||
|
mOnResponse.onPostiveResponse();
|
||||||
|
gotResponse = true;
|
||||||
|
dismiss();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
mOnResponse = (onResponse) getActivity();//Interface Implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||||
|
Dialog dialog = super.onCreateDialog(savedInstanceState);
|
||||||
|
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||||
|
return dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDismiss(DialogInterface dialog) {
|
||||||
|
// I user dismisses dialog by pressing outside the dialog.
|
||||||
|
if(!gotResponse)
|
||||||
|
mOnResponse.onNegativeResponse();
|
||||||
|
super.onDismiss(dialog);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
package fr.free.nrw.commons.upload;
|
package fr.free.nrw.commons.upload;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.Activity;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
|
@ -8,9 +11,11 @@ import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.v4.view.ViewCompat;
|
||||||
import android.support.v7.app.AlertDialog;
|
import android.support.v7.app.AlertDialog;
|
||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
import android.text.TextWatcher;
|
import android.text.TextWatcher;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
|
|
@ -25,6 +30,7 @@ import android.widget.Button;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.Spinner;
|
import android.widget.Spinner;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
|
@ -36,16 +42,16 @@ import butterknife.ButterKnife;
|
||||||
import butterknife.OnClick;
|
import butterknife.OnClick;
|
||||||
import butterknife.OnItemSelected;
|
import butterknife.OnItemSelected;
|
||||||
import butterknife.OnTouch;
|
import butterknife.OnTouch;
|
||||||
import dagger.android.support.DaggerFragment;
|
|
||||||
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.CommonsDaggerSupportFragment;
|
||||||
import fr.free.nrw.commons.settings.Prefs;
|
import fr.free.nrw.commons.settings.Prefs;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
import static android.view.MotionEvent.ACTION_DOWN;
|
import static android.view.MotionEvent.ACTION_DOWN;
|
||||||
import static android.view.MotionEvent.ACTION_UP;
|
import static android.view.MotionEvent.ACTION_UP;
|
||||||
|
|
||||||
public class SingleUploadFragment extends DaggerFragment {
|
public class SingleUploadFragment extends CommonsDaggerSupportFragment {
|
||||||
|
|
||||||
@BindView(R.id.titleEdit) EditText titleEdit;
|
@BindView(R.id.titleEdit) EditText titleEdit;
|
||||||
@BindView(R.id.descEdit) EditText descEdit;
|
@BindView(R.id.descEdit) EditText descEdit;
|
||||||
|
|
@ -54,6 +60,7 @@ public class SingleUploadFragment extends DaggerFragment {
|
||||||
@BindView(R.id.licenseSpinner) Spinner licenseSpinner;
|
@BindView(R.id.licenseSpinner) Spinner licenseSpinner;
|
||||||
|
|
||||||
@Inject @Named("default_preferences") SharedPreferences prefs;
|
@Inject @Named("default_preferences") SharedPreferences prefs;
|
||||||
|
@Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs;
|
||||||
|
|
||||||
private String license;
|
private String license;
|
||||||
private OnUploadActionInitiated uploadActionInitiatedHandler;
|
private OnUploadActionInitiated uploadActionInitiatedHandler;
|
||||||
|
|
@ -62,9 +69,6 @@ public class SingleUploadFragment extends DaggerFragment {
|
||||||
@Override
|
@Override
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||||
inflater.inflate(R.menu.activity_share, menu);
|
inflater.inflate(R.menu.activity_share, menu);
|
||||||
if (titleEdit != null) {
|
|
||||||
menu.findItem(R.id.menu_upload_single).setEnabled(titleEdit.getText().length() != 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -73,6 +77,11 @@ public class SingleUploadFragment extends DaggerFragment {
|
||||||
//What happens when the 'submit' icon is tapped
|
//What happens when the 'submit' icon is tapped
|
||||||
case R.id.menu_upload_single:
|
case R.id.menu_upload_single:
|
||||||
|
|
||||||
|
if (titleEdit.getText().toString().isEmpty()) {
|
||||||
|
Toast.makeText(getContext(), R.string.add_title_toast, Toast.LENGTH_LONG).show();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
String title = titleEdit.getText().toString();
|
String title = titleEdit.getText().toString();
|
||||||
String desc = descEdit.getText().toString();
|
String desc = descEdit.getText().toString();
|
||||||
|
|
||||||
|
|
@ -84,7 +93,6 @@ public class SingleUploadFragment extends DaggerFragment {
|
||||||
|
|
||||||
uploadActionInitiatedHandler.uploadActionInitiated(title, desc);
|
uploadActionInitiatedHandler.uploadActionInitiated(title, desc);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
|
|
@ -95,6 +103,14 @@ public class SingleUploadFragment extends DaggerFragment {
|
||||||
View rootView = inflater.inflate(R.layout.fragment_single_upload, container, false);
|
View rootView = inflater.inflate(R.layout.fragment_single_upload, container, false);
|
||||||
ButterKnife.bind(this, rootView);
|
ButterKnife.bind(this, rootView);
|
||||||
|
|
||||||
|
Intent activityIntent = getActivity().getIntent();
|
||||||
|
if (activityIntent.hasExtra("title")) {
|
||||||
|
titleEdit.setText(activityIntent.getStringExtra("title"));
|
||||||
|
}
|
||||||
|
if (activityIntent.hasExtra("description")) {
|
||||||
|
descEdit.setText(activityIntent.getStringExtra("description"));
|
||||||
|
}
|
||||||
|
|
||||||
ArrayList<String> licenseItems = new ArrayList<>();
|
ArrayList<String> licenseItems = new ArrayList<>();
|
||||||
licenseItems.add(getString(R.string.license_name_cc0));
|
licenseItems.add(getString(R.string.license_name_cc0));
|
||||||
licenseItems.add(getString(R.string.license_name_cc_by));
|
licenseItems.add(getString(R.string.license_name_cc_by));
|
||||||
|
|
@ -104,6 +120,18 @@ public class SingleUploadFragment extends DaggerFragment {
|
||||||
|
|
||||||
license = prefs.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3);
|
license = prefs.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3);
|
||||||
|
|
||||||
|
// If this is a direct upload from Nearby, autofill title and desc fields with the Place's values
|
||||||
|
boolean isNearbyUpload = ((ShareActivity) getActivity()).isNearbyUpload();
|
||||||
|
|
||||||
|
if (isNearbyUpload) {
|
||||||
|
String imageTitle = directPrefs.getString("Title", "");
|
||||||
|
String imageDesc = directPrefs.getString("Desc", "");
|
||||||
|
String imageCats = directPrefs.getString("Category", "");
|
||||||
|
Timber.d("Image title: " + imageTitle + ", image desc: " + imageDesc + ", image categories: " + imageCats);
|
||||||
|
titleEdit.setText(imageTitle);
|
||||||
|
descEdit.setText(imageDesc);
|
||||||
|
}
|
||||||
|
|
||||||
// check if this is the first time we have uploaded
|
// check if this is the first time we have uploaded
|
||||||
if (prefs.getString("Title", "").trim().length() == 0
|
if (prefs.getString("Title", "").trim().length() == 0
|
||||||
&& prefs.getString("Desc", "").trim().length() == 0) {
|
&& prefs.getString("Desc", "").trim().length() == 0) {
|
||||||
|
|
@ -136,11 +164,29 @@ public class SingleUploadFragment extends DaggerFragment {
|
||||||
|
|
||||||
titleEdit.addTextChangedListener(textWatcher);
|
titleEdit.addTextChangedListener(textWatcher);
|
||||||
|
|
||||||
|
titleEdit.setOnFocusChangeListener((v, hasFocus) -> {
|
||||||
|
if (!hasFocus) {
|
||||||
|
hideKeyboard(v);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
descEdit.setOnFocusChangeListener((v, hasFocus) -> {
|
||||||
|
if(!hasFocus){
|
||||||
|
hideKeyboard(v);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
setLicenseSummary(license);
|
setLicenseSummary(license);
|
||||||
|
|
||||||
return rootView;
|
return rootView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void hideKeyboard(View view) {
|
||||||
|
Log.i("hide", "hideKeyboard: ");
|
||||||
|
InputMethodManager inputMethodManager =(InputMethodManager)getActivity().getSystemService(Activity.INPUT_METHOD_SERVICE);
|
||||||
|
inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroyView() {
|
public void onDestroyView() {
|
||||||
titleEdit.removeTextChangedListener(textWatcher);
|
titleEdit.removeTextChangedListener(textWatcher);
|
||||||
|
|
@ -208,39 +254,45 @@ public class SingleUploadFragment extends DaggerFragment {
|
||||||
*/
|
*/
|
||||||
@OnTouch(R.id.titleEdit)
|
@OnTouch(R.id.titleEdit)
|
||||||
boolean titleInfo(View view, MotionEvent motionEvent) {
|
boolean titleInfo(View view, MotionEvent motionEvent) {
|
||||||
//Should replace right with end to support different right-to-left languages as well
|
final int value;
|
||||||
final int value = titleEdit.getRight() - titleEdit.getCompoundDrawables()[2].getBounds().width();
|
if (ViewCompat.getLayoutDirection(getView()) == ViewCompat.LAYOUT_DIRECTION_LTR) {
|
||||||
|
value = titleEdit.getRight() - titleEdit.getCompoundDrawables()[2].getBounds().width();
|
||||||
if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() >= value) {
|
if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() >= value) {
|
||||||
new AlertDialog.Builder(getContext())
|
showInfoAlert(R.string.media_detail_title, R.string.title_info);
|
||||||
.setTitle(R.string.media_detail_title)
|
return true;
|
||||||
.setMessage(R.string.title_info)
|
}
|
||||||
.setCancelable(true)
|
}
|
||||||
.setNeutralButton(android.R.string.ok, (dialog, id) -> dialog.cancel())
|
else {
|
||||||
.create()
|
value = titleEdit.getLeft() + titleEdit.getCompoundDrawables()[0].getBounds().width();
|
||||||
.show();
|
if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() <= value) {
|
||||||
return true;
|
showInfoAlert(R.string.media_detail_title, R.string.title_info);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnTouch(R.id.descEdit)
|
@OnTouch(R.id.descEdit)
|
||||||
boolean descriptionInfo(View view, MotionEvent motionEvent) {
|
boolean descriptionInfo(View view, MotionEvent motionEvent) {
|
||||||
final int value = descEdit.getRight() - descEdit.getCompoundDrawables()[2].getBounds().width();
|
final int value;
|
||||||
|
if (ViewCompat.getLayoutDirection(getView()) == ViewCompat.LAYOUT_DIRECTION_LTR) {
|
||||||
if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() >= value) {
|
value = descEdit.getRight() - descEdit.getCompoundDrawables()[2].getBounds().width();
|
||||||
new AlertDialog.Builder(getContext())
|
if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() >= value) {
|
||||||
.setTitle(R.string.media_detail_description)
|
showInfoAlert(R.string.media_detail_description,R.string.description_info);
|
||||||
.setMessage(R.string.description_info)
|
return true;
|
||||||
.setCancelable(true)
|
}
|
||||||
.setNeutralButton(android.R.string.ok, (dialog, id) -> dialog.cancel())
|
}
|
||||||
.create()
|
else{
|
||||||
.show();
|
value = descEdit.getLeft() + descEdit.getCompoundDrawables()[0].getBounds().width();
|
||||||
return true;
|
if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() <= value) {
|
||||||
|
showInfoAlert(R.string.media_detail_description,R.string.description_info);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("StringFormatInvalid")
|
||||||
private void setLicenseSummary(String license) {
|
private void setLicenseSummary(String license) {
|
||||||
licenseSummaryView.setText(getString(R.string.share_license_summary, getString(Utils.licenseNameFor(license))));
|
licenseSummaryView.setText(getString(R.string.share_license_summary, getString(Utils.licenseNameFor(license))));
|
||||||
}
|
}
|
||||||
|
|
@ -301,4 +353,14 @@ public class SingleUploadFragment extends DaggerFragment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void showInfoAlert (int titleStringID, int messageStringID){
|
||||||
|
new AlertDialog.Builder(getContext())
|
||||||
|
.setTitle(titleStringID)
|
||||||
|
.setMessage(messageStringID)
|
||||||
|
.setCancelable(true)
|
||||||
|
.setNeutralButton(android.R.string.ok, (dialog, id) -> dialog.cancel())
|
||||||
|
.create()
|
||||||
|
.show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
import android.content.res.AssetFileDescriptor;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
|
|
@ -49,7 +50,7 @@ public class UploadController {
|
||||||
private ServiceConnection uploadServiceConnection = new ServiceConnection() {
|
private ServiceConnection uploadServiceConnection = new ServiceConnection() {
|
||||||
@Override
|
@Override
|
||||||
public void onServiceConnected(ComponentName componentName, IBinder binder) {
|
public void onServiceConnected(ComponentName componentName, IBinder binder) {
|
||||||
uploadService = (UploadService) ((HandlerService.HandlerServiceLocalBinder)binder).getService();
|
uploadService = (UploadService) ((HandlerService.HandlerServiceLocalBinder) binder).getService();
|
||||||
isUploadServiceConnected = true;
|
isUploadServiceConnected = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,13 +82,14 @@ public class UploadController {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts a new upload task.
|
* Starts a new upload task.
|
||||||
* @param title the title of the contribution
|
*
|
||||||
* @param mediaUri the media URI of the contribution
|
* @param title the title of the contribution
|
||||||
* @param description the description of the contribution
|
* @param mediaUri the media URI of the contribution
|
||||||
* @param mimeType the MIME type of the contribution
|
* @param description the description of the contribution
|
||||||
* @param source the source of the contribution
|
* @param mimeType the MIME type of the contribution
|
||||||
|
* @param source the source of the contribution
|
||||||
* @param decimalCoords the coordinates in decimal. (e.g. "37.51136|-77.602615")
|
* @param decimalCoords the coordinates in decimal. (e.g. "37.51136|-77.602615")
|
||||||
* @param onComplete the progress tracker
|
* @param onComplete the progress tracker
|
||||||
*/
|
*/
|
||||||
public void startUpload(String title, Uri mediaUri, String description, String mimeType, String source, String decimalCoords, ContributionUploadProgress onComplete) {
|
public void startUpload(String title, Uri mediaUri, String description, String mimeType, String source, String decimalCoords, ContributionUploadProgress onComplete) {
|
||||||
Contribution contribution;
|
Contribution contribution;
|
||||||
|
|
@ -106,8 +108,9 @@ public class UploadController {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts a new upload task.
|
* Starts a new upload task.
|
||||||
|
*
|
||||||
* @param contribution the contribution object
|
* @param contribution the contribution object
|
||||||
* @param onComplete the progress tracker
|
* @param onComplete the progress tracker
|
||||||
*/
|
*/
|
||||||
public void startUpload(final Contribution contribution, final ContributionUploadProgress onComplete) {
|
public void startUpload(final Contribution contribution, final ContributionUploadProgress onComplete) {
|
||||||
//Set creator, desc, and license
|
//Set creator, desc, and license
|
||||||
|
|
@ -134,15 +137,17 @@ public class UploadController {
|
||||||
ContentResolver contentResolver = context.getContentResolver();
|
ContentResolver contentResolver = context.getContentResolver();
|
||||||
try {
|
try {
|
||||||
if (contribution.getDataLength() <= 0) {
|
if (contribution.getDataLength() <= 0) {
|
||||||
length = contentResolver
|
AssetFileDescriptor assetFileDescriptor = contentResolver
|
||||||
.openAssetFileDescriptor(contribution.getLocalUri(), "r")
|
.openAssetFileDescriptor(contribution.getLocalUri(), "r");
|
||||||
.getLength();
|
if (assetFileDescriptor != null) {
|
||||||
if (length == -1) {
|
length = assetFileDescriptor.getLength();
|
||||||
// Let us find out the long way!
|
if (length == -1) {
|
||||||
length = countBytes(contentResolver
|
// Let us find out the long way!
|
||||||
.openInputStream(contribution.getLocalUri()));
|
length = countBytes(contentResolver
|
||||||
|
.openInputStream(contribution.getLocalUri()));
|
||||||
|
}
|
||||||
|
contribution.setDataLength(length);
|
||||||
}
|
}
|
||||||
contribution.setDataLength(length);
|
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Timber.e(e, "IO Exception: ");
|
Timber.e(e, "IO Exception: ");
|
||||||
|
|
@ -152,7 +157,7 @@ public class UploadController {
|
||||||
Timber.e(e, "Security Exception: ");
|
Timber.e(e, "Security Exception: ");
|
||||||
}
|
}
|
||||||
|
|
||||||
String mimeType = (String)contribution.getTag("mimeType");
|
String mimeType = (String) contribution.getTag("mimeType");
|
||||||
Boolean imagePrefix = false;
|
Boolean imagePrefix = false;
|
||||||
|
|
||||||
if (mimeType == null || TextUtils.isEmpty(mimeType) || mimeType.endsWith("*")) {
|
if (mimeType == null || TextUtils.isEmpty(mimeType) || mimeType.endsWith("*")) {
|
||||||
|
|
@ -199,6 +204,7 @@ public class UploadController {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Counts the number of bytes in {@code stream}.
|
* Counts the number of bytes in {@code stream}.
|
||||||
|
*
|
||||||
* @param stream the stream
|
* @param stream the stream
|
||||||
* @return the number of bytes in {@code stream}
|
* @return the number of bytes in {@code stream}
|
||||||
* @throws IOException if an I/O error occurs
|
* @throws IOException if an I/O error occurs
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import android.annotation.SuppressLint;
|
||||||
import android.app.Notification;
|
import android.app.Notification;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.content.ContentProviderClient;
|
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
|
@ -52,9 +51,9 @@ public class UploadService extends HandlerService<Contribution> {
|
||||||
@Inject MediaWikiApi mwApi;
|
@Inject MediaWikiApi mwApi;
|
||||||
@Inject SessionManager sessionManager;
|
@Inject SessionManager sessionManager;
|
||||||
@Inject @Named("default_preferences") SharedPreferences prefs;
|
@Inject @Named("default_preferences") SharedPreferences prefs;
|
||||||
|
@Inject ContributionDao contributionDao;
|
||||||
|
|
||||||
private NotificationManager notificationManager;
|
private NotificationManager notificationManager;
|
||||||
private ContentProviderClient contributionsProviderClient;
|
|
||||||
private NotificationCompat.Builder curProgressNotification;
|
private NotificationCompat.Builder curProgressNotification;
|
||||||
private int toUpload;
|
private int toUpload;
|
||||||
|
|
||||||
|
|
@ -67,7 +66,6 @@ public class UploadService extends HandlerService<Contribution> {
|
||||||
public static final int NOTIFICATION_UPLOAD_IN_PROGRESS = 1;
|
public static final int NOTIFICATION_UPLOAD_IN_PROGRESS = 1;
|
||||||
public static final int NOTIFICATION_UPLOAD_COMPLETE = 2;
|
public static final int NOTIFICATION_UPLOAD_COMPLETE = 2;
|
||||||
public static final int NOTIFICATION_UPLOAD_FAILED = 3;
|
public static final int NOTIFICATION_UPLOAD_FAILED = 3;
|
||||||
private ContributionDao dao;
|
|
||||||
|
|
||||||
public UploadService() {
|
public UploadService() {
|
||||||
super("UploadService");
|
super("UploadService");
|
||||||
|
|
@ -107,7 +105,7 @@ public class UploadService extends HandlerService<Contribution> {
|
||||||
startForeground(NOTIFICATION_UPLOAD_IN_PROGRESS, curProgressNotification.build());
|
startForeground(NOTIFICATION_UPLOAD_IN_PROGRESS, curProgressNotification.build());
|
||||||
|
|
||||||
contribution.setTransferred(transferred);
|
contribution.setTransferred(transferred);
|
||||||
dao.save(contribution);
|
contributionDao.save(contribution);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -115,7 +113,6 @@ public class UploadService extends HandlerService<Contribution> {
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
contributionsProviderClient.release();
|
|
||||||
Timber.d("UploadService.onDestroy; %s are yet to be uploaded", unfinishedUploads);
|
Timber.d("UploadService.onDestroy; %s are yet to be uploaded", unfinishedUploads);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,8 +121,6 @@ public class UploadService extends HandlerService<Contribution> {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
|
|
||||||
notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||||
contributionsProviderClient = this.getContentResolver().acquireContentProviderClient(ContributionsContentProvider.AUTHORITY);
|
|
||||||
dao = new ContributionDao(contributionsProviderClient);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -147,7 +142,7 @@ public class UploadService extends HandlerService<Contribution> {
|
||||||
|
|
||||||
contribution.setState(Contribution.STATE_QUEUED);
|
contribution.setState(Contribution.STATE_QUEUED);
|
||||||
contribution.setTransferred(0);
|
contribution.setTransferred(0);
|
||||||
dao.save(contribution);
|
contributionDao.save(contribution);
|
||||||
toUpload++;
|
toUpload++;
|
||||||
if (curProgressNotification != null && toUpload != 1) {
|
if (curProgressNotification != null && toUpload != 1) {
|
||||||
curProgressNotification.setContentText(getResources().getQuantityString(R.plurals.uploads_pending_notification_indicator, toUpload, toUpload));
|
curProgressNotification.setContentText(getResources().getQuantityString(R.plurals.uploads_pending_notification_indicator, toUpload, toUpload));
|
||||||
|
|
@ -166,7 +161,7 @@ public class UploadService extends HandlerService<Contribution> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||||
if (intent.getAction().equals(ACTION_START_SERVICE) && freshStart) {
|
if (ACTION_START_SERVICE.equals(intent.getAction()) && freshStart) {
|
||||||
ContentValues failedValues = new ContentValues();
|
ContentValues failedValues = new ContentValues();
|
||||||
failedValues.put(ContributionDao.Table.COLUMN_STATE, Contribution.STATE_FAILED);
|
failedValues.put(ContributionDao.Table.COLUMN_STATE, Contribution.STATE_FAILED);
|
||||||
|
|
||||||
|
|
@ -262,7 +257,7 @@ public class UploadService extends HandlerService<Contribution> {
|
||||||
contribution.setImageUrl(uploadResult.getImageUrl());
|
contribution.setImageUrl(uploadResult.getImageUrl());
|
||||||
contribution.setState(Contribution.STATE_COMPLETED);
|
contribution.setState(Contribution.STATE_COMPLETED);
|
||||||
contribution.setDateUploaded(uploadResult.getDateUploaded());
|
contribution.setDateUploaded(uploadResult.getDateUploaded());
|
||||||
dao.save(contribution);
|
contributionDao.save(contribution);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Timber.d("I have a network fuckup");
|
Timber.d("I have a network fuckup");
|
||||||
|
|
@ -274,7 +269,7 @@ public class UploadService extends HandlerService<Contribution> {
|
||||||
toUpload--;
|
toUpload--;
|
||||||
if (toUpload == 0) {
|
if (toUpload == 0) {
|
||||||
// Sync modifications right after all uplaods are processed
|
// Sync modifications right after all uplaods are processed
|
||||||
ContentResolver.requestSync(sessionManager.getCurrentAccount(), ModificationsContentProvider.AUTHORITY, new Bundle());
|
ContentResolver.requestSync(sessionManager.getCurrentAccount(), ModificationsContentProvider.MODIFICATIONS_AUTHORITY, new Bundle());
|
||||||
stopForeground(true);
|
stopForeground(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -293,7 +288,7 @@ public class UploadService extends HandlerService<Contribution> {
|
||||||
notificationManager.notify(NOTIFICATION_UPLOAD_FAILED, failureNotification);
|
notificationManager.notify(NOTIFICATION_UPLOAD_FAILED, failureNotification);
|
||||||
|
|
||||||
contribution.setState(Contribution.STATE_FAILED);
|
contribution.setState(Contribution.STATE_FAILED);
|
||||||
dao.save(contribution);
|
contributionDao.save(contribution);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String findUniqueFilename(String fileName) throws IOException {
|
private String findUniqueFilename(String fileName) throws IOException {
|
||||||
|
|
|
||||||
36
app/src/main/java/fr/free/nrw/commons/utils/DateUtils.java
Normal file
36
app/src/main/java/fr/free/nrw/commons/utils/DateUtils.java
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
package fr.free.nrw.commons.utils;
|
||||||
|
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
public class DateUtils {
|
||||||
|
public static String getTimeAgo(Date currDate, Date itemDate) {
|
||||||
|
Calendar c = Calendar.getInstance();
|
||||||
|
c.setTime(currDate);
|
||||||
|
int yearNow = c.get(Calendar.YEAR);
|
||||||
|
int monthNow = c.get(Calendar.MONTH);
|
||||||
|
int dayNow = c.get(Calendar.DAY_OF_MONTH);
|
||||||
|
int hourNow = c.get(Calendar.HOUR_OF_DAY);
|
||||||
|
int minuteNow = c.get(Calendar.MINUTE);
|
||||||
|
c.setTime(itemDate);
|
||||||
|
int videoYear = c.get(Calendar.YEAR);
|
||||||
|
int videoMonth = c.get(Calendar.MONTH);
|
||||||
|
int videoDays = c.get(Calendar.DAY_OF_MONTH);
|
||||||
|
int videoHour = c.get(Calendar.HOUR_OF_DAY);
|
||||||
|
int videoMinute = c.get(Calendar.MINUTE);
|
||||||
|
|
||||||
|
if (yearNow != videoYear) {
|
||||||
|
return (String.valueOf(yearNow - videoYear) + "-" + "years");
|
||||||
|
} else if (monthNow != videoMonth) {
|
||||||
|
return (String.valueOf(monthNow - videoMonth) + "-" + "months");
|
||||||
|
} else if (dayNow != videoDays) {
|
||||||
|
return (String.valueOf(dayNow - videoDays) + "-" + "days");
|
||||||
|
} else if (hourNow != videoHour) {
|
||||||
|
return (String.valueOf(hourNow - videoHour) + "-" + "hours");
|
||||||
|
} else if (minuteNow != videoMinute) {
|
||||||
|
return (String.valueOf(minuteNow - videoMinute) + "-" + "minutes");
|
||||||
|
} else {
|
||||||
|
return "0-seconds";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,8 @@ public class ExecutorUtils {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public static Executor uiExecutor() { return uiExecutor; }
|
public static Executor uiExecutor() {
|
||||||
|
return uiExecutor;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,31 +4,34 @@ import android.os.Environment;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.io.OutputStreamWriter;
|
import java.io.OutputStreamWriter;
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
public class FileUtils {
|
public class FileUtils {
|
||||||
/**
|
/**
|
||||||
* Read and return the content of a resource file as string.
|
* Read and return the content of a resource file as string.
|
||||||
*
|
*
|
||||||
* @param fileName asset file's path (e.g. "/assets/queries/nearby_query.rq")
|
* @param fileName asset file's path (e.g. "/queries/nearby_query.rq")
|
||||||
* @return the content of the file
|
* @return the content of the file
|
||||||
*/
|
*/
|
||||||
public static String readFromResource(String fileName) throws IOException {
|
public static String readFromResource(String fileName) throws IOException {
|
||||||
StringBuilder buffer = new StringBuilder();
|
StringBuilder buffer = new StringBuilder();
|
||||||
BufferedReader reader = null;
|
BufferedReader reader = null;
|
||||||
try {
|
try {
|
||||||
reader = new BufferedReader(
|
InputStream inputStream = FileUtils.class.getResourceAsStream(fileName);
|
||||||
new InputStreamReader(
|
if (inputStream == null) {
|
||||||
CommonsApplication.class.getResourceAsStream(fileName), "UTF-8"));
|
throw new FileNotFoundException(fileName);
|
||||||
|
}
|
||||||
|
reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
|
||||||
String line;
|
String line;
|
||||||
while ((line = reader.readLine()) != null) {
|
while ((line = reader.readLine()) != null) {
|
||||||
buffer.append(line + "\n");
|
buffer.append(line).append("\n");
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (reader != null) {
|
if (reader != null) {
|
||||||
|
|
|
||||||
135
app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java
Normal file
135
app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
package fr.free.nrw.commons.utils;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.BitmapRegionDecoder;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
|
||||||
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by bluesir9 on 3/10/17.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class ImageUtils {
|
||||||
|
//atleast 50% of the image in question should be considered dark for the entire image to be dark
|
||||||
|
private static final double MINIMUM_DARKNESS_FACTOR = 0.50;
|
||||||
|
//atleast 50% of the image in question should be considered blurry for the entire image to be blurry
|
||||||
|
private static final double MINIMUM_BLURRYNESS_FACTOR = 0.50;
|
||||||
|
private static final int LAPLACIAN_VARIANCE_THRESHOLD = 70;
|
||||||
|
|
||||||
|
public enum Result {
|
||||||
|
IMAGE_DARK,
|
||||||
|
IMAGE_OK
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BitmapRegionDecoder allows us to process a large bitmap by breaking it down into smaller rectangles. The rectangles
|
||||||
|
* are obtained by setting an initial width, height and start position of the rectangle as a factor of the width and
|
||||||
|
* height of the original bitmap and then manipulating the width, height and position to loop over the entire original
|
||||||
|
* bitmap. Each individual rectangle is independently processed to check if its too dark. Based on
|
||||||
|
* the factor of "bright enough" individual rectangles amongst the total rectangles into which the image
|
||||||
|
* was divided, we will declare the image as wanted/unwanted
|
||||||
|
*
|
||||||
|
* @param bitmapRegionDecoder BitmapRegionDecoder for the image we wish to process
|
||||||
|
* @return Result.IMAGE_OK if image is neither dark nor blurry or if the input bitmapRegionDecoder provided is null
|
||||||
|
* Result.IMAGE_DARK if image is too dark
|
||||||
|
*/
|
||||||
|
public static Result checkIfImageIsTooDark(BitmapRegionDecoder bitmapRegionDecoder) {
|
||||||
|
if (bitmapRegionDecoder == null) {
|
||||||
|
Timber.e("Expected bitmapRegionDecoder was null");
|
||||||
|
return Result.IMAGE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int loadImageHeight = bitmapRegionDecoder.getHeight();
|
||||||
|
int loadImageWidth = bitmapRegionDecoder.getWidth();
|
||||||
|
|
||||||
|
int checkImageTopPosition = 0;
|
||||||
|
int checkImageBottomPosition = loadImageHeight / 10;
|
||||||
|
int checkImageLeftPosition = 0;
|
||||||
|
int checkImageRightPosition = loadImageWidth / 10;
|
||||||
|
|
||||||
|
int totalDividedRectangles = 0;
|
||||||
|
int numberOfDarkRectangles = 0;
|
||||||
|
|
||||||
|
while ((checkImageRightPosition <= loadImageWidth) && (checkImageLeftPosition < checkImageRightPosition)) {
|
||||||
|
while ((checkImageBottomPosition <= loadImageHeight) && (checkImageTopPosition < checkImageBottomPosition)) {
|
||||||
|
Timber.v("left: " + checkImageLeftPosition + " right: " + checkImageRightPosition + " top: " + checkImageTopPosition + " bottom: " + checkImageBottomPosition);
|
||||||
|
|
||||||
|
Rect rect = new Rect(checkImageLeftPosition,checkImageTopPosition,checkImageRightPosition,checkImageBottomPosition);
|
||||||
|
totalDividedRectangles++;
|
||||||
|
|
||||||
|
Bitmap processBitmap = bitmapRegionDecoder.decodeRegion(rect,null);
|
||||||
|
|
||||||
|
if (checkIfImageIsDark(processBitmap)) {
|
||||||
|
numberOfDarkRectangles++;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkImageTopPosition = checkImageBottomPosition;
|
||||||
|
checkImageBottomPosition += (checkImageBottomPosition < (loadImageHeight - checkImageBottomPosition)) ? checkImageBottomPosition : (loadImageHeight - checkImageBottomPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkImageTopPosition = 0; //reset to start
|
||||||
|
checkImageBottomPosition = loadImageHeight / 10; //reset to start
|
||||||
|
checkImageLeftPosition = checkImageRightPosition;
|
||||||
|
checkImageRightPosition += (checkImageRightPosition < (loadImageWidth - checkImageRightPosition)) ? checkImageRightPosition : (loadImageWidth - checkImageRightPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.d("dark rectangles count = " + numberOfDarkRectangles + ", total rectangles count = " + totalDividedRectangles);
|
||||||
|
|
||||||
|
if (numberOfDarkRectangles > totalDividedRectangles * MINIMUM_DARKNESS_FACTOR) {
|
||||||
|
return Result.IMAGE_DARK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.IMAGE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pulls the pixels into an array and then runs through it while checking the brightness of each pixel.
|
||||||
|
* The calculation of brightness of each pixel is done by extracting the RGB constituents of the pixel
|
||||||
|
* and then applying the formula to calculate its "Luminance". If this brightness value is less than
|
||||||
|
* 50 then the pixel is considered to be dark. Based on the MINIMUM_DARKNESS_FACTOR if enough pixels
|
||||||
|
* are dark then the entire bitmap is considered to be dark.
|
||||||
|
*
|
||||||
|
* <p>For more information on this brightness/darkness calculation technique refer the accepted answer
|
||||||
|
* on this -> https://stackoverflow.com/questions/35914461/how-to-detect-dark-photos-in-android/35914745
|
||||||
|
* SO question and follow the trail.
|
||||||
|
*
|
||||||
|
* @param bitmap The bitmap that needs to be checked.
|
||||||
|
* @return true if bitmap is dark or null, false if bitmap is bright
|
||||||
|
*/
|
||||||
|
private static boolean checkIfImageIsDark(Bitmap bitmap) {
|
||||||
|
if (bitmap == null) {
|
||||||
|
Timber.e("Expected bitmap was null");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int bitmapWidth = bitmap.getWidth();
|
||||||
|
int bitmapHeight = bitmap.getHeight();
|
||||||
|
|
||||||
|
int allPixelsCount = bitmapWidth * bitmapHeight;
|
||||||
|
int[] bitmapPixels = new int[allPixelsCount];
|
||||||
|
|
||||||
|
bitmap.getPixels(bitmapPixels,0,bitmapWidth,0,0,bitmapWidth,bitmapHeight);
|
||||||
|
boolean isImageDark = false;
|
||||||
|
int darkPixelsCount = 0;
|
||||||
|
|
||||||
|
for (int pixel : bitmapPixels) {
|
||||||
|
int r = Color.red(pixel);
|
||||||
|
int g = Color.green(pixel);
|
||||||
|
int b = Color.blue(pixel);
|
||||||
|
|
||||||
|
int brightness = (int) (0.2126 * r + 0.7152 * g + 0.0722 * b);
|
||||||
|
if (brightness < 50) {
|
||||||
|
//pixel is dark
|
||||||
|
darkPixelsCount++;
|
||||||
|
if (darkPixelsCount > allPixelsCount * MINIMUM_DARKNESS_FACTOR) {
|
||||||
|
isImageDark = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isImageDark;
|
||||||
|
}
|
||||||
|
}
|
||||||
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