Synced branch with master

This commit is contained in:
Vishan Seru 2018-02-18 00:32:25 +05:30
commit 4fc3040d52
479 changed files with 11666 additions and 3806 deletions

View file

@ -17,14 +17,17 @@ jdk:
android:
components:
- platform-tools
- tools
- build-tools-26.0.1
- platform-tools
- build-tools-26.0.2
- extra-google-m2repository
- extra-android-m2repository
- ${ANDROID_TARGET}
- android-25
- android-26
- sys-img-${ANDROID_ABI}-${ANDROID_TARGET}
licenses:
- 'android-sdk-license-.+'
before_script:
- echo no | android create avd --force -n test -t $ANDROID_TARGET --abi $ANDROID_ABI
@ -32,14 +35,16 @@ before_script:
- android-wait-for-emulator
script:
- ./gradlew clean check connectedCheck jacocoTestReport --stacktrace
- ./gradlew clean check connectedCheck jacocoTestReport
after_success:
- bash <(curl -s https://codecov.io/bash)
after_failure:
- echo '*** Connected Test Rsults ***'
- w3m -dump ${TRAVIS_BUILD_DIR}/app/build/reports/androidTests/connected/*Test.html
- echo '*** Debug Unit Test Results ***'
- w3m -dump ${TRAVIS_BUILD_DIR}/app/build/reports/tests/*/classes/*Test.html
- echo '*** Connected Test Results ***'
- w3m -dump ${TRAVIS_BUILD_DIR}/app/build/reports/androidTests/connected/flavors/*/*Test.html
before_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock

View file

@ -1,5 +1,47 @@
# Wikimedia Commons for Android
## v2.6.7
- Added null checks to prevent frequent crashes in ModificationsSyncAdapter
## v2.6.6
- Refactored Dagger to fix crashes encountered in production
- Fixed "?" displaying in description of Nearby places
- Database-related cleanup and tests
- Optimized dimens.xml
- Fixed issue where map opens with incorrect coordinates
## v2.6.5 beta
- Changed "send log" feature to only send logs to private Google group forum
- Switched to using Wikimedia maps server instead of Mapbox for privacy reasons
- Removed event logging from app for privacy reasons
- Fixed crash caused by rapidly switching from Nearby map to list while loading
## v2.6.4 beta
- Excluded httpclient and commons-logging to fix release build errors
- Fixed crashes caused by Fresco and Dagger
## v2.6.3 beta
- Same as 2.6.2 except with localizations added for Google Code-In
## v2.6.2 beta
- Reverted temporarily to last stable version while working on crash fix
## v2.6.1 beta
- Failed attempt to fix crashes in release build with the previous beta release
## v2.6.0 beta
- Multiple bugfixes for location updates and list/map loading in Nearby
- Multiple fixes for various crashes and memory leaks
- Added several unit tests
- Modified About page to include WMF disclaimer and modified Privacy Policy link to point to our individual privacy policy
- Added option for users to send logs to developers (has to be manually activated by user)
- Converted PNGs to WebPs
- Improved login screen with new design and privacy policy link
- Improved category display, if a category has an exact name entered, it will be shown first
- New UI for Nearby list
- Added product flavors for production and the beta-cluster Wikimedia servers
- Various improvements to navigation flow and backstack
## v2.5.0 beta
- Added one-time popup for beta users to provide feedback on IEG renewal proposal
- Added link to Commons policies in ShareActivity

1
CONTRIBUTING.md Normal file
View file

@ -0,0 +1 @@
Please see our guidelines in the wiki: https://github.com/commons-app/apps-android-commons/wiki/Volunteers-welcome%21

951
CREDITS
View file

@ -29,6 +29,7 @@ their contribution to the product.
* Jan Piotrowski
* Bruke Mekuria Mulugeta
* Paul Hawke
* Vishan Seru
3rd party open source libraries used:
* Butterknife
@ -38,3 +39,953 @@ their contribution to the product.
3rd party open source apps from which significant code has been reused:
* Android Wikipedia app https://github.com/wikimedia/apps-android-wikipedia
===========================================================================
The Wikimedia Commons Android app uses portions of MapBox.
mapbox-gl-native copyright (c) 2014-2018 Mapbox.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
===========================================================================
Mapbox GL uses portions of Android Gesture Detectors Framework.
Copyright (c) 2012, Almer Thie
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
===========================================================================
Mapbox GL uses portions of Android Support Library.
Copyright (c) 2005-2013, The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
===========================================================================
Mapbox GL uses portions of Boost.
Distributed under the Boost Software License, Version 1.0.
http://www.boost.org/LICENSE_1_0.txt
===========================================================================
Mapbox GL uses portions of Clipper.
Author : Angus Johnson
Version : 6.1.3a
Date : 22 January 2014
Website : http://www.angusj.com
Copyright : Angus Johnson 2010-2014
License:
Use, modification & distribution is subject to Boost Software License Ver 1.
http://www.boost.org/LICENSE_1_0.txt
Attributions:
The code in this library is an extension of Bala Vatti's clipping algorithm:
"A generic solution to polygon clipping"
Communications of the ACM, Vol 35, Issue 7 (July 1992) pp 56-63.
http://portal.acm.org/citation.cfm?id=129906
Computer graphics and geometric modeling: implementation and algorithms
By Max K. Agoston
Springer; 1 edition (January 4, 2005)
http://books.google.com/books?q=vatti+clipping+agoston
See also:
"Polygon Offsetting by Computing Winding Numbers"
Paper no. DETC2005-85513 pp. 565-575
ASME 2005 International Design Engineering Technical Conferences
and Computers and Information in Engineering Conference (IDETC/CIE2005)
September 24-28, 2005 , Long Beach, California, USA
http://www.me.berkeley.edu/~mcmains/pubs/DAC05OffsetPolygon.pdf
===========================================================================
Mapbox GL uses portions of BugshotKit.
The MIT License (MIT)
Copyright (c) 2014 marcoarment
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
===========================================================================
Mapbox GL uses portions of CSS Color Parser.
(c) Dean McNamee <dean@gmail.com>, 2012.
C++ port by Konstantin Käfer <mail@kkaefer.com>, 2014.
https://github.com/deanm/css-color-parser-js
https://github.com/kkaefer/css-color-parser-cpp
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
===========================================================================
Mapbox GL uses portions of GLFW.
Copyright (c) 2002-2006 Marcus Geelnard
Copyright (c) 2006-2010 Camilla Berglund <elmindreda@elmindreda.org>
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would
be appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not
be misrepresented as being the original software.
3. This notice may not be removed or altered from any source
distribution.
===========================================================================
Mapbox GL uses portions of libc++.
The libc++ library is dual licensed under both the University of Illinois
"BSD-Like" license and the MIT license. As a user of this code you may choose
to use it under either license. As a contributor, you agree to allow your code
to be used under both.
Full text of the relevant licenses is included below.
====
University of Illinois/NCSA
Open Source License
Copyright (c) 2009-2015 by the contributors listed in CREDITS.TXT
All rights reserved.
Developed by:
LLVM Team
University of Illinois at Urbana-Champaign
http://llvm.org
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal with
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimers.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimers in the
documentation and/or other materials provided with the distribution.
* Neither the names of the LLVM Team, University of Illinois at
Urbana-Champaign, nor the names of its contributors may be used to
endorse or promote products derived from this Software without specific
prior written permission.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE
SOFTWARE.
====
Copyright (c) 2009-2014 by the contributors listed in CREDITS.TXT
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
===========================================================================
Mapbox GL uses portions of libcurl.
COPYRIGHT AND PERMISSION NOTICE
Copyright (c) 1996 - 2015, Daniel Stenberg, <daniel@haxx.se>.
All rights reserved.
Permission to use, copy, modify, and distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright
notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. IN
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
OR OTHER DEALINGS IN THE SOFTWARE.
Except as contained in this notice, the name of a copyright holder shall not
be used in advertising or otherwise to promote the sale, use or other dealings
in this Software without prior written authorization of the copyright holder.
===========================================================================
Mapbox GL uses portions of libjpeg-turbo.
This software is based in part on the work of the Independent JPEG Group.
Copyright (C)2009-2015 D. R. Commander. All Rights Reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
- Neither the name of the libjpeg-turbo Project nor the names of its
contributors may be used to endorse or promote products derived from this
software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS",
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
TurboJPEG/LJT: this implements the TurboJPEG API using libjpeg or libjpeg-turbo
===========================================================================
Mapbox GL uses portions of libpng.
This copy of the libpng notices is provided for your convenience. In case of
any discrepancy between this copy and the notices in the file png.h that is
included in the libpng distribution, the latter shall prevail.
COPYRIGHT NOTICE, DISCLAIMER, and LICENSE:
If you modify libpng you may insert additional notices immediately following
this sentence.
This code is released under the libpng license.
libpng versions 1.0.7, July 1, 2000, through 1.6.18, July 23, 2015, are
Copyright (c) 2000-2002, 2004, 2006-2015 Glenn Randers-Pehrson, and are
distributed according to the same disclaimer and license as libpng-1.0.6
with the following individuals added to the list of Contributing Authors:
Simon-Pierre Cadieux
Eric S. Raymond
Mans Rullgard
Cosmin Truta
Gilles Vollant
James Yu
and with the following additions to the disclaimer:
There is no warranty against interference with your enjoyment of the
library or against infringement. There is no warranty that our
efforts or the library will fulfill any of your particular purposes
or needs. This library is provided with all faults, and the entire
risk of satisfactory quality, performance, accuracy, and effort is with
the user.
libpng versions 0.97, January 1998, through 1.0.6, March 20, 2000, are
Copyright (c) 1998-2000 Glenn Randers-Pehrson, and are distributed according
to the same disclaimer and license as libpng-0.96, with the following
individuals added to the list of Contributing Authors:
Tom Lane
Glenn Randers-Pehrson
Willem van Schaik
libpng versions 0.89, June 1996, through 0.96, May 1997, are
Copyright (c) 1996-1997 Andreas Dilger, and are
distributed according to the same disclaimer and license as libpng-0.88,
with the following individuals added to the list of Contributing Authors:
John Bowler
Kevin Bracey
Sam Bushell
Magnus Holmgren
Greg Roelofs
Tom Tanner
libpng versions 0.5, May 1995, through 0.88, January 1996, are
Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc.
For the purposes of this copyright and license, "Contributing Authors"
is defined as the following set of individuals:
Andreas Dilger
Dave Martindale
Guy Eric Schalnat
Paul Schmidt
Tim Wegner
The PNG Reference Library is supplied "AS IS". The Contributing Authors
and Group 42, Inc. disclaim all warranties, expressed or implied,
including, without limitation, the warranties of merchantability and of
fitness for any purpose. The Contributing Authors and Group 42, Inc.
assume no liability for direct, indirect, incidental, special, exemplary,
or consequential damages, which may result from the use of the PNG
Reference Library, even if advised of the possibility of such damage.
Permission is hereby granted to use, copy, modify, and distribute this
source code, or portions hereof, for any purpose, without fee, subject
to the following restrictions:
1. The origin of this source code must not be misrepresented.
2. Altered versions must be plainly marked as such and must not
be misrepresented as being the original source.
3. This Copyright notice may not be removed or altered from any
source or altered source distribution.
The Contributing Authors and Group 42, Inc. specifically permit, without
fee, and encourage the use of this source code as a component to
supporting the PNG file format in commercial products. If you use this
source code in a product, acknowledgment is not required but would be
appreciated.
===========================================================================
Mapbox GL uses portions of libuv.
libuv is part of the Node project: http://nodejs.org/
libuv may be distributed alone under Node's license:
====
Copyright Joyent, Inc. and other Node contributors. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
====
This license applies to all parts of libuv that are not externally
maintained libraries.
The externally maintained libraries used by libuv are:
- tree.h (from FreeBSD), copyright Niels Provos. Two clause BSD license.
- inet_pton and inet_ntop implementations, contained in src/inet.c, are
copyright the Internet Systems Consortium, Inc., and licensed under the ISC
license.
- stdint-msvc2008.h (from msinttypes), copyright Alexander Chemeris. Three
clause BSD license.
- pthread-fixes.h, pthread-fixes.c, copyright Google Inc. and Sony Mobile
Communications AB. Three clause BSD license.
- android-ifaddrs.h, android-ifaddrs.c, copyright Berkeley Software Design
Inc, Kenneth MacKay and Emergya (Cloud4all, FP7/2007-2013, grant agreement
n° 289016). Three clause BSD license.
===========================================================================
Mapbox GL uses portions of libzip.
Copyright (C) 1999-2014 Dieter Baron and Thomas Klausner
The authors can be contacted at <libzip@nih.at>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
3. The names of the authors may not be used to endorse or promote
products derived from this software without specific prior
written permission.
THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS
OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
===========================================================================
Mapbox GL uses portions of LOST.
Copyright (c) 2014 Mapzen
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
===========================================================================
Mapbox GL uses portions of the Mapbox iOS SDK, which was derived from the
Route-Me open source project, including the Alpstein fork of it.
The Route-Me license appears below.
Copyright (c) 2008-2013, Route-Me Contributors
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
===========================================================================
Mapbox GL uses portions of nunicode.
Copyright (c) 2013 Aleksey Tulinov <aleksey.tulinov@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
===========================================================================
Mapbox GL uses portions of OkHTTP.
Copyright 2014 Square, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
===========================================================================
Mapbox GL uses portions of OpenSSL.
LICENSE ISSUES
==============
The OpenSSL toolkit stays under a dual license, i.e. both the conditions of
the OpenSSL License and the original SSLeay license apply to the toolkit.
See below for the actual license texts. Actually both licenses are BSD-style
Open Source licenses. In case of any license issues related to OpenSSL
please contact openssl-core@openssl.org.
OpenSSL License
---------------
Copyright (c) 1998-2011 The OpenSSL Project. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
3. All advertising materials mentioning features or use of this
software must display the following acknowledgment:
"This product includes software developed by the OpenSSL Project
for use in the OpenSSL Toolkit. (http://www.openssl.org/)"
4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to
endorse or promote products derived from this software without
prior written permission. For written permission, please contact
openssl-core@openssl.org.
5. Products derived from this software may not be called "OpenSSL"
nor may "OpenSSL" appear in their names without prior written
permission of the OpenSSL Project.
6. Redistributions of any form whatsoever must retain the following
acknowledgment:
"This product includes software developed by the OpenSSL Project
for use in the OpenSSL Toolkit (http://www.openssl.org/)"
THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY
EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR
ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
OF THE POSSIBILITY OF SUCH DAMAGE.
This product includes cryptographic software written by Eric Young
(eay@cryptsoft.com). This product includes software written by Tim
Hudson (tjh@cryptsoft.com).
Original SSLeay License
-----------------------
Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com)
All rights reserved.
This package is an SSL implementation written
by Eric Young (eay@cryptsoft.com).
The implementation was written so as to conform with Netscapes SSL.
This library is free for commercial and non-commercial use as long as
The following conditions are aheared to. The following conditions
apply to all code found in this distribution, be it the RC4, RSA,
lhash, DES, etc., code; not just the SSL code. The SSL documentation
included with this distribution is covered by the same copyright terms
except that the holder is Tim Hudson (tjh@cryptsoft.com).
Copyright remains Eric Young's, and as such any Copyright notices in
the code are not to be removed.
If this package is used in a product, Eric Young should be given attribution
as the author of the parts of the library used.
This can be in the form of a textual message at program startup or
in documentation (online or textual) provided with the package.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. All advertising materials mentioning features or use of this software
must display the following acknowledgement:
"This product includes cryptographic software written by
Eric Young (eay@cryptsoft.com)"
The word 'cryptographic' can be left out if the rouines from the library
being used are not cryptographic related :-).
4. If you include any Windows specific code (or a derivative thereof) from
the apps directory (application code) you must include an acknowledgement:
"This product includes software written by Tim Hudson (tjh@cryptsoft.com)"
THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
The licence and distribution terms for any publically available version or
derivative of this code cannot be changed. i.e. this code cannot simply be
copied and put under another distribution licence
[including the GNU Public Licence.]
===========================================================================
Mapbox GL uses portions of RapidJSON.
Tencent is pleased to support the open source community by making RapidJSON
available.
Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. All rights
reserved.
If you have downloaded a copy of the RapidJSON binary from Tencent, please note
that the RapidJSON binary is licensed under the MIT License. If you have
downloaded a copy of the RapidJSON source code from Tencent, please note that
RapidJSON source code is licensed under the MIT License, except for the third-
party components listed below which are subject to different license terms.
Your integration of RapidJSON into your own projects may require compliance with
the MIT License, as well as the other licenses applicable to the third-party
components included within RapidJSON. To avoid the problematic JSON license in
your own projects, it's sufficient to exclude the bin/jsonchecker/ directory, as
it's the only code under the JSON license. A copy of the MIT License is included
in this file.
Other dependencies and licenses:
Open Source Software Licensed Under the BSD License:
--------------------------------------------------------------------
The msinttypes r29
Copyright (c) 2006-2013 Alexander Chemeris
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of copyright holder nor the names of its contributors may be
used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Open Source Software Licensed Under the JSON License:
--------------------------------------------------------------------
json.org
Copyright (c) 2002 JSON.org
All Rights Reserved.
JSON_checker
Copyright (c) 2002 JSON.org
All Rights Reserved.
Terms of the JSON License:
---------------------------------------------------
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
The Software shall be used for Good, not Evil.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Terms of the MIT License:
--------------------------------------------------------------------
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
===========================================================================
Mapbox GL uses portions of Reachability.
Copyright (c) 2011, Tony Million.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
===========================================================================
Mapbox GL uses portions of SQLite.
2001 September 15
The author disclaims copyright to this source code. In place of
a legal notice, here is a blessing:
May you do good and not evil.
May you find forgiveness for yourself and forgive others.
May you share freely, never taking more than you give.
===========================================================================
Mapbox GL uses portions of SVPulsingAnnotationView.
Copyright (c) 2013, Sam Vermette <hello@samvermette.com>
Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright notice
and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.
===========================================================================
Mapbox GL uses portions of zlib.
Acknowledgments:
The deflate format used by zlib was defined by Phil Katz. The deflate and
zlib specifications were written by L. Peter Deutsch. Thanks to all the
people who reported problems and suggested various improvements in zlib; they
are too numerous to cite here.
Copyright notice:
(C) 1995-2013 Jean-loup Gailly and Mark Adler
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
Jean-loup Gailly Mark Adler
jloup@gzip.org madler@alumni.caltech.edu
===========================================================================
Mapbox GL uses portions of Realm Objective-C.
Copyright 2015 Realm Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

35
ISSUE_TEMPLATE.md Normal file
View file

@ -0,0 +1,35 @@
_Before creating an issue, please search the existing issues to see if a similar one has already been created. You can search issues by specific labels (e.g. `label:nearby `) or just by typing keywords into the search filter._
**Summary:**
Summarize your issue in one sentence (what goes wrong, what did you expect to happen)
**Steps to reproduce:**
How can we reproduce the issue?
**Add System logs:**
Add logcat files here (if possible).
**Expected behavior:**
What did you expect the App to do?
**Observed behavior:**
What did you see instead? Describe your issue in detail here.
**Device and Android version:**
What make and model device (e.g., Samsung J7) did you encounter this on? What Android
version (e.g., Android 4.0 Ice Cream Sandwich or Android 6.0 Marshmallow) are you running? Is it
the stock version from the manufacturer or a custom ROM ?
**Commons app version:**
You can find this information by going to the navigation drawer in the app and tapping 'About'
**Screen-shots:**
Can be created by pressing the Volume Down and Power Button at the same time on Android 4.0 and higher.

View file

@ -2,9 +2,7 @@
The Wikimedia Commons Android app allows users to upload pictures from their Android phone/tablet to Wikimedia Commons. Download the app [here][1], or view our [website][2].
Initially started by the Wikimedia Foundation, this app is now maintained by volunteers. Anyone is welcome to improve it, just choose among the [open issues][3] and send us a pull request :-)
We are currently applying for an [IEG renewal][10] to work on the app for the next 6 months. Feedback is very much welcomed.
Initially started by the Wikimedia Foundation, this app is now maintained by grantees and volunteers of the Wikimedia community. Anyone is welcome to improve it, just choose among the [open issues][3] and send us a pull request :-)
<a href="https://f-droid.org/repository/browse/?fdid=fr.free.nrw.commons" target="_blank">
<img src="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="90"/></a>

View file

@ -1,58 +1,78 @@
apply from: '../gitutils.gradle'
apply plugin: 'com.android.application'
apply plugin: 'me.tatarka.retrolambda'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'jacoco-android'
apply from: 'quality.gradle'
apply plugin: 'com.getkeepsafe.dexcount'
dependencies {
compile 'com.github.nicolas-raoul:Quadtree:ac16ea8035bf07'
compile 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar'
compile 'in.yuvi:http.fluent:1.3'
compile 'com.android.volley:volley:1.0.0'
compile 'ch.acra:acra:4.7.0'
compile 'org.mediawiki:api:1.3'
compile 'commons-codec:commons-codec:1.10'
compile 'com.github.pedrovgs:renderers:3.3.3'
compile 'com.google.code.gson:gson:2.8.0'
compile 'com.jakewharton.timber:timber:4.5.1'
compile 'info.debatty:java-string-similarity:0.24'
compile ('com.mapbox.mapboxsdk:mapbox-android-sdk:5.1.0@aar'){
implementation 'com.github.nicolas-raoul:Quadtree:ac16ea8035bf07'
implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar'
implementation 'in.yuvi:http.fluent:1.3'
implementation 'com.android.volley:volley:1.0.0'
implementation 'ch.acra:acra:4.7.0'
implementation 'org.mediawiki:api:1.3'
implementation 'commons-codec:commons-codec:1.10'
implementation 'com.github.pedrovgs:renderers:3.3.3'
implementation 'com.google.code.gson:gson:2.8.1'
implementation 'com.jakewharton.timber:timber:4.5.1'
implementation 'info.debatty:java-string-similarity:0.24'
implementation ('com.mapbox.mapboxsdk:mapbox-android-sdk:5.2.1@aar'){
transitive=true
}
compile "com.android.support:support-v4:${project.supportLibVersion}"
compile "com.android.support:appcompat-v7:${project.supportLibVersion}"
compile "com.android.support:design:${project.supportLibVersion}"
compile "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION"
annotationProcessor "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
implementation "com.android.support:support-v4:$SUPPORT_LIB_VERSION"
implementation "com.android.support:appcompat-v7:$SUPPORT_LIB_VERSION"
implementation "com.android.support:design:$SUPPORT_LIB_VERSION"
compile 'com.squareup.okhttp3:okhttp:3.8.1'
compile 'com.squareup.okio:okio:1.13.0'
implementation "com.android.support:cardview-v7:$SUPPORT_LIB_VERSION"
compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION"
kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
implementation 'com.squareup.okhttp3:okhttp:3.8.1'
implementation 'com.squareup.okio:okio:1.13.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
// Because RxAndroid releases are few and far between, it is recommended you also
// explicitly depend on RxJava's latest version for bug fixes and new features.
compile 'io.reactivex.rxjava2:rxjava:2.1.2'
compile 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
compile 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.0.0'
compile 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.0.0'
compile 'com.jakewharton.rxbinding2:rxbinding-design:2.0.0'
implementation 'io.reactivex.rxjava2:rxjava:2.1.2'
implementation 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
implementation 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.0.0'
implementation 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.0.0'
implementation 'com.jakewharton.rxbinding2:rxbinding-design:2.0.0'
compile 'com.facebook.fresco:fresco:1.3.0'
compile 'com.facebook.stetho:stetho:1.5.0'
implementation 'com.facebook.fresco:fresco:1.5.0'
implementation 'com.facebook.stetho:stetho:1.5.0'
testCompile 'junit:junit:4.12'
testCompile 'org.robolectric:robolectric:3.3.2'
implementation "com.google.dagger:dagger:$DAGGER_VERSION"
implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
testCompile 'com.squareup.okhttp3:mockwebserver:3.8.1'
androidTestCompile 'com.squareup.okhttp3:mockwebserver:3.8.1'
androidTestCompile "com.android.support:support-annotations:${project.supportLibVersion}"
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'
kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
testImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
testImplementation 'junit:junit:4.12'
testImplementation 'org.robolectric:robolectric:3.4'
testImplementation 'org.mockito:mockito-all:1.10.19'
testImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1'
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1'
androidTestImplementation "com.android.support:support-annotations:$SUPPORT_LIB_VERSION"
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
debugImplementation "com.squareup.leakcanary:leakcanary-android:$LEAK_CANARY"
releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY"
testImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY"
implementation "com.google.dagger:dagger:$DAGGER_VERSION"
implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
}
android {
@ -63,24 +83,38 @@ android {
defaultConfig {
applicationId 'fr.free.nrw.commons'
versionCode 74
versionName '2.5.0'
versionCode 82
versionName '2.6.7'
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
minSdkVersion project.minSdkVersion
targetSdkVersion project.targetSdkVersion
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
}
sourceSets {
// use kotlin only in tests (for now)
test.java.srcDirs += 'src/test/kotlin'
// use main assets and resources in test
test.assets.srcDirs += 'src/main/assets'
test.resources.srcDirs += 'src/main/resoures'
}
buildTypes {
release {
minifyEnabled false // See https://stackoverflow.com/questions/40232404/google-play-apk-and-android-studio-apk-usb-debug-behaving-differently - proguard.cfg modification alone insufficient.
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
debug {
applicationIdSuffix ".debug"
testCoverageEnabled true
versionNameSuffix "-debug-" + getBranchName() + "~" + getBuildVersion()
}
}
flavorDimensions 'tier'
productFlavors {
prod {
buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.org/w/api.php\""
@ -92,6 +126,7 @@ android {
buildConfigField "String", "EVENTLOG_WIKI", "\"commonswiki\""
buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\""
buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Main_Page&welcome=yes\""
dimension 'tier'
}
beta {
@ -105,6 +140,7 @@ android {
buildConfigField "String", "EVENTLOG_WIKI", "\"commonswiki\""
buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\""
buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Main_Page&welcome=yes\""
dimension 'tier'
}
}
@ -122,5 +158,8 @@ android {
//FIXME: Temporary fix for https://github.com/commons-app/apps-android-commons/issues/709
configurations.all {
resolutionStrategy.force 'com.android.support:support-annotations:25.2.0'
exclude module: 'httpclient'
exclude module: 'commons-logging'
}
buildToolsVersion buildToolsVersion
}

View file

@ -1,30 +0,0 @@
package fr.free.nrw.commons;
import android.support.test.espresso.assertion.ViewAssertions;
import android.support.test.filters.LargeTest;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import fr.free.nrw.commons.nearby.NearbyActivity;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
@LargeTest
@RunWith(AndroidJUnit4.class)
public class NearbyActivityTest {
@Rule
public final ActivityTestRule<NearbyActivity> nearby =
new ActivityTestRule<>(NearbyActivity.class);
@Test
public void testActivityLaunch() {
onView(withText(R.string.title_activity_nearby))
.check(ViewAssertions.matches(isDisplayed()));
}
}

View file

@ -0,0 +1,30 @@
package fr.free.nrw.commons.upload;
import android.net.Uri;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import fr.free.nrw.commons.BuildConfig;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
@RunWith(AndroidJUnit4.class)
public class FileUtilsTest {
@Test
public void isSelfOwned() throws Exception {
Uri uri = Uri.parse("content://" + BuildConfig.APPLICATION_ID + ".provider/document/1");
boolean selfOwned = FileUtils.isSelfOwned(InstrumentationRegistry.getTargetContext(), uri);
assertThat(selfOwned, is(true));
}
@Test
public void isNotSelfOwned() throws Exception {
Uri uri = Uri.parse("content://com.android.providers.media.documents/document/1");
boolean selfOwned = FileUtils.isSelfOwned(InstrumentationRegistry.getTargetContext(), uri);
assertThat(selfOwned, is(false));
}
}

View file

@ -2,24 +2,28 @@
package="fr.free.nrw.commons">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
<uses-permission android:name="android.permission.READ_SYNC_STATS"/>
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.READ_SYNC_STATS" />
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
<uses-permission android:name="android.permission.GET_ACCOUNTS"/>
<uses-permission android:name="android.permission.USE_CREDENTIALS"/>
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS"/>
<uses-permission android:name="android.permission.MANAGE_DOCUMENTS"/>
<uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS"/>
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
<uses-permission android:name="android.permission.MANAGE_DOCUMENTS" />
<uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" />
<uses-permission android:name="android.permission.READ_LOGS"/>
<!-- Needed only if your app targets Android 5.0 (API level 21) or higher. -->
<uses-feature android:name="android.hardware.location.gps" />
<application
android:name=".CommonsApplication"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.AppCompat"
android:theme="@style/LightAppTheme"
android:supportsRtl="true" >
<activity android:name="org.acra.CrashReportDialog"
android:theme="@android:style/Theme.Dialog"
@ -27,23 +31,19 @@
android:excludeFromRecents="true"
android:finishOnTaskLaunch="true" />
<activity
android:name=".auth.LoginActivity"
>
<activity android:name=".auth.LoginActivity">
<intent-filter>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER" />
<action android:name="android.intent.action.MAIN" />
</intent-filter>
</activity>
<activity
android:name=".WelcomeActivity"
>
</activity>
<activity android:name=".WelcomeActivity" />
<activity
android:name=".upload.ShareActivity"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
>
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
@ -51,11 +51,11 @@
<data android:mimeType="audio/ogg" />
</intent-filter>
</activity>
<activity
android:name=".upload.MultipleShareActivity"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
>
android:name=".upload.MultipleShareActivity"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
@ -65,33 +65,38 @@
</activity>
<activity
android:name=".contributions.ContributionsActivity"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
>
</activity>
android:name=".contributions.ContributionsActivity"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" />
<activity
android:name=".settings.SettingsActivity"
android:label="@string/title_activity_settings"
/>
android:label="@string/title_activity_settings" />
<activity
android:name=".AboutActivity"
android:label="@string/title_activity_about"
android:parentActivityName=".contributions.ContributionsActivity" />
<activity
android:name=".auth.SignupActivity"
android:label="@string/title_activity_signup"/>
android:label="@string/title_activity_signup" />
<activity
android:name=".nearby.NearbyActivity"
android:label="@string/title_activity_nearby"
android:parentActivityName=".contributions.ContributionsActivity" />
<service android:name=".upload.UploadService" >
</service>
<activity
android:name=".notification.NotificationActivity"
android:label="@string/navigation_item_notification" />
<service android:name=".upload.UploadService" />
<service
android:name=".auth.WikiAccountAuthenticatorService"
android:exported="true"
android:process=":auth" >
android:process=":auth">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
@ -102,27 +107,25 @@
</service>
<service
android:name=".contributions.ContributionsSyncService"
android:exported="true">
android:name=".contributions.ContributionsSyncService"
android:exported="true">
<intent-filter>
<action
android:name="android.content.SyncAdapter" />
<action android:name="android.content.SyncAdapter" />
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/contributions_sync_adapter" />
android:name="android.content.SyncAdapter"
android:resource="@xml/contributions_sync_adapter" />
</service>
<service
android:name=".modifications.ModificationsSyncService"
android:exported="true">
android:name=".modifications.ModificationsSyncService"
android:exported="true">
<intent-filter>
<action
android:name="android.content.SyncAdapter" />
<action android:name="android.content.SyncAdapter" />
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/modifications_sync_adapter" />
android:name="android.content.SyncAdapter"
android:resource="@xml/modifications_sync_adapter" />
</service>
<provider
@ -132,31 +135,29 @@
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/>
android:resource="@xml/provider_paths" />
</provider>
<provider
android:name=".contributions.ContributionsContentProvider"
android:label="@string/provider_contributions"
android:syncable="true"
android:authorities="fr.free.nrw.commons.contributions.contentprovider"
android:exported="false">
</provider>
android:name=".contributions.ContributionsContentProvider"
android:authorities="fr.free.nrw.commons.contributions.contentprovider"
android:exported="false"
android:label="@string/provider_contributions"
android:syncable="true" />
<provider
android:name=".modifications.ModificationsContentProvider"
android:label="@string/provider_modifications"
android:syncable="true"
android:authorities="fr.free.nrw.commons.modifications.contentprovider"
android:exported="false">
</provider>
android:name=".modifications.ModificationsContentProvider"
android:authorities="fr.free.nrw.commons.modifications.contentprovider"
android:exported="false"
android:label="@string/provider_modifications"
android:syncable="true" />
<provider
android:name=".category.CategoryContentProvider"
android:label="@string/provider_categories"
android:syncable="false"
android:authorities="fr.free.nrw.commons.categories.contentprovider"
android:exported="false">
</provider>
android:name=".category.CategoryContentProvider"
android:authorities="fr.free.nrw.commons.categories.contentprovider"
android:exported="false"
android:label="@string/provider_categories"
android:syncable="false" />
</application>

View file

@ -0,0 +1,26 @@
{
"version": 8,
"sources": {
"wikimedia-osm": {
"type": "raster",
"tiles": [
"https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png"
],
"tileSize": 128
}
},
"layers": [
{
"id": "background",
"type": "background",
"paint": {
"background-color": "#606060"
}
},
{
"id": "osm",
"type": "raster",
"source": "wikimedia-osm"
}
]
}

View file

@ -1,19 +1,29 @@
package fr.free.nrw.commons;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.ui.widget.HtmlTextView;
/**
* Represents about screen of this app
*/
public class AboutActivity extends NavigationBaseActivity {
@BindView(R.id.about_version) TextView versionText;
@BindView(R.id.about_license) HtmlTextView aboutLicenseText;
/**
* This method helps in the creation About screen
*
* @param savedInstanceState Data bundle
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -21,15 +31,38 @@ public class AboutActivity extends NavigationBaseActivity {
ButterKnife.bind(this);
String aboutText = getString(R.string.about_license, getString(R.string.trademarked_name));
String aboutText = getString(R.string.about_license);
aboutLicenseText.setHtmlText(aboutText);
versionText.setText(BuildConfig.VERSION_NAME);
initDrawer();
}
public static void startYourself(Context context) {
Intent settingsIntent = new Intent(context, AboutActivity.class);
context.startActivity(settingsIntent);
@OnClick(R.id.facebook_launch_icon)
public void launchFacebook(View view) {
Intent intent;
try {
intent = new Intent(Intent.ACTION_VIEW, Uri.parse("fb://page/" + "1921335171459985"));
intent.setPackage("com.facebook.katana");
startActivity(intent);
} catch (Exception e) {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://www.facebook.com/" + "1921335171459985")));
}
}
@OnClick(R.id.github_launch_icon)
public void launchGithub(View view) {
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/commons-app/apps-android-commons\\"));
startActivity(browserIntent);
}
@OnClick(R.id.website_launch_icon)
public void launchWebsite(View view) {
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://commons-app.github.io/\\"));
startActivity(browserIntent);
}
}

View file

@ -1,40 +1,34 @@
package fr.free.nrw.commons;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.database.sqlite.SQLiteDatabase;
import android.preference.PreferenceManager;
import android.support.v4.util.LruCache;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.stetho.Stetho;
import com.squareup.leakcanary.LeakCanary;
import com.squareup.leakcanary.RefWatcher;
import org.acra.ACRA;
import org.acra.ReportingInteractionMode;
import org.acra.annotation.ReportsCrashes;
import java.io.File;
import java.io.IOException;
import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.caching.CacheController;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.data.Category;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.category.CategoryDao;
import fr.free.nrw.commons.contributions.ContributionDao;
import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.modifications.ModifierSequence;
import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.nearby.NearbyPlaces;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.di.CommonsApplicationComponent;
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
import fr.free.nrw.commons.utils.FileUtils;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
// TODO: Use ProGuard to rip out reporting when publishing
@ -48,86 +42,43 @@ import timber.log.Timber;
)
public class CommonsApplication extends Application {
private Account currentAccount = null; // Unlike a savings account...
@Inject SessionManager sessionManager;
@Inject DBOpenHelper dbOpenHelper;
public static final Object[] EVENT_UPLOAD_ATTEMPT = {"MobileAppUploadAttempts", 5334329L};
public static final Object[] EVENT_LOGIN_ATTEMPT = {"MobileAppLoginAttempts", 5257721L};
public static final Object[] EVENT_SHARE_ATTEMPT = {"MobileAppShareAttempts", 5346170L};
public static final Object[] EVENT_CATEGORIZATION_ATTEMPT = {"MobileAppCategorizationAttempts", 5359208L};
@Inject @Named("default_preferences") SharedPreferences defaultPrefs;
@Inject @Named("application_preferences") SharedPreferences applicationPrefs;
@Inject @Named("prefs") SharedPreferences otherPrefs;
public static final String DEFAULT_EDIT_SUMMARY = "Uploaded using Android Commons app";
public static final String FEEDBACK_EMAIL = "commons-app-android@googlegroups.com";
public static final String LOGS_PRIVATE_EMAIL = "commons-app-android-private@googlegroups.com";
public static final String FEEDBACK_EMAIL_SUBJECT = "Commons Android App (%s) Feedback";
private static CommonsApplication instance = null;
private MediaWikiApi api = null;
private LruCache<String, String> thumbnailUrlCache = new LruCache<>(1024);
private CacheController cacheData = null;
private DBOpenHelper dbOpenHelper = null;
private NearbyPlaces nearbyPlaces = null;
private RefWatcher refWatcher;
/**
* This should not be called by ANY application code (other than the magic Android glue)
* Use CommonsApplication.getInstance() instead to get the singleton.
* Used to declare and initialize various components and dependencies
*/
public CommonsApplication() {
CommonsApplication.instance = this;
}
public static CommonsApplication getInstance() {
if (instance == null) {
instance = new CommonsApplication();
}
return instance;
}
public MediaWikiApi getMWApi() {
if (api == null) {
api = new ApacheHttpClientMediaWikiApi(BuildConfig.WIKIMEDIA_API_HOST);
}
return api;
}
public CacheController getCacheData() {
if (cacheData == null) {
cacheData = new CacheController();
}
return cacheData;
}
public LruCache<String, String> getThumbnailUrlCache() {
return thumbnailUrlCache;
}
public synchronized DBOpenHelper getDBOpenHelper() {
if (dbOpenHelper == null) {
dbOpenHelper = new DBOpenHelper(this);
}
return dbOpenHelper;
}
public synchronized NearbyPlaces getNearbyPlaces() {
if (nearbyPlaces == null) {
nearbyPlaces = new NearbyPlaces();
}
return nearbyPlaces;
}
@Override
public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
ApplicationlessInjection
.getInstance(this)
.getCommonsApplicationComponent()
.inject(this);
Fresco.initialize(this);
if (setupLeakCanary() == RefWatcher.DISABLED) {
return;
}
LeakCanary.install(this);
Timber.plant(new Timber.DebugTree());
if (!BuildConfig.DEBUG) {
ACRA.init(this);
} else {
@ -136,52 +87,36 @@ public class CommonsApplication extends Application {
// Fire progress callbacks for every 3% of uploaded content
System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0");
}
Fresco.initialize(this);
//For caching area -> categories
cacheData = new CacheController();
/**
* Helps in setting up LeakCanary library
* @return instance of LeakCanary
*/
protected RefWatcher setupLeakCanary() {
if (LeakCanary.isInAnalyzerProcess(this)) {
return RefWatcher.DISABLED;
}
return LeakCanary.install(this);
}
/**
* Provides a way to get member refWatcher
*
* @param context Application context
* @return application member refWatcher
*/
public static RefWatcher getRefWatcher(Context context) {
CommonsApplication application = (CommonsApplication) context.getApplicationContext();
return application.refWatcher;
}
/**
* @return Account|null
* clears data of current application
* @param context Application context
* @param logoutListener Implementation of interface LogoutListener
*/
public Account getCurrentAccount() {
if (currentAccount == null) {
AccountManager accountManager = AccountManager.get(this);
Account[] allAccounts = accountManager.getAccountsByType(AccountUtil.accountType());
if (allAccounts.length != 0) {
currentAccount = allAccounts[0];
}
}
return currentAccount;
}
public Boolean revalidateAuthToken() {
AccountManager accountManager = AccountManager.get(this);
Account curAccount = getCurrentAccount();
if (curAccount == null) {
return false; // This should never happen
}
accountManager.invalidateAuthToken(AccountUtil.accountType(), getMWApi().getAuthCookie());
try {
String authCookie = accountManager.blockingGetAuthToken(curAccount, "", false);
getMWApi().setAuthCookie(authCookie);
return true;
} catch (OperationCanceledException | NullPointerException | IOException | AuthenticatorException e) {
e.printStackTrace();
return false;
}
}
public boolean deviceHasCamera() {
PackageManager pm = getPackageManager();
return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA) ||
pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT);
}
public void clearApplicationData(Context context, LogoutListener logoutListener) {
File cacheDirectory = context.getCacheDir();
File applicationDirectory = new File(cacheDirectory.getParent());
@ -194,70 +129,37 @@ public class CommonsApplication extends Application {
}
}
AccountManager accountManager = AccountManager.get(this);
Account[] allAccounts = accountManager.getAccountsByType(AccountUtil.accountType());
AccountManagerCallback<Boolean> amCallback = new AccountManagerCallback<Boolean>() {
private int index = 0;
void setIndex(int index) {
this.index = index;
}
int getIndex() {
return index;
}
@Override
public void run(AccountManagerFuture<Boolean> accountManagerFuture) {
setIndex(getIndex() + 1);
try {
if (accountManagerFuture != null && accountManagerFuture.getResult()) {
Timber.d("Account removed successfully.");
}
} catch (OperationCanceledException | IOException | AuthenticatorException e) {
e.printStackTrace();
}
if (getIndex() == allAccounts.length) {
sessionManager.clearAllAccounts()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> {
Timber.d("All accounts have been removed");
//TODO: fix preference manager
PreferenceManager.getDefaultSharedPreferences(getInstance())
.edit().clear().commit();
SharedPreferences preferences = context
.getSharedPreferences("fr.free.nrw.commons", MODE_PRIVATE);
preferences.edit().clear().commit();
context.getSharedPreferences("prefs", Context.MODE_PRIVATE)
.edit().clear().commit();
preferences.edit().putBoolean("firstrun", false).apply();
defaultPrefs.edit().clear().apply();
applicationPrefs.edit().clear().apply();
applicationPrefs.edit().putBoolean("firstrun", false).apply();
otherPrefs.edit().clear().apply();
updateAllDatabases();
currentAccount = null;
logoutListener.onLogoutComplete();
}
}
};
for (Account account : allAccounts) {
accountManager.removeAccount(account, amCallback, null);
}
});
}
/**
* Deletes all tables and re-creates them.
*/
public void updateAllDatabases() {
DBOpenHelper dbOpenHelper = CommonsApplication.getInstance().getDBOpenHelper();
private void updateAllDatabases() {
dbOpenHelper.getReadableDatabase().close();
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
ModifierSequence.Table.onDelete(db);
Category.Table.onDelete(db);
Contribution.Table.onDelete(db);
ModifierSequenceDao.Table.onDelete(db);
CategoryDao.Table.onDelete(db);
ContributionDao.Table.onDelete(db);
}
/**
* Interface used to get log-out events
*/
public interface LogoutListener {
void onLogoutComplete();
}

View file

@ -1,6 +1,5 @@
package fr.free.nrw.commons;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.Handler;
@ -9,7 +8,9 @@ import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
public abstract class HandlerService<T> extends Service {
import fr.free.nrw.commons.di.CommonsDaggerService;
public abstract class HandlerService<T> extends CommonsDaggerService {
private volatile Looper threadLooper;
private volatile ServiceHandler threadHandler;
private String serviceName;

View file

@ -2,12 +2,25 @@ package fr.free.nrw.commons;
import android.support.annotation.Nullable;
/**
* represents Licence object
*/
public class License {
private String key;
private String template;
private String url;
private String name;
/**
* Constructs a new instance of License.
*
* @param key license key
* @param template license template
* @param url license URL
* @param name licence name
*
* @throws RuntimeException if License.key or Licence.template is null
*/
public License(String key, String template, String url, String name) {
if (key == null) {
throw new RuntimeException("License.key must not be null");
@ -21,10 +34,18 @@ public class License {
this.name = name;
}
/**
* Gets the license key.
* @return license key as a String.
*/
public String getKey() {
return key;
}
/**
* Gets the license template.
* @return license template as a String.
*/
public String getTemplate() {
return template;
}
@ -38,6 +59,12 @@ public class License {
}
}
/**
* Gets the license URL
*
* @param language license language
* @return URL
*/
public @Nullable String getUrl(String language) {
if (url == null) {
return null;

View file

@ -5,21 +5,31 @@ import android.content.res.Resources;
import android.support.annotation.Nullable;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
/**
* Represents a list of Licenses
*/
public class LicenseList {
private Map<String, License> licenses = new HashMap<>();
private Resources res;
/**
* Constructs new instance of LicenceList
*
* @param activity License activity
*/
public LicenseList(Activity activity) {
res = activity.getResources();
XmlPullParser parser = res.getXml(R.xml.wikimedia_licenses);
String namespace = "https://www.mediawiki.org/wiki/Extension:UploadWizard/xmlns/licenses";
while (Utils.xmlFastForward(parser, namespace, "license")) {
while (xmlFastForward(parser, namespace, "license")) {
String id = parser.getAttributeValue(null, "id");
String template = parser.getAttributeValue(null, "template");
String url = parser.getAttributeValue(null, "url");
@ -29,14 +39,28 @@ public class LicenseList {
}
}
/**
* Gets a collection of licenses
* @return License values
*/
public Collection<License> values() {
return licenses.values();
}
/**
* Gets license
* @param key License key
* @return License that matches key
*/
public License get(String key) {
return licenses.get(key);
}
/**
* Creates a license from template
* @param template License template
* @return null
*/
@Nullable
License licenseForTemplate(String template) {
String ucTemplate = new PageTitle(template).getDisplayText();
@ -48,6 +72,11 @@ public class LicenseList {
return null;
}
/**
* Gets template name id
* @param template License template
* @return name id of template
*/
private String nameIdForTemplate(String template) {
// hack :D (converts dashes and periods to underscores)
// cc-by-sa-3.0 -> cc_by_sa_3_0
@ -55,9 +84,44 @@ public class LicenseList {
"_").replace(".", "_");
}
/**
* Gets name of given template
* @param template License template
* @return name of template
*/
private String nameForTemplate(String template) {
int nameId = res.getIdentifier("fr.free.nrw.commons:string/"
+ nameIdForTemplate(template), null, null);
return (nameId != 0) ? res.getString(nameId) : template;
}
/**
* Fast-forward an XmlPullParser to the next instance of the given element
* in the input stream (namespaced).
*
* @param parser
* @param namespace
* @param element
* @return true on match, false on failure
*/
private boolean xmlFastForward(XmlPullParser parser, String namespace, String element) {
try {
while (parser.next() != XmlPullParser.END_DOCUMENT) {
if (parser.getEventType() == XmlPullParser.START_TAG &&
parser.getNamespace().equals(namespace) &&
parser.getName().equals(element)) {
// We found it!
return true;
}
}
return false;
} catch (XmlPullParserException e) {
e.printStackTrace();
return false;
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
}

View file

@ -47,16 +47,35 @@ public class Media implements Parcelable {
private HashMap<String, Object> tags = new HashMap<>();
private @Nullable LatLng coordinates;
/**
* Provides local constructor
*/
protected Media() {
this.categories = new ArrayList<>();
this.descriptions = new HashMap<>();
}
/**
* Provides a minimal constructor
*
* @param filename Media filename
*/
public Media(String filename) {
this();
this.filename = filename;
}
/**
* Provide Media constructor
* @param localUri Media URI
* @param imageUrl Media image URL
* @param filename Media filename
* @param description Media description
* @param dataLength Media date length
* @param dateCreated Media creation date
* @param dateUploaded Media date uploaded
* @param creator Media creator
*/
public Media(Uri localUri, String imageUrl, String filename, String description,
long dataLength, Date dateCreated, @Nullable Date dateUploaded, String creator) {
this();
@ -90,19 +109,33 @@ public class Media implements Parcelable {
descriptions = in.readHashMap(ClassLoader.getSystemClassLoader());
}
/**
* Gets tag of media
* @param key Media key
* @return Media tag
*/
public Object getTag(String key) {
return tags.get(key);
}
/**
* Modifies( or creates a) tag of media
* @param key Media key
* @param value Media value
*/
public void setTag(String key, Object value) {
tags.put(key, value);
}
/**
* Gets media display title
* @return Media title
*/
public String getDisplayTitle() {
if (filename == null) {
return "";
}
// FIXME: Gross hack bercause my regex skills suck maybe or I am too lazy who knows
// FIXME: Gross hack because my regex skills suck maybe or I am too lazy who knows
String title = getFilePageTitle().getDisplayText().replaceFirst("^File:", "");
Matcher matcher = displayTitlePattern.matcher(title);
if (matcher.matches()) {
@ -112,109 +145,215 @@ public class Media implements Parcelable {
}
}
/**
* Gets file page title
* @return New media page title
*/
public PageTitle getFilePageTitle() {
return new PageTitle("File:" + getFilename().replaceFirst("^File:", ""));
}
/**
* Gets local URI
* @return Media local URI
*/
public Uri getLocalUri() {
return localUri;
}
/**
* Gets image URL
* can be null.
* @return Image URL
*/
@Nullable
public String getImageUrl() {
if (imageUrl == null) {
if (imageUrl == null && this.getFilename() != null) {
imageUrl = Utils.makeThumbBaseUrl(this.getFilename());
}
return imageUrl;
}
/**
* Gets the name of the file.
* @return file name as a string
*/
public String getFilename() {
return filename;
}
/**
* Sets the name of the file.
* @param filename the new name of the file
*/
public void setFilename(String filename) {
this.filename = filename;
}
/**
* Gets the file description.
* @return file description as a string
*/
public String getDescription() {
return description;
}
/**
* Sets the file description.
* @param description the new description of the file
*/
public void setDescription(String description) {
this.description = description;
}
/**
* Gets the datalength of the file.
* @return file datalength as a long
*/
public long getDataLength() {
return dataLength;
}
/**
* Sets the datalength of the file.
* @param dataLength as a long
*/
public void setDataLength(long dataLength) {
this.dataLength = dataLength;
}
/**
* Gets the creation date of the file.
* @return creation date as a Date
*/
public Date getDateCreated() {
return dateCreated;
}
/**
* Sets the creation date of the file.
* @param date creation date as a Date
*/
public void setDateCreated(Date date) {
this.dateCreated = date;
}
/**
* Gets the upload date of the file.
* Can be null.
* @return upload date as a Date
*/
public @Nullable
Date getDateUploaded() {
return dateUploaded;
}
/**
* Gets the name of the creator of the file.
* @return creator name as a String
*/
public String getCreator() {
return creator;
}
/**
* Sets the creator name of the file.
* @param creator creator name as a string
*/
public void setCreator(String creator) {
this.creator = creator;
}
/**
* Gets the width of the media.
* @return file width as an int
*/
public int getWidth() {
return width;
}
/**
* Sets the width of the media.
* @param width file width as an int
*/
public void setWidth(int width) {
this.width = width;
}
/**
* Gets the height of the media.
* @return file height as an int
*/
public int getHeight() {
return height;
}
/**
* Sets the height of the media.
* @param height file height as an int
*/
public void setHeight(int height) {
this.height = height;
}
/**
* Gets the license name of the file.
* @return license as a String
*/
public String getLicense() {
return license;
}
/**
* Sets the license name of the file.
* @param license license name as a String
*/
public void setLicense(String license) {
this.license = license;
}
/**
* Gets the coordinates of where the file was created.
* @return file coordinates as a LatLng
*/
public @Nullable
LatLng getCoordinates() {
return coordinates;
}
/**
* Sets the coordinates of where the file was created.
* @param coordinates file coordinates as a LatLng
*/
public void setCoordinates(@Nullable LatLng coordinates) {
this.coordinates = coordinates;
}
/**
* Gets the categories the file falls under.
* @return file categories as an ArrayList of Strings
*/
@SuppressWarnings("unchecked")
public ArrayList<String> getCategories() {
return (ArrayList<String>) categories.clone(); // feels dirty
}
/**
* Sets the categories the file falls under.
* </p>
* Does not append: i.e. will clear the current categories
* and then add the specified ones.
* @param categories file categories as a list of Strings
*/
public void setCategories(List<String> categories) {
this.categories.clear();
this.categories.addAll(categories);
}
/**
* Modifies (or sets) media descriptions
* @param descriptions Media descriptions
*/
void setDescriptions(Map<String, String> descriptions) {
for (String key : this.descriptions.keySet()) {
this.descriptions.remove(key);
@ -224,6 +363,11 @@ public class Media implements Parcelable {
}
}
/**
* Gets media description in preferred language
* @param preferredLanguage Language preferred
* @return Description in preferred language
*/
public String getDescription(String preferredLanguage) {
if (descriptions.containsKey(preferredLanguage)) {
// See if the requested language is there.
@ -240,11 +384,21 @@ public class Media implements Parcelable {
}
}
/**
* Method of Parcelable interface
* @return zero
*/
@Override
public int describeContents() {
return 0;
}
/**
* Creates a way to transfer information between two or more
* activities.
* @param parcel Instance of Parcel
* @param flags Parcel flag
*/
@Override
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeParcelable(localUri, flags);

View file

@ -11,12 +11,12 @@ import org.xml.sax.SAXException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
@ -33,46 +33,39 @@ import timber.log.Timber;
* which are not intrinsic to the media and may change due to editing.
*/
public class MediaDataExtractor {
private final MediaWikiApi mediaWikiApi;
private boolean fetched;
private String filename;
private ArrayList<String> categories;
private Map<String, String> descriptions;
private Date date;
private String license;
private @Nullable LatLng coordinates;
private LicenseList licenseList;
/**
* @param filename of the target media object, should include 'File:' prefix
*/
public MediaDataExtractor(String filename, LicenseList licenseList) {
this.filename = filename;
categories = new ArrayList<>();
descriptions = new HashMap<>();
fetched = false;
this.licenseList = licenseList;
@Inject
public MediaDataExtractor(MediaWikiApi mwApi) {
this.categories = new ArrayList<>();
this.descriptions = new HashMap<>();
this.fetched = false;
this.mediaWikiApi = mwApi;
}
/**
/*
* Actually fetch the data over the network.
* todo: use local caching?
*
* Warning: synchronous i/o, call on a background thread
*/
public void fetch() throws IOException {
public void fetch(String filename, LicenseList licenseList) throws IOException {
if (fetched) {
throw new IllegalStateException("Tried to call MediaDataExtractor.fetch() again.");
}
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
MediaResult result = api.fetchMediaByFilename(filename);
MediaResult result = mediaWikiApi.fetchMediaByFilename(filename);
// In-page category links are extracted from source, as XML doesn't cover [[links]]
extractCategories(result.getWikiSource());
// Description template info is extracted from preprocessor XML
processWikiParseTree(result.getParseTreeXmlSource());
processWikiParseTree(result.getParseTreeXmlSource(), licenseList);
fetched = true;
}
@ -91,7 +84,7 @@ public class MediaDataExtractor {
}
}
private void processWikiParseTree(String source) throws IOException {
private void processWikiParseTree(String source, LicenseList licenseList) throws IOException {
Document doc;
try {
DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
@ -153,7 +146,7 @@ public class MediaDataExtractor {
}
private Node findTemplate(Element parentNode, String title_) throws IOException {
String title= new PageTitle(title_).getDisplayText();
String title = new PageTitle(title_).getDisplayText();
NodeList nodes = parentNode.getChildNodes();
for (int i = 0, length = nodes.getLength(); i < length; i++) {
Node node = nodes.item(i);
@ -179,7 +172,7 @@ public class MediaDataExtractor {
}
private static abstract class TemplateChildNodeComparator {
abstract public boolean match(Node node);
public abstract boolean match(Node node);
}
private Node findTemplateParameter(Node templateNode, String name) throws IOException {
@ -290,7 +283,7 @@ public class MediaDataExtractor {
/**
* Take our metadata and inject it into a live Media object.
* Media object might contain stale or cached data, or emptiness.
* @param media
* @param media Media object to inject into
*/
public void fill(Media media) {
if (!fetched) {

View file

@ -7,16 +7,17 @@ import fr.free.nrw.commons.mwapi.MediaWikiApi;
class MediaThumbnailFetchTask extends AsyncTask<String, String, String> {
protected final Media media;
private MediaWikiApi mediaWikiApi;
public MediaThumbnailFetchTask(@NonNull Media media) {
public MediaThumbnailFetchTask(@NonNull Media media, MediaWikiApi mwApi) {
this.media = media;
this.mediaWikiApi = mwApi;
}
@Override
protected String doInBackground(String... params) {
try {
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
return api.findThumbnailByFilename(params[0]);
return mediaWikiApi.findThumbnailByFilename(params[0]);
} catch (Exception e) {
// Do something better!
}

View file

@ -4,6 +4,7 @@ import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.graphics.drawable.VectorDrawableCompat;
import android.support.v4.util.LruCache;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.widget.Toast;
@ -11,9 +12,16 @@ import android.widget.Toast;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
import com.facebook.drawee.view.SimpleDraweeView;
import javax.inject.Inject;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber;
public class MediaWikiImageView extends SimpleDraweeView {
@Inject MediaWikiApi mwApi;
@Inject LruCache<String, String> thumbnailUrlCache;
private ThumbnailFetchTask currentThumbnailTask;
public MediaWikiImageView(Context context) {
@ -31,19 +39,23 @@ public class MediaWikiImageView extends SimpleDraweeView {
init();
}
/**
* Sets the media. Fetches its thumbnail if necessary.
* @param media the new media
*/
public void setMedia(Media media) {
if (currentThumbnailTask != null) {
currentThumbnailTask.cancel(true);
}
if(media == null) {
if (media == null) {
return;
}
if (CommonsApplication.getInstance().getThumbnailUrlCache().get(media.getFilename()) != null) {
setImageUrl(CommonsApplication.getInstance().getThumbnailUrlCache().get(media.getFilename()));
if (thumbnailUrlCache.get(media.getFilename()) != null) {
setImageUrl(thumbnailUrlCache.get(media.getFilename()));
} else {
setImageUrl(null);
currentThumbnailTask = new ThumbnailFetchTask(media);
currentThumbnailTask = new ThumbnailFetchTask(media, mwApi);
currentThumbnailTask.execute(media.getFilename());
}
}
@ -56,7 +68,15 @@ public class MediaWikiImageView extends SimpleDraweeView {
super.onDetachedFromWindow();
}
/**
* Initializes MediaWikiImageView.
*/
private void init() {
ApplicationlessInjection
.getInstance(getContext()
.getApplicationContext())
.getCommonsApplicationComponent()
.inject(this);
setHierarchy(GenericDraweeHierarchyBuilder
.newInstance(getResources())
.setPlaceholderImage(VectorDrawableCompat.create(getResources(),
@ -66,13 +86,17 @@ public class MediaWikiImageView extends SimpleDraweeView {
.build());
}
/**
* Displays the image from the URL.
* @param url the URL of the image
*/
private void setImageUrl(@Nullable String url) {
setImageURI(url);
}
private class ThumbnailFetchTask extends MediaThumbnailFetchTask {
ThumbnailFetchTask(@NonNull Media media) {
super(media);
ThumbnailFetchTask(@NonNull Media media, @NonNull MediaWikiApi mwApi) {
super(media, mwApi);
}
@Override
@ -85,7 +109,7 @@ public class MediaWikiImageView extends SimpleDraweeView {
} else {
// only cache meaningful thumbnails received from network.
try {
CommonsApplication.getInstance().getThumbnailUrlCache().put(media.getFilename(), result);
thumbnailUrlCache.put(media.getFilename(), result);
} catch (NullPointerException npe) {
Timber.e("error when adding pic to cache " + npe);

View file

@ -84,6 +84,12 @@ public class PageTitle {
return titleKey;
}
/**
* Gets the canonicalized title for displaying (such as "File:My example.jpg").
* </p>
* Essentially equivalent to getPrefixedText
* @return canonical title as a String
*/
@Override
public String toString() {
return getPrefixedText();

View file

@ -1,99 +1,26 @@
package fr.free.nrw.commons;
import android.content.Context;
import android.os.Build;
import android.preference.PreferenceManager;
import android.text.Html;
import android.text.Spanned;
import android.support.annotation.NonNull;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
import org.w3c.dom.Node;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import fr.free.nrw.commons.settings.Prefs;
import timber.log.Timber;
public class Utils {
// Get SHA1 of file from input stream
public static String getSHA1(InputStream is) {
MessageDigest digest;
try {
digest = MessageDigest.getInstance("SHA1");
} catch (NoSuchAlgorithmException e) {
Timber.e(e, "Exception while getting Digest");
return "";
}
byte[] buffer = new byte[8192];
int read;
try {
while ((read = is.read(buffer)) > 0) {
digest.update(buffer, 0, read);
}
byte[] md5sum = digest.digest();
BigInteger bigInt = new BigInteger(1, md5sum);
String output = bigInt.toString(16);
// Fill to 40 chars
output = String.format("%40s", output).replace(' ', '0');
Timber.i("File SHA1: %s", output);
return output;
} catch (IOException e) {
Timber.e(e, "IO Exception");
return "";
} finally {
try {
is.close();
} catch (IOException e) {
Timber.e(e, "Exception on closing MD5 input stream");
}
}
}
/**
* Fix Html.fromHtml is deprecated problem
*
* @param source provided Html string
* @return returned Spanned of appropriate method according to version check
*/
public static Spanned fromHtml(String source) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return Html.fromHtml(source, Html.FROM_HTML_MODE_LEGACY);
} else {
//noinspection deprecation
return Html.fromHtml(source);
}
}
/**
* Strips localization symbols from a string.
* Removes the suffix after "@" and quotes.
@ -110,49 +37,23 @@ public class Utils {
}
}
public static Date parseMWDate(String mwDate) {
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH); // Assuming MW always gives me UTC
try {
return isoFormat.parse(mwDate);
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
public static String toMWDate(Date date) {
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH); // Assuming MW always gives me UTC
isoFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
return isoFormat.format(date);
}
public static String makeThumbBaseUrl(String filename) {
/**
* Creates an URL for thumbnail
*
* @param filename Thumbnail file name
* @return URL of thumbnail
*/
public static String makeThumbBaseUrl(@NonNull String filename) {
String name = new PageTitle(filename).getPrefixedText();
String sha = new String(Hex.encodeHex(DigestUtils.md5(name)));
return String.format("%s/%s/%s/%s", BuildConfig.IMAGE_URL_BASE, sha.substring(0, 1), sha.substring(0, 2), urlEncode(name));
}
public static String getStringFromDOM(Node dom) {
Transformer transformer = null;
try {
transformer = TransformerFactory.newInstance().newTransformer();
} catch (TransformerConfigurationException | TransformerFactoryConfigurationError e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
StringWriter outputStream = new StringWriter();
DOMSource domSource = new DOMSource(dom);
StreamResult strResult = new StreamResult(outputStream);
try {
transformer.transform(domSource, strResult);
} catch (TransformerException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return outputStream.toString();
}
/**
* URL Encode an URL in UTF-8 format
* @param url Unformatted URL
* @return Encoded URL
*/
public static String urlEncode(String url) {
try {
return URLEncoder.encode(url, "utf-8");
@ -161,39 +62,21 @@ public class Utils {
}
}
public static long countBytes(InputStream stream) throws IOException {
long count = 0;
BufferedInputStream bis = new BufferedInputStream(stream);
while (bis.read() != -1) {
count++;
}
return count;
}
/**
* Capitalizes the first character of a string.
*
* @param string String to alter
* @return string with capitalized first character
*/
public static String capitalize(String string) {
return string.substring(0, 1).toUpperCase(Locale.getDefault()) + string.substring(1);
}
public static String licenseTemplateFor(String license) {
switch (license) {
case Prefs.Licenses.CC_BY_3:
return "{{self|cc-by-3.0}}";
case Prefs.Licenses.CC_BY_4:
return "{{self|cc-by-4.0}}";
case Prefs.Licenses.CC_BY_SA_3:
return "{{self|cc-by-sa-3.0}}";
case Prefs.Licenses.CC_BY_SA_4:
return "{{self|cc-by-sa-4.0}}";
case Prefs.Licenses.CC0:
return "{{self|cc-zero}}";
case Prefs.Licenses.CC_BY:
return "{{self|cc-by-3.0}}";
case Prefs.Licenses.CC_BY_SA:
return "{{self|cc-by-sa-3.0}}";
}
throw new RuntimeException("Unrecognized license value: " + license);
}
/**
* Generates licence name with given ID
* @param license License ID
* @return Name of license
*/
public static int licenseNameFor(String license) {
switch (license) {
case Prefs.Licenses.CC_BY_3:
@ -214,51 +97,12 @@ public class Utils {
throw new RuntimeException("Unrecognized license value: " + license);
}
public static String licenseUrlFor(String license) {
switch (license) {
case Prefs.Licenses.CC_BY_3:
return "https://creativecommons.org/licenses/by/3.0/";
case Prefs.Licenses.CC_BY_4:
return "https://creativecommons.org/licenses/by/4.0/";
case Prefs.Licenses.CC_BY_SA_3:
return "https://creativecommons.org/licenses/by-sa/3.0/";
case Prefs.Licenses.CC_BY_SA_4:
return "https://creativecommons.org/licenses/by-sa/4.0/";
case Prefs.Licenses.CC0:
return "https://creativecommons.org/publicdomain/zero/1.0/";
}
throw new RuntimeException("Unrecognized license value: " + license);
}
/**
* Fast-forward an XmlPullParser to the next instance of the given element
* in the input stream (namespaced).
*
* @param parser
* @param namespace
* @param element
* @return true on match, false on failure
* Fixing incorrect extension
* @param title File name
* @param extension Correct extension
* @return File with correct extension
*/
public static boolean xmlFastForward(XmlPullParser parser, String namespace, String element) {
try {
while (parser.next() != XmlPullParser.END_DOCUMENT) {
if (parser.getEventType() == XmlPullParser.START_TAG
&& parser.getNamespace().equals(namespace)
&& parser.getName().equals(element)) {
// We found it!
return true;
}
}
return false;
} catch (XmlPullParserException e) {
e.printStackTrace();
return false;
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
public static String fixExtension(String title, String extension) {
Pattern jpegPattern = Pattern.compile("\\.jpeg$", Pattern.CASE_INSENSITIVE);
@ -274,11 +118,45 @@ public class Utils {
return title;
}
public static boolean isNullOrWhiteSpace(String value) {
return value == null || value.trim().isEmpty();
}
/**
* Tells whether dark theme is active or not
* @param context Activity context
* @return The state of dark theme
*/
public static boolean isDarkTheme(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean("theme", false);
}
/**
* Will be used to fetch the logs generated by the app ever since the beginning of times....
* i.e. since the time the app started.
*
* @return String containing all the logs since the time the app started
*/
public static String getAppLogs() {
final String processId = Integer.toString(android.os.Process.myPid());
StringBuilder stringBuilder = new StringBuilder();
try {
String[] command = new String[] {"logcat","-d","-v","threadtime"};
Process process = Runtime.getRuntime().exec(command);
BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(process.getInputStream())
);
String line;
while ((line = bufferedReader.readLine()) != null) {
if (line.contains(processId)) {
stringBuilder.append(line);
}
}
} catch (IOException ioe) {
Timber.e("getAppLogs failed", ioe);
}
return stringBuilder.toString();
}
}

View file

@ -18,6 +18,11 @@ public class WelcomeActivity extends BaseActivity {
private WelcomePagerAdapter adapter = new WelcomePagerAdapter();
/**
* Initialises exiting fields and dependencies
*
* @param savedInstanceState WelcomeActivity bundled data
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -30,12 +35,20 @@ public class WelcomeActivity extends BaseActivity {
adapter.setCallback(this::finish);
}
/**
* References WelcomePageAdapter to null before the activity is destroyed
*/
@Override
public void onDestroy() {
adapter.setCallback(null);
super.onDestroy();
}
/**
* Creates a way to change current activity to WelcomeActivity
*
* @param context Activity context
*/
public static void startYourself(Context context) {
Intent welcomeIntent = new Intent(context, WelcomeActivity.class);
context.startActivity(welcomeIntent);

View file

@ -10,13 +10,6 @@ import butterknife.ButterKnife;
import butterknife.OnClick;
public class WelcomePagerAdapter extends PagerAdapter {
private static final int PAGE_FINAL = 4;
private Callback callback;
public interface Callback {
void onYesClicked();
}
static final int[] PAGE_LAYOUTS = new int[]{
R.layout.welcome_wikipedia,
R.layout.welcome_do_upload,
@ -24,16 +17,34 @@ public class WelcomePagerAdapter extends PagerAdapter {
R.layout.welcome_image_details,
R.layout.welcome_final
};
private static final int PAGE_FINAL = 4;
private Callback callback;
/**
* Changes callback to provided one
*
* @param callback New callback
* it can be null.
*/
public void setCallback(@Nullable Callback callback) {
this.callback = callback;
}
/**
* Gets total number of layouts
* @return Number of layouts
*/
@Override
public int getCount() {
return PAGE_LAYOUTS.length;
}
/**
* Compares given view with provided object
* @param view Adapter view
* @param object Adapter object
* @return Equality between view and object
*/
@Override
public boolean isViewFromObject(View view, Object object) {
return (view == object);
@ -52,16 +63,29 @@ public class WelcomePagerAdapter extends PagerAdapter {
return layout;
}
/**
* Provides a way to remove an item from container
* @param container Adapter view group container
* @param position Index of item
* @param obj Adapter object
*/
@Override
public void destroyItem(ViewGroup container, int position, Object obj) {
container.removeView((View) obj);
}
public interface Callback {
void onYesClicked();
}
class ViewHolder {
ViewHolder(View view) {
ButterKnife.bind(this, view);
}
/**
* Triggers on click callback on button click
*/
@OnClick(R.id.welcomeYesButton)
void onClicked() {
if (callback != null) {

View file

@ -4,21 +4,33 @@ import android.accounts.Account;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.content.ContentResolver;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
import timber.log.Timber;
import static android.accounts.AccountManager.ERROR_CODE_REMOTE_EXCEPTION;
import static android.accounts.AccountManager.KEY_ACCOUNT_NAME;
import static android.accounts.AccountManager.KEY_ACCOUNT_TYPE;
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.CONTRIBUTION_AUTHORITY;
import static fr.free.nrw.commons.modifications.ModificationsContentProvider.MODIFICATIONS_AUTHORITY;
public class AccountUtil {
public static void createAccount(@Nullable AccountAuthenticatorResponse response,
String username, String password) {
public static final String ACCOUNT_TYPE = "fr.free.nrw.commons";
public static final String AUTH_COOKIE = "authCookie";
public static final String AUTH_TOKEN_TYPE = "CommonsAndroid";
private final Context context;
Account account = new Account(username, accountType());
public AccountUtil(Context context) {
this.context = context;
}
public void createAccount(@Nullable AccountAuthenticatorResponse response,
String username, String password) {
Account account = new Account(username, ACCOUNT_TYPE);
boolean created = accountManager().addAccountExplicitly(account, password, null);
Timber.d("account creation " + (created ? "successful" : "failure"));
@ -26,8 +38,8 @@ public class AccountUtil {
if (created) {
if (response != null) {
Bundle bundle = new Bundle();
bundle.putString(AccountManager.KEY_ACCOUNT_NAME, username);
bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, accountType());
bundle.putString(KEY_ACCOUNT_NAME, username);
bundle.putString(KEY_ACCOUNT_TYPE, ACCOUNT_TYPE);
response.onResult(bundle);
@ -35,28 +47,18 @@ public class AccountUtil {
} else {
if (response != null) {
response.onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, "");
response.onError(ERROR_CODE_REMOTE_EXCEPTION, "");
}
Timber.d("account creation failure");
}
// FIXME: If the user turns it off, it shouldn't be auto turned back on
ContentResolver.setSyncAutomatically(account, ContributionsContentProvider.AUTHORITY, true); // Enable sync by default!
ContentResolver.setSyncAutomatically(account, ModificationsContentProvider.AUTHORITY, true); // Enable sync by default!
ContentResolver.setSyncAutomatically(account, CONTRIBUTION_AUTHORITY, true); // Enable sync by default!
ContentResolver.setSyncAutomatically(account, MODIFICATIONS_AUTHORITY, true); // Enable sync by default!
}
@NonNull
public static String accountType() {
return "fr.free.nrw.commons";
}
private static AccountManager accountManager() {
return AccountManager.get(app());
}
@NonNull
private static CommonsApplication app() {
return CommonsApplication.getInstance();
private AccountManager accountManager() {
return AccountManager.get(context);
}
}

View file

@ -1,84 +1,45 @@
package fr.free.nrw.commons.auth;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerFuture;
import android.os.Bundle;
import fr.free.nrw.commons.CommonsApplication;
import javax.inject.Inject;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
import static fr.free.nrw.commons.auth.AccountUtil.AUTH_COOKIE;
public abstract class AuthenticatedActivity extends NavigationBaseActivity {
private String accountType;
CommonsApplication app;
@Inject SessionManager sessionManager;
@Inject
MediaWikiApi mediaWikiApi;
private String authCookie;
public AuthenticatedActivity() {
this.accountType = AccountUtil.accountType();
}
private void getAuthCookie(Account account, AccountManager accountManager) {
Single.fromCallable(() -> accountManager.blockingGetAuthToken(account, "", false))
.subscribeOn(Schedulers.io())
.doOnError(Timber::e)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onAuthCookieAcquired, throwable -> onAuthFailure());
}
private void addAccount(AccountManager accountManager) {
Single.just(accountManager.addAccount(accountType, null, null, null, AuthenticatedActivity.this, null, null))
.subscribeOn(Schedulers.io())
.map(AccountManagerFuture::getResult)
.doOnEvent((bundle, throwable) -> {
if (!bundle.containsKey(AccountManager.KEY_ACCOUNT_NAME)) {
throw new RuntimeException("Bundle doesn't contain account-name key: "
+ AccountManager.KEY_ACCOUNT_NAME);
}
})
.map(bundle -> bundle.getString(AccountManager.KEY_ACCOUNT_NAME))
.doOnError(Timber::e)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(s -> {
Account[] allAccounts = accountManager.getAccountsByType(accountType);
Account curAccount = allAccounts[0];
getAuthCookie(curAccount, accountManager);
},
throwable -> onAuthFailure());
}
protected void requestAuthToken() {
if (authCookie != null) {
onAuthCookieAcquired(authCookie);
return;
}
AccountManager accountManager = AccountManager.get(this);
Account curAccount = app.getCurrentAccount();
if (curAccount == null) {
addAccount(accountManager);
} else {
getAuthCookie(curAccount, accountManager);
authCookie = sessionManager.getAuthCookie();
if (authCookie != null) {
onAuthCookieAcquired(authCookie);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
app = CommonsApplication.getInstance();
if (savedInstanceState != null) {
authCookie = savedInstanceState.getString("authCookie");
authCookie = savedInstanceState.getString(AUTH_COOKIE);
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("authCookie", authCookie);
outState.putString(AUTH_COOKIE, authCookie);
}
protected abstract void onAuthCookieAcquired(String authCookie);

View file

@ -1,115 +1,118 @@
package fr.free.nrw.commons.auth;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorActivity;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.app.ProgressDialog;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.support.annotation.ColorRes;
import android.support.annotation.NonNull;
import android.support.annotation.StringRes;
import android.support.design.widget.TextInputLayout;
import android.support.v4.app.NavUtils;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatDelegate;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import java.io.IOException;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.WelcomeActivity;
import fr.free.nrw.commons.PageTitle;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.WelcomeActivity;
import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
import static android.view.KeyEvent.KEYCODE_ENTER;
import static android.view.View.VISIBLE;
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE;
import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE;
public class LoginActivity extends AccountAuthenticatorActivity {
public static final String PARAM_USERNAME = "fr.free.nrw.commons.login.username";
private SharedPreferences prefs = null;
@Inject MediaWikiApi mwApi;
@Inject AccountUtil accountUtil;
@Inject SessionManager sessionManager;
@Inject @Named("application_preferences") SharedPreferences prefs;
@Inject @Named("default_preferences") SharedPreferences defaultPrefs;
private Button loginButton;
private EditText usernameEdit;
private EditText passwordEdit;
private EditText twoFactorEdit;
@BindView(R.id.loginButton) Button loginButton;
@BindView(R.id.signupButton) Button signupButton;
@BindView(R.id.loginUsername) EditText usernameEdit;
@BindView(R.id.loginPassword) EditText passwordEdit;
@BindView(R.id.loginTwoFactor) EditText twoFactorEdit;
@BindView(R.id.error_message_container) ViewGroup errorMessageContainer;
@BindView(R.id.error_message) TextView errorMessage;
@BindView(R.id.two_factor_container)TextInputLayout twoFactorContainer;
ProgressDialog progressDialog;
private AppCompatDelegate delegate;
private LoginTextWatcher textWatcher = new LoginTextWatcher();
private CommonsApplication app;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTheme(Utils.isDarkTheme(this) ? R.style.DarkAppTheme : R.style.LightAppTheme);
getDelegate().installViewFactory();
getDelegate().onCreate(savedInstanceState);
app = CommonsApplication.getInstance();
super.onCreate(savedInstanceState);
ApplicationlessInjection
.getInstance(this.getApplicationContext())
.getCommonsApplicationComponent()
.inject(this);
setContentView(R.layout.activity_login);
loginButton = (Button) findViewById(R.id.loginButton);
Button signupButton = (Button) findViewById(R.id.signupButton);
usernameEdit = (EditText) findViewById(R.id.loginUsername);
passwordEdit = (EditText) findViewById(R.id.loginPassword);
twoFactorEdit = (EditText) findViewById(R.id.loginTwoFactor);
prefs = getSharedPreferences("fr.free.nrw.commons", MODE_PRIVATE);
ButterKnife.bind(this);
usernameEdit.addTextChangedListener(textWatcher);
passwordEdit.addTextChangedListener(textWatcher);
twoFactorEdit.addTextChangedListener(textWatcher);
passwordEdit.setOnEditorActionListener(newLoginInputActionListener());
loginButton.setOnClickListener(this::performLogin);
signupButton.setOnClickListener(this::signUp);
loginButton.setOnClickListener(view -> performLogin());
signupButton.setOnClickListener(view -> signUp());
}
private class LoginTextWatcher implements TextWatcher {
@Override
public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence charSequence, int start, int count, int after) {
}
@Override
public void afterTextChanged(Editable editable) {
if (usernameEdit.getText().length() != 0 && passwordEdit.getText().length() != 0 &&
(BuildConfig.DEBUG || twoFactorEdit.getText().length() != 0 || twoFactorEdit.getVisibility() != View.VISIBLE)) {
loginButton.setEnabled(true);
} else {
loginButton.setEnabled(false);
}
}
}
private TextView.OnEditorActionListener newLoginInputActionListener() {
return (textView, actionId, keyEvent) -> {
if (loginButton.isEnabled()) {
if (actionId == IME_ACTION_DONE) {
performLogin(textView);
return true;
} else if ((keyEvent != null) && keyEvent.getKeyCode() == KEYCODE_ENTER) {
performLogin(textView);
return true;
}
}
return false;
};
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
getDelegate().onPostCreate(savedInstanceState);
}
@Override
protected void onResume() {
super.onResume();
if (prefs.getBoolean("firstrun", true)) {
WelcomeActivity.startYourself(this);
prefs.edit().putBoolean("firstrun", false).apply();
}
if (app.getCurrentAccount() != null) {
if (sessionManager.getCurrentAccount() != null) {
startMainActivity();
}
}
@ -127,22 +130,113 @@ public class LoginActivity extends AccountAuthenticatorActivity {
usernameEdit.removeTextChangedListener(textWatcher);
passwordEdit.removeTextChangedListener(textWatcher);
twoFactorEdit.removeTextChangedListener(textWatcher);
delegate.onDestroy();
super.onDestroy();
}
private void performLogin(View view) {
private void performLogin() {
Timber.d("Login to start!");
LoginTask task = getLoginTask();
task.execute();
final String username = canonicializeUsername(usernameEdit.getText().toString());
final String password = passwordEdit.getText().toString();
String twoFactorCode = twoFactorEdit.getText().toString();
showLoggingProgressBar();
Observable.fromCallable(() -> login(username, password, twoFactorCode))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> handleLogin(username, password, result));
}
private LoginTask getLoginTask() {
return new LoginTask(
this,
canonicializeUsername(usernameEdit.getText().toString()),
passwordEdit.getText().toString(),
twoFactorEdit.getText().toString()
);
private String login(String username, String password, String twoFactorCode) {
try {
if (twoFactorCode.isEmpty()) {
return mwApi.login(username, password);
} else {
return mwApi.login(username, password, twoFactorCode);
}
} catch (IOException e) {
// Do something better!
return "NetworkFailure";
}
}
private void handleLogin(String username, String password, String result) {
Timber.d("Login done!");
if (result.equals("PASS")) {
handlePassResult(username, password);
} else {
handleOtherResults(result);
}
}
private void showLoggingProgressBar() {
progressDialog = new ProgressDialog(this);
progressDialog.setIndeterminate(true);
progressDialog.setTitle(getString(R.string.logging_in_title));
progressDialog.setMessage(getString(R.string.logging_in_message));
progressDialog.setCanceledOnTouchOutside(false);
progressDialog.show();
}
private void handlePassResult(String username, String password) {
showSuccessAndDismissDialog();
requestAuthToken();
AccountAuthenticatorResponse response = null;
Bundle extras = getIntent().getExtras();
if (extras != null) {
Timber.d("Bundle of extras: %s", extras);
response = extras.getParcelable(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE);
if (response != null) {
Bundle authResult = new Bundle();
authResult.putString(AccountManager.KEY_ACCOUNT_NAME, username);
authResult.putString(AccountManager.KEY_ACCOUNT_TYPE, ACCOUNT_TYPE);
response.onResult(authResult);
}
}
accountUtil.createAccount(response, username, password);
startMainActivity();
}
protected void requestAuthToken() {
AccountManager accountManager = AccountManager.get(this);
Account curAccount = sessionManager.getCurrentAccount();
if (curAccount != null) {
accountManager.setAuthToken(curAccount, AUTH_TOKEN_TYPE, mwApi.getAuthCookie());
}
}
/**
* Match known failure message codes and provide messages.
*
* @param result String
*/
private void handleOtherResults(String result) {
if (result.equals("NetworkFailure")) {
// Matches NetworkFailure which is created by the doInBackground method
showMessageAndCancelDialog(R.string.login_failed_network);
} else if (result.toLowerCase().contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) {
// Matches nosuchuser, nosuchusershort, noname
showMessageAndCancelDialog(R.string.login_failed_username);
emptySensitiveEditFields();
} else if (result.toLowerCase().contains("wrongpassword".toLowerCase())) {
// Matches wrongpassword, wrongpasswordempty
showMessageAndCancelDialog(R.string.login_failed_password);
emptySensitiveEditFields();
} else if (result.toLowerCase().contains("throttle".toLowerCase())) {
// Matches unknown throttle error codes
showMessageAndCancelDialog(R.string.login_failed_throttled);
} else if (result.toLowerCase().contains("userblocked".toLowerCase())) {
// Matches login-userblocked
showMessageAndCancelDialog(R.string.login_failed_blocked);
} else if (result.equals("2FA")) {
askUserForTwoFactorAuth();
} else {
// Occurs with unhandled login failure codes
Timber.d("Login failed with reason: %s", result);
showMessageAndCancelDialog(R.string.login_failed_generic);
}
}
/**
@ -154,6 +248,29 @@ public class LoginActivity extends AccountAuthenticatorActivity {
return new PageTitle(username).getText();
}
@Override
protected void onStart() {
super.onStart();
delegate.onStart();
}
@Override
protected void onStop() {
super.onStop();
delegate.onStop();
}
@Override
protected void onPostResume() {
super.onPostResume();
getDelegate().onPostResume();
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
getDelegate().setContentView(view, params);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
@ -164,36 +281,26 @@ public class LoginActivity extends AccountAuthenticatorActivity {
return super.onOptionsItemSelected(item);
}
/**
* Called when Sign Up button is clicked.
* @param view View
*/
public void signUp(View view) {
Intent intent = new Intent(this, SignupActivity.class);
startActivity(intent);
@Override
@NonNull
public MenuInflater getMenuInflater() {
return getDelegate().getMenuInflater();
}
public void askUserForTwoFactorAuth() {
if (BuildConfig.DEBUG) {
twoFactorEdit.setVisibility(View.VISIBLE);
showUserToastAndCancelDialog(R.string.login_failed_2fa_needed);
} else {
showUserToastAndCancelDialog(R.string.login_failed_2fa_not_supported);
}
progressDialog.dismiss();
twoFactorContainer.setVisibility(VISIBLE);
twoFactorEdit.setVisibility(VISIBLE);
showMessageAndCancelDialog(R.string.login_failed_2fa_needed);
}
public void showUserToastAndCancelDialog(int resId) {
showUserToast(resId);
public void showMessageAndCancelDialog(@StringRes int resId) {
showMessage(resId, R.color.secondaryDarkColor);
progressDialog.cancel();
}
private void showUserToast(int resId) {
Toast.makeText(this, resId, Toast.LENGTH_LONG).show();
}
public void showSuccessToastAndDismissDialog() {
Toast successToast = Toast.makeText(this, R.string.login_success, Toast.LENGTH_SHORT);
successToast.show();
public void showSuccessAndDismissDialog() {
showMessage(R.string.login_success, R.color.primaryDarkColor);
progressDialog.dismiss();
}
@ -203,8 +310,57 @@ public class LoginActivity extends AccountAuthenticatorActivity {
}
public void startMainActivity() {
ContributionsActivity.startYourself(this);
NavigationBaseActivity.startActivityWithFlags(this, ContributionsActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP);
finish();
}
private void signUp() {
Intent intent = new Intent(this, SignupActivity.class);
startActivity(intent);
}
private TextView.OnEditorActionListener newLoginInputActionListener() {
return (textView, actionId, keyEvent) -> {
if (loginButton.isEnabled()) {
if (actionId == IME_ACTION_DONE) {
performLogin();
return true;
} else if ((keyEvent != null) && keyEvent.getKeyCode() == KEYCODE_ENTER) {
performLogin();
return true;
}
}
return false;
};
}
private void showMessage(@StringRes int resId, @ColorRes int colorResId) {
errorMessage.setText(getString(resId));
errorMessage.setTextColor(ContextCompat.getColor(this, colorResId));
errorMessageContainer.setVisibility(VISIBLE);
}
private AppCompatDelegate getDelegate() {
if (delegate == null) {
delegate = AppCompatDelegate.create(this, null);
}
return delegate;
}
private class LoginTextWatcher implements TextWatcher {
@Override
public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence charSequence, int start, int count, int after) {
}
@Override
public void afterTextChanged(Editable editable) {
boolean enabled = usernameEdit.getText().length() != 0 && passwordEdit.getText().length() != 0
&& (BuildConfig.DEBUG || twoFactorEdit.getText().length() != 0 || twoFactorEdit.getVisibility() != VISIBLE);
loginButton.setEnabled(enabled);
}
}
}

View file

@ -1,125 +0,0 @@
package fr.free.nrw.commons.auth;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.app.ProgressDialog;
import android.os.AsyncTask;
import android.os.Bundle;
import java.io.IOException;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.mwapi.EventLog;
import timber.log.Timber;
class LoginTask extends AsyncTask<String, String, String> {
private LoginActivity loginActivity;
private String username;
private String password;
private String twoFactorCode = "";
private CommonsApplication app;
public LoginTask(LoginActivity loginActivity, String username, String password, String twoFactorCode) {
this.loginActivity = loginActivity;
this.username = username;
this.password = password;
this.twoFactorCode = twoFactorCode;
app = CommonsApplication.getInstance();
}
@Override
protected void onPreExecute() {
super.onPreExecute();
loginActivity.progressDialog = new ProgressDialog(loginActivity);
loginActivity.progressDialog.setIndeterminate(true);
loginActivity.progressDialog.setTitle(loginActivity.getString(R.string.logging_in_title));
loginActivity.progressDialog.setMessage(loginActivity.getString(R.string.logging_in_message));
loginActivity.progressDialog.setCanceledOnTouchOutside(false);
loginActivity.progressDialog.show();
}
@Override
protected String doInBackground(String... params) {
try {
if (twoFactorCode.isEmpty()) {
return app.getMWApi().login(username, password);
} else {
return app.getMWApi().login(username, password, twoFactorCode);
}
} catch (IOException e) {
// Do something better!
return "NetworkFailure";
}
}
@Override
protected void onPostExecute(String result) {
super.onPostExecute(result);
Timber.d("Login done!");
EventLog.schema(CommonsApplication.EVENT_LOGIN_ATTEMPT)
.param("username", username)
.param("result", result)
.log();
if (result.equals("PASS")) {
handlePassResult();
} else {
handleOtherResults(result);
}
}
private void handlePassResult() {
loginActivity.showSuccessToastAndDismissDialog();
AccountAuthenticatorResponse response = null;
Bundle extras = loginActivity.getIntent().getExtras();
if (extras != null) {
Timber.d("Bundle of extras: %s", extras);
response = extras.getParcelable(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE);
if (response != null) {
Bundle authResult = new Bundle();
authResult.putString(AccountManager.KEY_ACCOUNT_NAME, username);
authResult.putString(AccountManager.KEY_ACCOUNT_TYPE, AccountUtil.accountType());
response.onResult(authResult);
}
}
AccountUtil.createAccount(response, username, password);
loginActivity.startMainActivity();
}
/**
* Match known failure message codes and provide messages.
* @param result String
*/
private void handleOtherResults(String result) {
if (result.equals("NetworkFailure")) {
// Matches NetworkFailure which is created by the doInBackground method
loginActivity.showUserToastAndCancelDialog(R.string.login_failed_network);
} else if (result.toLowerCase().contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) {
// Matches nosuchuser, nosuchusershort, noname
loginActivity.showUserToastAndCancelDialog(R.string.login_failed_username);
loginActivity.emptySensitiveEditFields();
} else if (result.toLowerCase().contains("wrongpassword".toLowerCase())) {
// Matches wrongpassword, wrongpasswordempty
loginActivity.showUserToastAndCancelDialog(R.string.login_failed_password);
loginActivity.emptySensitiveEditFields();
} else if (result.toLowerCase().contains("throttle".toLowerCase())) {
// Matches unknown throttle error codes
loginActivity.showUserToastAndCancelDialog(R.string.login_failed_throttled);
} else if (result.toLowerCase().contains("userblocked".toLowerCase())) {
// Matches login-userblocked
loginActivity.showUserToastAndCancelDialog(R.string.login_failed_blocked);
} else if (result.equals("2FA")) {
loginActivity.askUserForTwoFactorAuth();
} else {
// Occurs with unhandled login failure codes
Timber.d("Login failed with reason: %s", result);
loginActivity.showUserToastAndCancelDialog(R.string.login_failed_generic);
}
}
}

View file

@ -0,0 +1,85 @@
package fr.free.nrw.commons.auth;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.Context;
import android.content.SharedPreferences;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import io.reactivex.Completable;
import io.reactivex.Observable;
import timber.log.Timber;
import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE;
/**
* Manage the current logged in user session.
*/
public class SessionManager {
private final Context context;
private final MediaWikiApi mediaWikiApi;
private Account currentAccount; // Unlike a savings account... ;-)
private SharedPreferences sharedPreferences;
public SessionManager(Context context, MediaWikiApi mediaWikiApi, SharedPreferences sharedPreferences) {
this.context = context;
this.mediaWikiApi = mediaWikiApi;
this.currentAccount = null;
this.sharedPreferences = sharedPreferences;
}
/**
* @return Account|null
*/
public Account getCurrentAccount() {
if (currentAccount == null) {
AccountManager accountManager = AccountManager.get(context);
Account[] allAccounts = accountManager.getAccountsByType(ACCOUNT_TYPE);
if (allAccounts.length != 0) {
currentAccount = allAccounts[0];
}
}
return currentAccount;
}
public Boolean revalidateAuthToken() {
AccountManager accountManager = AccountManager.get(context);
Account curAccount = getCurrentAccount();
if (curAccount == null) {
return false; // This should never happen
}
accountManager.invalidateAuthToken(ACCOUNT_TYPE, mediaWikiApi.getAuthCookie());
String authCookie = getAuthCookie();
if (authCookie == null) {
return false;
}
mediaWikiApi.setAuthCookie(authCookie);
return true;
}
public String getAuthCookie() {
boolean isLoggedIn = sharedPreferences.getBoolean("isUserLoggedIn", false);
if (!isLoggedIn) {
Timber.e("User is not logged in");
return null;
} else {
String authCookie = sharedPreferences.getString("getAuthCookie", null);
if (authCookie == null) {
Timber.e("Auth cookie is null even after login");
}
return authCookie;
}
}
public Completable clearAllAccounts() {
AccountManager accountManager = AccountManager.get(context);
Account[] allAccounts = accountManager.getAccountsByType(ACCOUNT_TYPE);
return Completable.fromObservable(Observable.fromArray(allAccounts)
.map(a -> accountManager.removeAccount(a, null, null).getResult()))
.doOnComplete(() -> currentAccount = null);
}
}

View file

@ -7,7 +7,6 @@ import android.webkit.WebViewClient;
import android.widget.Toast;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.theme.BaseActivity;
import timber.log.Timber;
@ -39,11 +38,8 @@ public class SignupActivity extends BaseActivity {
//Signup success, so clear cookies, notify user, and load LoginActivity again
Timber.d("Overriding URL %s", url);
Toast toast = Toast.makeText(
CommonsApplication.getInstance(),
"Account created!",
Toast.LENGTH_LONG
);
Toast toast = Toast.makeText(SignupActivity.this,
"Account created!", Toast.LENGTH_LONG);
toast.show();
// terminate on task completion.
finish();

View file

@ -5,51 +5,37 @@ import android.accounts.Account;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.accounts.NetworkErrorException;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.IOException;
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import static android.accounts.AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION;
import static android.accounts.AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE;
import static android.accounts.AccountManager.KEY_ACCOUNT_NAME;
import static android.accounts.AccountManager.KEY_ACCOUNT_TYPE;
import static android.accounts.AccountManager.KEY_AUTHTOKEN;
import static android.accounts.AccountManager.KEY_BOOLEAN_RESULT;
import static android.accounts.AccountManager.KEY_ERROR_CODE;
import static android.accounts.AccountManager.KEY_ERROR_MESSAGE;
import static android.accounts.AccountManager.KEY_INTENT;
import static fr.free.nrw.commons.auth.LoginActivity.PARAM_USERNAME;
import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE;
import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE;
public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
private static final String[] SYNC_AUTHORITIES = {ContributionsContentProvider.CONTRIBUTION_AUTHORITY, ModificationsContentProvider.MODIFICATIONS_AUTHORITY};
private Context context;
@NonNull
private final Context context;
WikiAccountAuthenticator(Context context) {
public WikiAccountAuthenticator(@NonNull Context context) {
super(context);
this.context = context;
}
private Bundle unsupportedOperation() {
@Override
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
Bundle bundle = new Bundle();
bundle.putInt(KEY_ERROR_CODE, ERROR_CODE_UNSUPPORTED_OPERATION);
// HACK: the docs indicate that this is a required key bit it's not displayed to the user.
bundle.putString(KEY_ERROR_MESSAGE, "");
bundle.putString("test", "editProperties");
return bundle;
}
private boolean supportedAccountType(@Nullable String type) {
return AccountUtil.accountType().equals(type);
}
@Override
public Bundle addAccount(@NonNull AccountAuthenticatorResponse response,
@NonNull String accountType, @Nullable String authTokenType,
@ -57,87 +43,48 @@ public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
throws NetworkErrorException {
if (!supportedAccountType(accountType)) {
return unsupportedOperation();
Bundle bundle = new Bundle();
bundle.putString("test", "addAccount");
return bundle;
}
return addAccount(response);
}
private Bundle addAccount(AccountAuthenticatorResponse response) {
Intent Intent = new Intent(context, LoginActivity.class);
Intent.putExtra(KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
Bundle bundle = new Bundle();
bundle.putParcelable(KEY_INTENT, Intent);
return bundle;
}
@Override
public Bundle confirmCredentials(@NonNull AccountAuthenticatorResponse response,
@NonNull Account account, @Nullable Bundle options)
throws NetworkErrorException {
return unsupportedOperation();
Bundle bundle = new Bundle();
bundle.putString("test", "confirmCredentials");
return bundle;
}
@Override
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
return unsupportedOperation();
}
private String getAuthCookie(String username, String password) throws IOException {
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
//TODO add 2fa support here
String result = api.login(username, password);
if (result.equals("PASS")) {
return api.getAuthCookie();
} else {
return null;
}
}
@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account,
String authTokenType, Bundle options) throws NetworkErrorException {
// Extract the username and password from the Account Manager, and ask
// the server for an appropriate AuthToken.
final AccountManager am = AccountManager.get(context);
final String password = am.getPassword(account);
if (password != null) {
String authCookie;
try {
authCookie = getAuthCookie(account.name, password);
} catch (IOException e) {
// Network error!
e.printStackTrace();
throw new NetworkErrorException(e);
}
if (authCookie != null) {
final Bundle result = new Bundle();
result.putString(KEY_ACCOUNT_NAME, account.name);
result.putString(KEY_ACCOUNT_TYPE, AccountUtil.accountType());
result.putString(KEY_AUTHTOKEN, authCookie);
return result;
}
}
// If we get here, then we couldn't access the user's password - so we
// need to re-prompt them for their credentials. We do that by creating
// an intent to display our AuthenticatorActivity panel.
final Intent intent = new Intent(context, LoginActivity.class);
intent.putExtra(PARAM_USERNAME, account.name);
intent.putExtra(KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
final Bundle bundle = new Bundle();
bundle.putParcelable(KEY_INTENT, intent);
public Bundle getAuthToken(@NonNull AccountAuthenticatorResponse response,
@NonNull Account account, @NonNull String authTokenType,
@Nullable Bundle options)
throws NetworkErrorException {
Bundle bundle = new Bundle();
bundle.putString("test", "getAuthToken");
return bundle;
}
@Nullable
@Override
public String getAuthTokenLabel(@NonNull String authTokenType) {
//Note: the wikipedia app actually returns a string here....
//return supportedAccountType(authTokenType) ? context.getString(R.string.wikimedia) : null;
return null;
return supportedAccountType(authTokenType) ? AUTH_TOKEN_TYPE : null;
}
@Nullable
@Override
public Bundle updateCredentials(@NonNull AccountAuthenticatorResponse response,
@NonNull Account account, @Nullable String authTokenType,
@Nullable Bundle options)
throws NetworkErrorException {
Bundle bundle = new Bundle();
bundle.putString("test", "updateCredentials");
return bundle;
}
@Nullable
@ -146,16 +93,50 @@ public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
@NonNull Account account, @NonNull String[] features)
throws NetworkErrorException {
Bundle bundle = new Bundle();
bundle.putBoolean(KEY_BOOLEAN_RESULT, false);
bundle.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
return bundle;
}
@Nullable
@Override
public Bundle updateCredentials(@NonNull AccountAuthenticatorResponse response,
@NonNull Account account, @Nullable String authTokenType,
@Nullable Bundle options) throws NetworkErrorException {
return unsupportedOperation();
private boolean supportedAccountType(@Nullable String type) {
return ACCOUNT_TYPE.equals(type);
}
private Bundle addAccount(AccountAuthenticatorResponse response) {
Intent intent = new Intent(context, LoginActivity.class);
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
return bundle;
}
private Bundle unsupportedOperation() {
Bundle bundle = new Bundle();
bundle.putInt(AccountManager.KEY_ERROR_CODE, AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION);
// HACK: the docs indicate that this is a required key bit it's not displayed to the user.
bundle.putString(AccountManager.KEY_ERROR_MESSAGE, "");
return bundle;
}
@Override
public Bundle getAccountRemovalAllowed(AccountAuthenticatorResponse response,
Account account) throws NetworkErrorException {
Bundle result = super.getAccountRemovalAllowed(response, account);
if (result.containsKey(AccountManager.KEY_BOOLEAN_RESULT)
&& !result.containsKey(AccountManager.KEY_INTENT)) {
boolean allowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT);
if (allowed) {
for (String auth : SYNC_AUTHORITIES) {
ContentResolver.cancelSync(account, auth);
}
}
}
return result;
}
}

View file

@ -1,24 +1,26 @@
package fr.free.nrw.commons.auth;
import android.accounts.AccountManager;
import android.app.Service;
import android.accounts.AbstractAccountAuthenticator;
import android.content.Intent;
import android.os.IBinder;
import android.support.annotation.Nullable;
public class WikiAccountAuthenticatorService extends Service {
import fr.free.nrw.commons.di.CommonsDaggerService;
private static WikiAccountAuthenticator wikiAccountAuthenticator = null;
public class WikiAccountAuthenticatorService extends CommonsDaggerService {
@Nullable
private AbstractAccountAuthenticator authenticator;
@Override
public IBinder onBind(Intent intent) {
if (!intent.getAction().equals(AccountManager.ACTION_AUTHENTICATOR_INTENT)) {
return null;
}
if (wikiAccountAuthenticator == null) {
wikiAccountAuthenticator = new WikiAccountAuthenticator(this);
}
return wikiAccountAuthenticator.getIBinder();
public void onCreate() {
super.onCreate();
authenticator = new WikiAccountAuthenticator(this);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return authenticator == null ? null : authenticator.getIBinder();
}
}

View file

@ -1,10 +1,7 @@
package fr.free.nrw.commons.category;
import android.content.ContentProviderClient;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.app.Fragment;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
@ -31,11 +28,14 @@ import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.data.Category;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.upload.MwVolleyApi;
import fr.free.nrw.commons.utils.StringSortingUtils;
import io.reactivex.Observable;
@ -45,12 +45,11 @@ import timber.log.Timber;
import static android.view.KeyEvent.ACTION_UP;
import static android.view.KeyEvent.KEYCODE_BACK;
import static fr.free.nrw.commons.category.CategoryContentProvider.AUTHORITY;
/**
* Displays the category suggestion and selection screen. Category search is initiated here.
*/
public class CategorizationFragment extends Fragment {
public class CategorizationFragment extends CommonsDaggerSupportFragment {
public static final int SEARCH_CATS_LIMIT = 25;
@ -65,16 +64,19 @@ public class CategorizationFragment extends Fragment {
@BindView(R.id.categoriesExplanation)
TextView categoriesSkip;
@Inject MediaWikiApi mwApi;
@Inject @Named("default_preferences") SharedPreferences prefs;
@Inject CategoryDao categoryDao;
private RVRendererAdapter<CategoryItem> categoriesAdapter;
private OnCategoriesSaveHandler onCategoriesSaveHandler;
private HashMap<String, ArrayList<String>> categoriesCache;
private List<CategoryItem> selectedCategories = new ArrayList<>();
private ContentProviderClient databaseClient;
private final CategoriesAdapterFactory adapterFactory = new CategoriesAdapterFactory(item -> {
if (item.isSelected()) {
selectedCategories.add(item);
updateCategoryCount(item, databaseClient);
updateCategoryCount(item);
} else {
selectedCategories.remove(item);
}
@ -88,13 +90,6 @@ public class CategorizationFragment extends Fragment {
categoriesList.setLayoutManager(new LinearLayoutManager(getContext()));
RxView.clicks(categoriesSkip)
.takeUntil(RxView.detaches(categoriesSkip))
.subscribe(o -> {
getActivity().onBackPressed();
getActivity().finish();
});
ArrayList<CategoryItem> items = new ArrayList<>();
categoriesCache = new HashMap<>();
if (savedInstanceState != null) {
@ -139,12 +134,6 @@ public class CategorizationFragment extends Fragment {
}
}
@Override
public void onDestroy() {
super.onDestroy();
databaseClient.release();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
@ -180,7 +169,6 @@ public class CategorizationFragment extends Fragment {
setHasOptionsMenu(true);
onCategoriesSaveHandler = (OnCategoriesSaveHandler) getActivity();
getActivity().setTitle(R.string.categories_activity_title);
databaseClient = getActivity().getContentResolver().acquireContentProviderClient(AUTHORITY);
}
private void updateCategoryList(String filter) {
@ -205,7 +193,9 @@ public class CategorizationFragment extends Fragment {
.sorted(sortBySimilarity(filter))
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
s -> categoriesAdapter.add(s), Timber::e, () -> {
s -> categoriesAdapter.add(s),
Timber::e,
() -> {
categoriesAdapter.notifyDataSetChanged();
categoriesSearchInProgress.setVisibility(View.GONE);
@ -253,16 +243,15 @@ public class CategorizationFragment extends Fragment {
private Observable<CategoryItem> titleCategories() {
//Retrieve the title that was saved when user tapped submit icon
SharedPreferences titleDesc = PreferenceManager.getDefaultSharedPreferences(getActivity());
String title = titleDesc.getString("Title", "");
String title = prefs.getString("Title", "");
return CommonsApplication.getInstance().getMWApi()
return mwApi
.searchTitles(title, SEARCH_CATS_LIMIT)
.map(name -> new CategoryItem(name, false));
}
private Observable<CategoryItem> recentCategories() {
return Observable.fromIterable(Category.recentCategories(databaseClient, SEARCH_CATS_LIMIT))
return Observable.fromIterable(categoryDao.recentCategories(SEARCH_CATS_LIMIT))
.map(s -> new CategoryItem(s, false));
}
@ -279,7 +268,7 @@ public class CategorizationFragment extends Fragment {
}
//otherwise, search API for matching categories
return CommonsApplication.getInstance().getMWApi()
return mwApi
.allCategories(term, SEARCH_CATS_LIMIT)
.map(name -> new CategoryItem(name, false));
}
@ -290,7 +279,7 @@ public class CategorizationFragment extends Fragment {
return Observable.empty();
}
return CommonsApplication.getInstance().getMWApi()
return mwApi
.searchCategories(term, SEARCH_CATS_LIMIT)
.map(s -> new CategoryItem(s, false));
}
@ -308,28 +297,22 @@ public class CategorizationFragment extends Fragment {
//Check if item contains a 4-digit word anywhere within the string (.* is wildcard)
//And that item does not equal the current year or previous year
//And if it is an irrelevant category such as Media_needing_categories_as_of_16_June_2017(Issue #750)
//Check if the year in the form of XX(X)0s is relevant, i.e. in the 2000s or 2010s as stated in Issue #1029
return ((item.matches(".*(19|20)\\d{2}.*") && !item.contains(yearInString) && !item.contains(prevYearInString))
|| item.matches("(.*)needing(.*)") || item.matches("(.*)taken on(.*)"));
|| item.matches("(.*)needing(.*)") || item.matches("(.*)taken on(.*)")
|| (item.matches(".*0s.*") && !item.matches(".*(200|201)0s.*")));
}
private void updateCategoryCount(CategoryItem item, ContentProviderClient client) {
Category cat = lookupCategory(item.getName());
cat.incTimesUsed();
cat.save(client);
}
private void updateCategoryCount(CategoryItem item) {
Category category = categoryDao.find(item.getName());
private Category lookupCategory(String name) {
Category cat = Category.find(databaseClient, name);
if (cat == null) {
// Newly used category...
cat = new Category();
cat.setName(name);
cat.setLastUsed(new Date());
cat.setTimesUsed(0);
// Newly used category...
if (category == null) {
category = new Category(null, item.getName(), new Date(), 0);
}
return cat;
category.incTimesUsed();
categoryDao.save(category);
}
public int getCurrentSelectedCount() {

View file

@ -0,0 +1,96 @@
package fr.free.nrw.commons.category;
import android.net.Uri;
import java.util.Date;
/**
* Represents a category
*/
public class Category {
private Uri contentUri;
private String name;
private Date lastUsed;
private int timesUsed;
public Category() {
}
public Category(Uri contentUri, String name, Date lastUsed, int timesUsed) {
this.contentUri = contentUri;
this.name = name;
this.lastUsed = lastUsed;
this.timesUsed = timesUsed;
}
/**
* Gets name
*
* @return name
*/
public String getName() {
return name;
}
/**
* Modifies name
*
* @param name Category name
*/
public void setName(String name) {
this.name = name;
}
/**
* Gets last used date
*
* @return Last used date
*/
public Date getLastUsed() {
// warning: Date objects are mutable.
return (Date)lastUsed.clone();
}
/**
* Generates new last used date
*/
private void touch() {
lastUsed = new Date();
}
/**
* Gets no. of times the category is used
*
* @return no. of times used
*/
public int getTimesUsed() {
return timesUsed;
}
/**
* Increments timesUsed by 1 and sets last used date as now.
*/
public void incTimesUsed() {
timesUsed++;
touch();
}
/**
* Gets the content URI for this category
*
* @return content URI
*/
public Uri getContentUri() {
return contentUri;
}
/**
* Modifies the content URI - marking this category as already saved in the database
*
* @param contentUri the content URI
*/
public void setContentUri(Uri contentUri) {
this.contentUri = contentUri;
}
}

View file

@ -1,6 +1,5 @@
package fr.free.nrw.commons.category;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
@ -10,17 +9,18 @@ import android.net.Uri;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.data.Category;
import javax.inject.Inject;
import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.di.CommonsDaggerContentProvider;
import timber.log.Timber;
import static android.content.UriMatcher.NO_MATCH;
import static fr.free.nrw.commons.data.Category.Table.ALL_FIELDS;
import static fr.free.nrw.commons.data.Category.Table.COLUMN_ID;
import static fr.free.nrw.commons.data.Category.Table.TABLE_NAME;
import static fr.free.nrw.commons.category.CategoryDao.Table.ALL_FIELDS;
import static fr.free.nrw.commons.category.CategoryDao.Table.COLUMN_ID;
import static fr.free.nrw.commons.category.CategoryDao.Table.TABLE_NAME;
public class CategoryContentProvider extends ContentProvider {
public class CategoryContentProvider extends CommonsDaggerContentProvider {
public static final String AUTHORITY = "fr.free.nrw.commons.categories.contentprovider";
// For URI matcher
@ -37,19 +37,11 @@ public class CategoryContentProvider extends ContentProvider {
uriMatcher.addURI(AUTHORITY, BASE_PATH + "/#", CATEGORIES_ID);
}
private DBOpenHelper dbOpenHelper;
public static Uri uriForId(int id) {
return Uri.parse(BASE_URI.toString() + "/" + id);
}
@SuppressWarnings("ConstantConditions")
@Override
public boolean onCreate() {
CommonsApplication app = ((CommonsApplication) getContext().getApplicationContext());
dbOpenHelper = app.getDBOpenHelper();
return false;
}
@Inject DBOpenHelper dbOpenHelper;
@SuppressWarnings("ConstantConditions")
@Override

View file

@ -1,115 +1,64 @@
package fr.free.nrw.commons.data;
package fr.free.nrw.commons.category;
import android.content.ContentProviderClient;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.RemoteException;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import fr.free.nrw.commons.category.CategoryContentProvider;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
public class Category {
private Uri contentUri;
public class CategoryDao {
private String name;
private Date lastUsed;
private int timesUsed;
private final Provider<ContentProviderClient> clientProvider;
// Getters/setters
public String getName() {
return name;
@Inject
public CategoryDao(@Named("category") Provider<ContentProviderClient> clientProvider) {
this.clientProvider = clientProvider;
}
public void setName(String name) {
this.name = name;
}
private Date getLastUsed() {
// warning: Date objects are mutable.
return (Date)lastUsed.clone();
}
public void setLastUsed(Date lastUsed) {
// warning: Date objects are mutable.
this.lastUsed = (Date)lastUsed.clone();
}
private void touch() {
lastUsed = new Date();
}
private int getTimesUsed() {
return timesUsed;
}
public void setTimesUsed(int timesUsed) {
this.timesUsed = timesUsed;
}
public void incTimesUsed() {
timesUsed++;
touch();
}
//region Database/content-provider stuff
/**
* Persist category.
* @param client ContentProviderClient to handle DB connection
*/
public void save(ContentProviderClient client) {
public void save(Category category) {
ContentProviderClient db = clientProvider.get();
try {
if (contentUri == null) {
contentUri = client.insert(CategoryContentProvider.BASE_URI, this.toContentValues());
if (category.getContentUri() == null) {
category.setContentUri(db.insert(CategoryContentProvider.BASE_URI, toContentValues(category)));
} else {
client.update(contentUri, toContentValues(), null, null);
db.update(category.getContentUri(), toContentValues(category), null, null);
}
} catch (RemoteException e) {
throw new RuntimeException(e);
} finally {
db.release();
}
}
private ContentValues toContentValues() {
ContentValues cv = new ContentValues();
cv.put(Table.COLUMN_NAME, getName());
cv.put(Table.COLUMN_LAST_USED, getLastUsed().getTime());
cv.put(Table.COLUMN_TIMES_USED, getTimesUsed());
return cv;
}
private static Category fromCursor(Cursor cursor) {
// Hardcoding column positions!
Category c = new Category();
c.contentUri = CategoryContentProvider.uriForId(cursor.getInt(0));
c.name = cursor.getString(1);
c.lastUsed = new Date(cursor.getLong(2));
c.timesUsed = cursor.getInt(3);
return c;
}
/**
* Find persisted category in database, based on its name.
* @param client ContentProviderClient to handle DB connection
*
* @param name Category's name
* @return category from database, or null if not found
*/
public static @Nullable Category find(ContentProviderClient client, String name) {
@Nullable
Category find(String name) {
Cursor cursor = null;
ContentProviderClient db = clientProvider.get();
try {
cursor = client.query(
cursor = db.query(
CategoryContentProvider.BASE_URI,
Category.Table.ALL_FIELDS,
Category.Table.COLUMN_NAME + "=?",
Table.ALL_FIELDS,
Table.COLUMN_NAME + "=?",
new String[]{name},
null);
if (cursor != null && cursor.moveToFirst()) {
return Category.fromCursor(cursor);
return fromCursor(cursor);
}
} catch (RemoteException e) {
// This feels lazy, but to hell with checked exceptions. :)
@ -118,29 +67,32 @@ public class Category {
if (cursor != null) {
cursor.close();
}
db.release();
}
return null;
}
/**
* Retrieve recently-used categories, ordered by descending date.
*
* @return a list containing recent categories
*/
public static @NonNull ArrayList<String> recentCategories(ContentProviderClient client, int limit) {
ArrayList<String> items = new ArrayList<>();
@NonNull
List<String> recentCategories(int limit) {
List<String> items = new ArrayList<>();
Cursor cursor = null;
ContentProviderClient db = clientProvider.get();
try {
cursor = client.query(
cursor = db.query(
CategoryContentProvider.BASE_URI,
Category.Table.ALL_FIELDS,
Table.ALL_FIELDS,
null,
new String[]{},
Category.Table.COLUMN_LAST_USED + " DESC");
Table.COLUMN_LAST_USED + " DESC");
// fixme add a limit on the original query instead of falling out of the loop?
while (cursor != null && cursor.moveToNext()
&& cursor.getPosition() < limit) {
Category cat = Category.fromCursor(cursor);
items.add(cat.getName());
items.add(fromCursor(cursor).getName());
}
} catch (RemoteException e) {
throw new RuntimeException(e);
@ -148,17 +100,36 @@ public class Category {
if (cursor != null) {
cursor.close();
}
db.release();
}
return items;
}
Category fromCursor(Cursor cursor) {
// Hardcoding column positions!
return new Category(
CategoryContentProvider.uriForId(cursor.getInt(0)),
cursor.getString(1),
new Date(cursor.getLong(2)),
cursor.getInt(3)
);
}
private ContentValues toContentValues(Category category) {
ContentValues cv = new ContentValues();
cv.put(CategoryDao.Table.COLUMN_NAME, category.getName());
cv.put(CategoryDao.Table.COLUMN_LAST_USED, category.getLastUsed().getTime());
cv.put(CategoryDao.Table.COLUMN_TIMES_USED, category.getTimesUsed());
return cv;
}
public static class Table {
public static final String TABLE_NAME = "categories";
public static final String COLUMN_ID = "_id";
public static final String COLUMN_NAME = "name";
public static final String COLUMN_LAST_USED = "last_used";
public static final String COLUMN_TIMES_USED = "times_used";
static final String COLUMN_NAME = "name";
static final String COLUMN_LAST_USED = "last_used";
static final String COLUMN_TIMES_USED = "times_used";
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
public static final String[] ALL_FIELDS = {
@ -168,7 +139,9 @@ public class Category {
COLUMN_TIMES_USED
};
private static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME;
static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
+ COLUMN_ID + " INTEGER PRIMARY KEY,"
+ COLUMN_NAME + " STRING,"
+ COLUMN_LAST_USED + " INTEGER,"
@ -180,7 +153,7 @@ public class Category {
}
public static void onDelete(SQLiteDatabase db) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
db.execSQL(DROP_TABLE_STATEMENT);
onCreate(db);
}
@ -208,5 +181,4 @@ public class Category {
}
}
}
//endregion
}

View file

@ -1,13 +1,8 @@
package fr.free.nrw.commons.contributions;
import android.content.ContentProviderClient;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.Parcel;
import android.os.RemoteException;
import android.text.TextUtils;
import android.support.annotation.NonNull;
import java.text.SimpleDateFormat;
import java.util.Date;
@ -16,7 +11,6 @@ import java.util.Locale;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.settings.Prefs;
public class Contribution extends Media {
@ -43,7 +37,6 @@ public class Contribution extends Media {
public static final String SOURCE_GALLERY = "gallery";
public static final String SOURCE_EXTERNAL = "external";
private ContentProviderClient client;
private Uri contentUri;
private String source;
private String editSummary;
@ -51,24 +44,42 @@ public class Contribution extends Media {
private int state;
private long transferred;
private String decimalCoords;
private boolean isMultiple;
public boolean getMultiple() {
return isMultiple;
public Contribution(Uri contentUri, String filename, Uri localUri, String imageUrl, Date timestamp,
int state, long dataLength, Date dateUploaded, long transferred,
String source, String description, String creator, boolean isMultiple,
int width, int height, String license) {
super(localUri, imageUrl, filename, description, dataLength, timestamp, dateUploaded, creator);
this.contentUri = contentUri;
this.state = state;
this.timestamp = timestamp;
this.transferred = transferred;
this.source = source;
this.isMultiple = isMultiple;
this.width = width;
this.height = height;
this.license = license;
}
public void setMultiple(boolean multiple) {
isMultiple = multiple;
}
public Contribution(Uri localUri, String remoteUri, String filename, String description, long dataLength, Date dateCreated, Date dateUploaded, String creator, String editSummary, String decimalCoords) {
super(localUri, remoteUri, filename, description, dataLength, dateCreated, dateUploaded, creator);
public Contribution(Uri localUri, String imageUrl, String filename, String description, long dataLength,
Date dateCreated, Date dateUploaded, String creator, String editSummary, String decimalCoords) {
super(localUri, imageUrl, filename, description, dataLength, dateCreated, dateUploaded, creator);
this.decimalCoords = decimalCoords;
this.editSummary = editSummary;
timestamp = new Date(System.currentTimeMillis());
}
public Contribution(Parcel in) {
super(in);
contentUri = in.readParcelable(Uri.class.getClassLoader());
source = in.readString();
timestamp = (Date) in.readSerializable();
state = in.readInt();
transferred = in.readLong();
isMultiple = in.readInt() == 1;
}
@Override
public void writeToParcel(Parcel parcel, int flags) {
super.writeToParcel(parcel, flags);
@ -80,14 +91,12 @@ public class Contribution extends Media {
parcel.writeInt(isMultiple ? 1 : 0);
}
public Contribution(Parcel in) {
super(in);
contentUri = in.readParcelable(Uri.class.getClassLoader());
source = in.readString();
timestamp = (Date) in.readSerializable();
state = in.readInt();
transferred = in.readLong();
isMultiple = in.readInt() == 1;
public boolean getMultiple() {
return isMultiple;
}
public void setMultiple(boolean multiple) {
isMultiple = multiple;
}
public long getTransferred() {
@ -106,10 +115,18 @@ public class Contribution extends Media {
return contentUri;
}
public void setContentUri(Uri contentUri) {
this.contentUri = contentUri;
}
public Date getTimestamp() {
return timestamp;
}
public void setTimestamp(Date timestamp) {
this.timestamp = timestamp;
}
public int getState() {
return state;
}
@ -149,68 +166,12 @@ public class Contribution extends Media {
}
buffer.append("== {{int:license-header}} ==\n")
.append(Utils.licenseTemplateFor(getLicense())).append("\n\n")
.append(licenseTemplateFor(getLicense())).append("\n\n")
.append("{{Uploaded from Mobile|platform=Android|version=").append(BuildConfig.VERSION_NAME).append("}}\n")
.append(getTrackingTemplates());
return buffer.toString();
}
public void setContentProviderClient(ContentProviderClient client) {
this.client = client;
}
public void save() {
try {
if (contentUri == null) {
contentUri = client.insert(ContributionsContentProvider.BASE_URI, this.toContentValues());
} else {
client.update(contentUri, toContentValues(), null, null);
}
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
public void delete() {
try {
if (contentUri == null) {
// noooo
throw new RuntimeException("tried to delete item with no content URI");
} else {
client.delete(contentUri, null, null);
}
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
public ContentValues toContentValues() {
ContentValues cv = new ContentValues();
cv.put(Table.COLUMN_FILENAME, getFilename());
if (getLocalUri() != null) {
cv.put(Table.COLUMN_LOCAL_URI, getLocalUri().toString());
}
if (getImageUrl() != null) {
cv.put(Table.COLUMN_IMAGE_URL, getImageUrl());
}
if (getDateUploaded() != null) {
cv.put(Table.COLUMN_UPLOADED, getDateUploaded().getTime());
}
cv.put(Table.COLUMN_LENGTH, getDataLength());
cv.put(Table.COLUMN_TIMESTAMP, getTimestamp().getTime());
cv.put(Table.COLUMN_STATE, getState());
cv.put(Table.COLUMN_TRANSFERRED, transferred);
cv.put(Table.COLUMN_SOURCE, source);
cv.put(Table.COLUMN_DESCRIPTION, description);
cv.put(Table.COLUMN_CREATOR, creator);
cv.put(Table.COLUMN_MULTIPLE, isMultiple ? 1 : 0);
cv.put(Table.COLUMN_WIDTH, width);
cv.put(Table.COLUMN_HEIGHT, height);
cv.put(Table.COLUMN_LICENSE, license);
return cv;
}
@Override
public void setFilename(String filename) {
this.filename = filename;
@ -224,33 +185,6 @@ public class Contribution extends Media {
timestamp = new Date(System.currentTimeMillis());
}
public static Contribution fromCursor(Cursor cursor) {
// Hardcoding column positions!
Contribution c = new Contribution();
//Check that cursor has a value to avoid CursorIndexOutOfBoundsException
if (cursor.getCount() > 0) {
c.contentUri = ContributionsContentProvider.uriForId(cursor.getInt(0));
c.filename = cursor.getString(1);
c.localUri = TextUtils.isEmpty(cursor.getString(2)) ? null : Uri.parse(cursor.getString(2));
c.imageUrl = cursor.getString(3);
c.timestamp = cursor.getLong(4) == 0 ? null : new Date(cursor.getLong(4));
c.state = cursor.getInt(5);
c.dataLength = cursor.getLong(6);
c.dateUploaded = cursor.getLong(7) == 0 ? null : new Date(cursor.getLong(7));
c.transferred = cursor.getLong(8);
c.source = cursor.getString(9);
c.description = cursor.getString(10);
c.creator = cursor.getString(11);
c.isMultiple = cursor.getInt(12) == 1;
c.width = cursor.getInt(13);
c.height = cursor.getInt(14);
c.license = cursor.getString(15);
}
return c;
}
public String getSource() {
return source;
}
@ -267,118 +201,25 @@ public class Contribution extends Media {
this.decimalCoords = decimalCoords;
}
public static class Table {
public static final String TABLE_NAME = "contributions";
public static final String COLUMN_ID = "_id";
public static final String COLUMN_FILENAME = "filename";
public static final String COLUMN_LOCAL_URI = "local_uri";
public static final String COLUMN_IMAGE_URL = "image_url";
public static final String COLUMN_TIMESTAMP = "timestamp";
public static final String COLUMN_STATE = "state";
public static final String COLUMN_LENGTH = "length";
public static final String COLUMN_UPLOADED = "uploaded";
public static final String COLUMN_TRANSFERRED = "transferred"; // Currently transferred number of bytes
public static final String COLUMN_SOURCE = "source";
public static final String COLUMN_DESCRIPTION = "description";
public static final String COLUMN_CREATOR = "creator"; // Initial uploader
public static final String COLUMN_MULTIPLE = "multiple";
public static final String COLUMN_WIDTH = "width";
public static final String COLUMN_HEIGHT = "height";
public static final String COLUMN_LICENSE = "license";
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
public static final String[] ALL_FIELDS = {
COLUMN_ID,
COLUMN_FILENAME,
COLUMN_LOCAL_URI,
COLUMN_IMAGE_URL,
COLUMN_TIMESTAMP,
COLUMN_STATE,
COLUMN_LENGTH,
COLUMN_UPLOADED,
COLUMN_TRANSFERRED,
COLUMN_SOURCE,
COLUMN_DESCRIPTION,
COLUMN_CREATOR,
COLUMN_MULTIPLE,
COLUMN_WIDTH,
COLUMN_HEIGHT,
COLUMN_LICENSE
};
private static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
+ "_id INTEGER PRIMARY KEY,"
+ "filename STRING,"
+ "local_uri STRING,"
+ "image_url STRING,"
+ "uploaded INTEGER,"
+ "timestamp INTEGER,"
+ "state INTEGER,"
+ "length INTEGER,"
+ "transferred INTEGER,"
+ "source STRING,"
+ "description STRING,"
+ "creator STRING,"
+ "multiple INTEGER,"
+ "width INTEGER,"
+ "height INTEGER,"
+ "LICENSE STRING"
+ ");";
public static void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE_STATEMENT);
@NonNull
private String licenseTemplateFor(String license) {
switch (license) {
case Prefs.Licenses.CC_BY_3:
return "{{self|cc-by-3.0}}";
case Prefs.Licenses.CC_BY_4:
return "{{self|cc-by-4.0}}";
case Prefs.Licenses.CC_BY_SA_3:
return "{{self|cc-by-sa-3.0}}";
case Prefs.Licenses.CC_BY_SA_4:
return "{{self|cc-by-sa-4.0}}";
case Prefs.Licenses.CC0:
return "{{self|cc-zero}}";
case Prefs.Licenses.CC_BY:
return "{{self|cc-by-3.0}}";
case Prefs.Licenses.CC_BY_SA:
return "{{self|cc-by-sa-3.0}}";
}
public static void onDelete(SQLiteDatabase db) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
onCreate(db);
}
public static void onUpdate(SQLiteDatabase db, int from, int to) {
if (from == to) {
return;
}
if (from == 1) {
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN description STRING;");
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN creator STRING;");
from++;
onUpdate(db, from, to);
return;
}
if (from == 2) {
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN multiple INTEGER;");
db.execSQL("UPDATE " + TABLE_NAME + " SET multiple = 0");
from++;
onUpdate(db, from, to);
return;
}
if (from == 3) {
// Do nothing
from++;
onUpdate(db, from, to);
return;
}
if (from == 4) {
// Do nothing -- added Category
from++;
onUpdate(db, from, to);
return;
}
if (from == 5) {
// Added width and height fields
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN width INTEGER;");
db.execSQL("UPDATE " + TABLE_NAME + " SET width = 0");
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN height INTEGER;");
db.execSQL("UPDATE " + TABLE_NAME + " SET height = 0");
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN license STRING;");
db.execSQL("UPDATE " + TABLE_NAME + " SET license='" + Prefs.Licenses.CC_BY_SA_3 + "';");
from++;
onUpdate(db, from, to);
return;
}
}
throw new RuntimeException("Unrecognized license value: " + license);
}
}

View file

@ -93,10 +93,15 @@ class ContributionController {
shareIntent.putExtra(EXTRA_SOURCE, SOURCE_GALLERY);
break;
case SELECT_FROM_CAMERA:
shareIntent.setType("image/jpeg"); //FIXME: Find out appropriate mime type
//FIXME: Find out appropriate mime type
// AFAIK this is the right type for a JPEG image
// https://developer.android.com/training/sharing/send.html#send-binary-content
shareIntent.setType("image/jpeg");
shareIntent.putExtra(EXTRA_STREAM, lastGeneratedCaptureUri);
shareIntent.putExtra(EXTRA_SOURCE, SOURCE_CAMERA);
break;
default:
break;
}
Timber.i("Image selected");
try {

View file

@ -0,0 +1,281 @@
package fr.free.nrw.commons.contributions;
import android.content.ContentProviderClient;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.RemoteException;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import java.util.Date;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import fr.free.nrw.commons.settings.Prefs;
import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS;
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI;
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.uriForId;
public class ContributionDao {
/*
This sorts in the following order:
Currently Uploading
Failed (Sorted in ascending order of time added - FIFO)
Queued to Upload (Sorted in ascending order of time added - FIFO)
Completed (Sorted in descending order of time added)
This is why Contribution.STATE_COMPLETED is -1.
*/
static final String CONTRIBUTION_SORT = Table.COLUMN_STATE + " DESC, "
+ Table.COLUMN_UPLOADED + " DESC , ("
+ Table.COLUMN_TIMESTAMP + " * "
+ Table.COLUMN_STATE + ")";
private final Provider<ContentProviderClient> clientProvider;
@Inject
public ContributionDao(@Named("contribution") Provider<ContentProviderClient> clientProvider) {
this.clientProvider = clientProvider;
}
Cursor loadAllContributions() {
ContentProviderClient db = clientProvider.get();
try {
return db.query(BASE_URI, ALL_FIELDS, "", null, CONTRIBUTION_SORT);
} catch (RemoteException e) {
return null;
} finally {
db.release();
}
}
public void save(Contribution contribution) {
ContentProviderClient db = clientProvider.get();
try {
if (contribution.getContentUri() == null) {
contribution.setContentUri(db.insert(BASE_URI, toContentValues(contribution)));
} else {
db.update(contribution.getContentUri(), toContentValues(contribution), null, null);
}
} catch (RemoteException e) {
throw new RuntimeException(e);
} finally {
db.release();
}
}
public void delete(Contribution contribution) {
ContentProviderClient db = clientProvider.get();
try {
if (contribution.getContentUri() == null) {
// noooo
throw new RuntimeException("tried to delete item with no content URI");
} else {
db.delete(contribution.getContentUri(), null, null);
}
} catch (RemoteException e) {
throw new RuntimeException(e);
} finally {
db.release();
}
}
ContentValues toContentValues(Contribution contribution) {
ContentValues cv = new ContentValues();
cv.put(Table.COLUMN_FILENAME, contribution.getFilename());
if (contribution.getLocalUri() != null) {
cv.put(Table.COLUMN_LOCAL_URI, contribution.getLocalUri().toString());
}
if (contribution.getImageUrl() != null) {
cv.put(Table.COLUMN_IMAGE_URL, contribution.getImageUrl());
}
if (contribution.getDateUploaded() != null) {
cv.put(Table.COLUMN_UPLOADED, contribution.getDateUploaded().getTime());
}
cv.put(Table.COLUMN_LENGTH, contribution.getDataLength());
cv.put(Table.COLUMN_TIMESTAMP, contribution.getTimestamp().getTime());
cv.put(Table.COLUMN_STATE, contribution.getState());
cv.put(Table.COLUMN_TRANSFERRED, contribution.getTransferred());
cv.put(Table.COLUMN_SOURCE, contribution.getSource());
cv.put(Table.COLUMN_DESCRIPTION, contribution.getDescription());
cv.put(Table.COLUMN_CREATOR, contribution.getCreator());
cv.put(Table.COLUMN_MULTIPLE, contribution.getMultiple() ? 1 : 0);
cv.put(Table.COLUMN_WIDTH, contribution.getWidth());
cv.put(Table.COLUMN_HEIGHT, contribution.getHeight());
cv.put(Table.COLUMN_LICENSE, contribution.getLicense());
return cv;
}
public Contribution fromCursor(Cursor cursor) {
// Hardcoding column positions!
//Check that cursor has a value to avoid CursorIndexOutOfBoundsException
if (cursor.getCount() > 0) {
return new Contribution(
uriForId(cursor.getInt(0)),
cursor.getString(1),
parseUri(cursor.getString(2)),
cursor.getString(3),
parseTimestamp(cursor.getLong(4)),
cursor.getInt(5),
cursor.getLong(6),
parseTimestamp(cursor.getLong(7)),
cursor.getLong(8),
cursor.getString(9),
cursor.getString(10),
cursor.getString(11),
cursor.getInt(12) == 1,
cursor.getInt(13),
cursor.getInt(14),
cursor.getString(15));
}
return null;
}
@Nullable
private static Date parseTimestamp(long timestamp) {
return timestamp == 0 ? null : new Date(timestamp);
}
@Nullable
private static Uri parseUri(String uriString) {
return TextUtils.isEmpty(uriString) ? null : Uri.parse(uriString);
}
public static class Table {
public static final String TABLE_NAME = "contributions";
public static final String COLUMN_ID = "_id";
public static final String COLUMN_FILENAME = "filename";
public static final String COLUMN_LOCAL_URI = "local_uri";
public static final String COLUMN_IMAGE_URL = "image_url";
public static final String COLUMN_TIMESTAMP = "timestamp";
public static final String COLUMN_STATE = "state";
public static final String COLUMN_LENGTH = "length";
public static final String COLUMN_UPLOADED = "uploaded";
public static final String COLUMN_TRANSFERRED = "transferred"; // Currently transferred number of bytes
public static final String COLUMN_SOURCE = "source";
public static final String COLUMN_DESCRIPTION = "description";
public static final String COLUMN_CREATOR = "creator"; // Initial uploader
public static final String COLUMN_MULTIPLE = "multiple";
public static final String COLUMN_WIDTH = "width";
public static final String COLUMN_HEIGHT = "height";
public static final String COLUMN_LICENSE = "license";
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
public static final String[] ALL_FIELDS = {
COLUMN_ID,
COLUMN_FILENAME,
COLUMN_LOCAL_URI,
COLUMN_IMAGE_URL,
COLUMN_TIMESTAMP,
COLUMN_STATE,
COLUMN_LENGTH,
COLUMN_UPLOADED,
COLUMN_TRANSFERRED,
COLUMN_SOURCE,
COLUMN_DESCRIPTION,
COLUMN_CREATOR,
COLUMN_MULTIPLE,
COLUMN_WIDTH,
COLUMN_HEIGHT,
COLUMN_LICENSE
};
public static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME;
public static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
+ "_id INTEGER PRIMARY KEY,"
+ "filename STRING,"
+ "local_uri STRING,"
+ "image_url STRING,"
+ "uploaded INTEGER,"
+ "timestamp INTEGER,"
+ "state INTEGER,"
+ "length INTEGER,"
+ "transferred INTEGER,"
+ "source STRING,"
+ "description STRING,"
+ "creator STRING,"
+ "multiple INTEGER,"
+ "width INTEGER,"
+ "height INTEGER,"
+ "LICENSE STRING"
+ ");";
// Upgrade from version 1 ->
static final String ADD_CREATOR_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN creator STRING;";
static final String ADD_DESCRIPTION_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN description STRING;";
// Upgrade from version 2 ->
static final String ADD_MULTIPLE_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN multiple INTEGER;";
static final String SET_DEFAULT_MULTIPLE = "UPDATE " + TABLE_NAME + " SET multiple = 0";
// Upgrade from version 5 ->
static final String ADD_WIDTH_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN width INTEGER;";
static final String SET_DEFAULT_WIDTH = "UPDATE " + TABLE_NAME + " SET width = 0";
static final String ADD_HEIGHT_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN height INTEGER;";
static final String SET_DEFAULT_HEIGHT = "UPDATE " + TABLE_NAME + " SET height = 0";
static final String ADD_LICENSE_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN license STRING;";
static final String SET_DEFAULT_LICENSE = "UPDATE " + TABLE_NAME + " SET license='" + Prefs.Licenses.CC_BY_SA_3 + "';";
public static void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE_STATEMENT);
}
public static void onDelete(SQLiteDatabase db) {
db.execSQL(DROP_TABLE_STATEMENT);
onCreate(db);
}
public static void onUpdate(SQLiteDatabase db, int from, int to) {
if (from == to) {
return;
}
if (from == 1) {
db.execSQL(ADD_DESCRIPTION_FIELD);
db.execSQL(ADD_CREATOR_FIELD);
from++;
onUpdate(db, from, to);
return;
}
if (from == 2) {
db.execSQL(ADD_MULTIPLE_FIELD);
db.execSQL(SET_DEFAULT_MULTIPLE);
from++;
onUpdate(db, from, to);
return;
}
if (from == 3) {
// Do nothing
from++;
onUpdate(db, from, to);
return;
}
if (from == 4) {
// Do nothing -- added Category
from++;
onUpdate(db, from, to);
return;
}
if (from == 5) {
// Added width and height fields
db.execSQL(ADD_WIDTH_FIELD);
db.execSQL(SET_DEFAULT_WIDTH);
db.execSQL(ADD_HEIGHT_FIELD);
db.execSQL(SET_DEFAULT_HEIGHT);
db.execSQL(ADD_LICENSE_FIELD);
db.execSQL(SET_DEFAULT_LICENSE);
from++;
onUpdate(db, from, to);
return;
}
}
}
}

View file

@ -9,7 +9,6 @@ import android.database.Cursor;
import android.database.DataSetObserver;
import android.os.Bundle;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
@ -23,13 +22,17 @@ import android.widget.AdapterView;
import java.util.ArrayList;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.ButterKnife;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.HandlerService;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.AuthenticatedActivity;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.upload.UploadService;
import io.reactivex.android.schedulers.AndroidSchedulers;
@ -39,15 +42,22 @@ import timber.log.Timber;
import static android.content.ContentResolver.requestSync;
import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED;
import static fr.free.nrw.commons.contributions.Contribution.Table.ALL_FIELDS;
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.AUTHORITY;
import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS;
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI;
import static fr.free.nrw.commons.settings.Prefs.UPLOADS_SHOWING;
public class ContributionsActivity extends AuthenticatedActivity
implements LoaderManager.LoaderCallbacks<Cursor>, AdapterView.OnItemClickListener,
MediaDetailPagerFragment.MediaDetailProvider, FragmentManager.OnBackStackChangedListener,
ContributionsListFragment.SourceRefresher {
public class ContributionsActivity
extends AuthenticatedActivity
implements LoaderManager.LoaderCallbacks<Cursor>,
AdapterView.OnItemClickListener,
MediaDetailPagerFragment.MediaDetailProvider,
FragmentManager.OnBackStackChangedListener,
ContributionsListFragment.SourceRefresher {
@Inject MediaWikiApi mediaWikiApi;
@Inject SessionManager sessionManager;
@Inject @Named("default_preferences") SharedPreferences prefs;
@Inject ContributionDao contributionDao;
private Cursor allContributions;
private ContributionsListFragment contributionsList;
@ -55,21 +65,6 @@ public class ContributionsActivity extends AuthenticatedActivity
private UploadService uploadService;
private boolean isUploadServiceConnected;
private ArrayList<DataSetObserver> observersWaitingForLoad = new ArrayList<>();
private String CONTRIBUTION_SELECTION = "";
/*
This sorts in the following order:
Currently Uploading
Failed (Sorted in ascending order of time added - FIFO)
Queued to Upload (Sorted in ascending order of time added - FIFO)
Completed (Sorted in descending order of time added)
This is why Contribution.STATE_COMPLETED is -1.
*/
private String CONTRIBUTION_SORT = Contribution.Table.COLUMN_STATE + " DESC, "
+ Contribution.Table.COLUMN_UPLOADED + " DESC , ("
+ Contribution.Table.COLUMN_TIMESTAMP + " * "
+ Contribution.Table.COLUMN_STATE + ")";
private CompositeDisposable compositeDisposable = new CompositeDisposable();
@ -84,7 +79,7 @@ public class ContributionsActivity extends AuthenticatedActivity
@Override
public void onServiceDisconnected(ComponentName componentName) {
// this should never happen
throw new RuntimeException("UploadService died but the rest of the process did not!");
Timber.e(new RuntimeException("UploadService died but the rest of the process did not!"));
}
};
@ -101,12 +96,8 @@ public class ContributionsActivity extends AuthenticatedActivity
@Override
protected void onResume() {
super.onResume();
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
boolean isSettingsChanged =
sharedPreferences.getBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false);
editor.apply();
boolean isSettingsChanged = prefs.getBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false);
prefs.edit().putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false).apply();
if (isSettingsChanged) {
refreshSource();
}
@ -114,16 +105,14 @@ public class ContributionsActivity extends AuthenticatedActivity
@Override
protected void onAuthCookieAcquired(String authCookie) {
// Do a sync every time we get here!
CommonsApplication app = ((CommonsApplication) getApplication());
requestSync(app.getCurrentAccount(), AUTHORITY, new Bundle());
// Do a sync everytime we get here!
requestSync(sessionManager.getCurrentAccount(), ContributionsContentProvider.CONTRIBUTION_AUTHORITY, new Bundle());
Intent uploadServiceIntent = new Intent(this, UploadService.class);
uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE);
startService(uploadServiceIntent);
bindService(uploadServiceIntent, uploadServiceConnection, Context.BIND_AUTO_CREATE);
allContributions = getContentResolver().query(BASE_URI, ALL_FIELDS,
CONTRIBUTION_SELECTION, null, CONTRIBUTION_SORT);
allContributions = contributionDao.loadAllContributions();
getSupportLoaderManager().initLoader(0, null, this);
}
@ -137,17 +126,20 @@ public class ContributionsActivity extends AuthenticatedActivity
// Activity can call methods in the fragment by acquiring a
// reference to the Fragment from FragmentManager, using findFragmentById()
FragmentManager supportFragmentManager = getSupportFragmentManager();
contributionsList = (ContributionsListFragment) supportFragmentManager
contributionsList = (ContributionsListFragment)supportFragmentManager
.findFragmentById(R.id.contributionsListFragment);
supportFragmentManager.addOnBackStackChangedListener(this);
if (savedInstanceState != null) {
mediaDetails = (MediaDetailPagerFragment) supportFragmentManager
mediaDetails = (MediaDetailPagerFragment)supportFragmentManager
.findFragmentById(R.id.contributionsFragmentContainer);
getSupportLoaderManager().initLoader(0, null, this);
}
requestAuthToken();
initDrawer();
setTitle(getString(R.string.title_activity_contributions));
setUploadCount();
}
@Override
@ -178,24 +170,23 @@ public class ContributionsActivity extends AuthenticatedActivity
public void retryUpload(int i) {
allContributions.moveToPosition(i);
Contribution c = Contribution.fromCursor(allContributions);
Contribution c = contributionDao.fromCursor(allContributions);
if (c.getState() == STATE_FAILED) {
uploadService.queue(UploadService.ACTION_UPLOAD_FILE, c);
Timber.d("Restarting for %s", c.toContentValues());
Timber.d("Restarting for %s", c.toString());
} else {
Timber.d("Skipping re-upload for non-failed %s", c.toContentValues());
Timber.d("Skipping re-upload for non-failed %s", c.toString());
}
}
public void deleteUpload(int i) {
allContributions.moveToPosition(i);
Contribution c = Contribution.fromCursor(allContributions);
Contribution c = contributionDao.fromCursor(allContributions);
if (c.getState() == STATE_FAILED) {
Timber.d("Deleting failed contrib %s", c.toContentValues());
c.setContentProviderClient(getContentResolver().acquireContentProviderClient(AUTHORITY));
c.delete();
Timber.d("Deleting failed contrib %s", c.toString());
contributionDao.delete(c);
} else {
Timber.d("Skipping deletion for non-failed contrib %s", c.toContentValues());
Timber.d("Skipping deletion for non-failed contrib %s", c.toString());
}
}
@ -229,24 +220,23 @@ public class ContributionsActivity extends AuthenticatedActivity
@Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
int uploads = sharedPref.getInt(UPLOADS_SHOWING, 100);
int uploads = prefs.getInt(UPLOADS_SHOWING, 100);
return new CursorLoader(this, BASE_URI,
ALL_FIELDS, CONTRIBUTION_SELECTION, null,
CONTRIBUTION_SORT + "LIMIT " + uploads);
ALL_FIELDS, "", null,
ContributionDao.CONTRIBUTION_SORT + "LIMIT " + uploads);
}
@Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
contributionsList.changeProgressBarVisibility(false);
if (contributionsList.getAdapter() == null) {
contributionsList.setAdapter(new ContributionsListAdapter(getApplicationContext(),
cursor, 0));
cursor, 0, contributionDao));
} else {
((CursorAdapter) contributionsList.getAdapter()).swapCursor(cursor);
}
setUploadCount();
contributionsList.clearSyncMessage();
notifyAndMigrateDataSetObservers();
}
@ -263,7 +253,7 @@ public class ContributionsActivity extends AuthenticatedActivity
// not yet ready to return data
return null;
} else {
return Contribution.fromCursor((Cursor) contributionsList.getAdapter().getItem(i));
return contributionDao.fromCursor((Cursor) contributionsList.getAdapter().getItem(i));
}
}
@ -277,19 +267,16 @@ public class ContributionsActivity extends AuthenticatedActivity
@SuppressWarnings("ConstantConditions")
private void setUploadCount() {
CommonsApplication app = ((CommonsApplication) getApplication());
compositeDisposable.add(
app.getMWApi()
.getUploadCount(app.getCurrentAccount().name)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
uploadCount -> getSupportActionBar().setSubtitle(getResources()
.getQuantityString(R.plurals.contributions_subtitle,
uploadCount, uploadCount)),
t -> Timber.e(t, "Fetching upload count failed")
)
);
compositeDisposable.add(mediaWikiApi
.getUploadCount(sessionManager.getCurrentAccount().name)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
uploadCount -> getSupportActionBar().setSubtitle(getResources()
.getQuantityString(R.plurals.contributions_subtitle,
uploadCount, uploadCount)),
t -> Timber.e(t, "Fetching upload count failed")
));
}
@Override
@ -341,9 +328,4 @@ public class ContributionsActivity extends AuthenticatedActivity
public void refreshSource() {
getSupportLoaderManager().restartLoader(0, null, this);
}
public static void startYourself(Context context) {
context.startActivity(new Intent(context, ContributionsActivity.class));
}
}

View file

@ -1,6 +1,5 @@
package fr.free.nrw.commons.contributions;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
@ -10,36 +9,36 @@ import android.net.Uri;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import fr.free.nrw.commons.CommonsApplication;
import javax.inject.Inject;
import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.di.CommonsDaggerContentProvider;
import timber.log.Timber;
import static android.content.UriMatcher.NO_MATCH;
import static fr.free.nrw.commons.contributions.Contribution.Table.ALL_FIELDS;
import static fr.free.nrw.commons.contributions.Contribution.Table.TABLE_NAME;
import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS;
import static fr.free.nrw.commons.contributions.ContributionDao.Table.TABLE_NAME;
public class ContributionsContentProvider extends ContentProvider {
public class ContributionsContentProvider extends CommonsDaggerContentProvider {
private static final int CONTRIBUTIONS = 1;
private static final int CONTRIBUTIONS_ID = 2;
private static final String BASE_PATH = "contributions";
private static final UriMatcher uriMatcher = new UriMatcher(NO_MATCH);
public static final String AUTHORITY = "fr.free.nrw.commons.contributions.contentprovider";
public static final String CONTRIBUTION_AUTHORITY = "fr.free.nrw.commons.contributions.contentprovider";
public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY + "/" + BASE_PATH);
public static final Uri BASE_URI = Uri.parse("content://" + CONTRIBUTION_AUTHORITY + "/" + BASE_PATH);
static {
uriMatcher.addURI(AUTHORITY, BASE_PATH, CONTRIBUTIONS);
uriMatcher.addURI(AUTHORITY, BASE_PATH + "/#", CONTRIBUTIONS_ID);
uriMatcher.addURI(CONTRIBUTION_AUTHORITY, BASE_PATH, CONTRIBUTIONS);
uriMatcher.addURI(CONTRIBUTION_AUTHORITY, BASE_PATH + "/#", CONTRIBUTIONS_ID);
}
public static Uri uriForId(int id) {
return Uri.parse(BASE_URI.toString() + "/" + id);
}
@Override
public boolean onCreate() {
return false;
}
@Inject DBOpenHelper dbOpenHelper;
@SuppressWarnings("ConstantConditions")
@Override
@ -50,8 +49,7 @@ public class ContributionsContentProvider extends ContentProvider {
int uriType = uriMatcher.match(uri);
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext();
SQLiteDatabase db = app.getDBOpenHelper().getReadableDatabase();
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
Cursor cursor;
switch (uriType) {
@ -87,8 +85,7 @@ public class ContributionsContentProvider extends ContentProvider {
@Override
public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
int uriType = uriMatcher.match(uri);
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext();
SQLiteDatabase sqlDB = app.getDBOpenHelper().getWritableDatabase();
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
long id;
switch (uriType) {
case CONTRIBUTIONS:
@ -107,13 +104,12 @@ public class ContributionsContentProvider extends ContentProvider {
int rows;
int uriType = uriMatcher.match(uri);
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext();
SQLiteDatabase sqlDB = app.getDBOpenHelper().getWritableDatabase();
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
switch (uriType) {
case CONTRIBUTIONS_ID:
Timber.d("Deleting contribution id %s", uri.getLastPathSegment());
rows = sqlDB.delete(TABLE_NAME,
rows = db.delete(TABLE_NAME,
"_id = ?",
new String[]{uri.getLastPathSegment()}
);
@ -130,8 +126,7 @@ public class ContributionsContentProvider extends ContentProvider {
public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
Timber.d("Hello, bulk insert! (ContributionsContentProvider)");
int uriType = uriMatcher.match(uri);
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext();
SQLiteDatabase sqlDB = app.getDBOpenHelper().getWritableDatabase();
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
sqlDB.beginTransaction();
switch (uriType) {
case CONTRIBUTIONS:
@ -162,8 +157,7 @@ public class ContributionsContentProvider extends ContentProvider {
error out otherwise.
*/
int uriType = uriMatcher.match(uri);
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext();
SQLiteDatabase sqlDB = app.getDBOpenHelper().getWritableDatabase();
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
int rowsUpdated;
switch (uriType) {
case CONTRIBUTIONS:
@ -175,7 +169,7 @@ public class ContributionsContentProvider extends ContentProvider {
if (TextUtils.isEmpty(selection)) {
rowsUpdated = sqlDB.update(TABLE_NAME,
contentValues,
Contribution.Table.COLUMN_ID + " = ?",
ContributionDao.Table.COLUMN_ID + " = ?",
new String[]{String.valueOf(id)});
} else {
throw new IllegalArgumentException(

View file

@ -11,8 +11,11 @@ import fr.free.nrw.commons.R;
class ContributionsListAdapter extends CursorAdapter {
public ContributionsListAdapter(Context context, Cursor c, int flags) {
private final ContributionDao contributionDao;
public ContributionsListAdapter(Context context, Cursor c, int flags, ContributionDao contributionDao) {
super(context, c, flags);
this.contributionDao = contributionDao;
}
@Override
@ -26,7 +29,7 @@ class ContributionsListAdapter extends CursorAdapter {
@Override
public void bindView(View view, Context context, Cursor cursor) {
final ContributionViewHolder views = (ContributionViewHolder)view.getTag();
final Contribution contribution = Contribution.fromCursor(cursor);
final Contribution contribution = contributionDao.fromCursor(cursor);
views.imageView.setMedia(contribution);
views.titleView.setText(contribution.getDisplayTitle());
@ -34,7 +37,7 @@ class ContributionsListAdapter extends CursorAdapter {
views.seqNumView.setText(String.valueOf(cursor.getPosition() + 1));
views.seqNumView.setVisibility(View.VISIBLE);
switch(contribution.getState()) {
switch (contribution.getState()) {
case Contribution.STATE_COMPLETED:
views.stateView.setVisibility(View.GONE);
views.progressView.setVisibility(View.GONE);
@ -50,7 +53,7 @@ class ContributionsListAdapter extends CursorAdapter {
views.progressView.setVisibility(View.VISIBLE);
long total = contribution.getDataLength();
long transferred = contribution.getTransferred();
if(transferred == 0 || transferred >= total) {
if (transferred == 0 || transferred >= total) {
views.progressView.setIndeterminate(true);
} else {
views.progressView.setProgress((int)(((double)transferred / (double)total) * 100));

View file

@ -5,9 +5,7 @@ import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
import android.view.LayoutInflater;
@ -19,30 +17,43 @@ import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.GridView;
import android.widget.ListAdapter;
import android.widget.ProgressBar;
import android.widget.TextView;
import java.util.Arrays;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.nearby.NearbyActivity;
import timber.log.Timber;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
import static android.app.Activity.RESULT_OK;
import static android.content.Context.MODE_PRIVATE;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.view.View.GONE;
public class ContributionsListFragment extends Fragment {
public class ContributionsListFragment extends CommonsDaggerSupportFragment {
@BindView(R.id.contributionsList)
GridView contributionsList;
@BindView(R.id.waitingMessage)
TextView waitingMessage;
@BindView(R.id.emptyMessage)
TextView emptyMessage;
@BindView(R.id.loadingContributionsProgressBar)
ProgressBar progressBar;
@Inject
@Named("prefs")
SharedPreferences prefs;
@Inject
@Named("default_preferences")
SharedPreferences defaultPrefs;
private ContributionController controller;
@Override
@ -57,7 +68,6 @@ public class ContributionsListFragment extends Fragment {
}
//TODO: Should this be in onResume?
SharedPreferences prefs = getActivity().getSharedPreferences("prefs", MODE_PRIVATE);
String lastModified = prefs.getString("lastSyncTimestamp", "");
Timber.d("Last Sync Timestamp: %s", lastModified);
@ -67,6 +77,7 @@ public class ContributionsListFragment extends Fragment {
waitingMessage.setVisibility(GONE);
}
changeProgressBarVisibility(true);
return v;
}
@ -78,6 +89,10 @@ public class ContributionsListFragment extends Fragment {
this.contributionsList.setAdapter(adapter);
}
public void changeProgressBarVisibility(boolean isVisible) {
this.progressBar.setVisibility(isVisible ? View.VISIBLE : View.GONE);
}
@Override
public void onSaveInstanceState(Bundle outState) {
if (outState == null) {
@ -155,9 +170,7 @@ public class ContributionsListFragment extends Fragment {
return true;
case R.id.menu_from_camera:
SharedPreferences sharedPref = PreferenceManager
.getDefaultSharedPreferences(CommonsApplication.getInstance());
boolean useExtStorage = sharedPref.getBoolean("useExternalStorage", true);
boolean useExtStorage = defaultPrefs.getBoolean("useExternalStorage", true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && useExtStorage) {
// Here, thisActivity is the current activity
if (ContextCompat.checkSelfPermission(getActivity(), WRITE_EXTERNAL_STORAGE)
@ -201,7 +214,7 @@ public class ContributionsListFragment extends Fragment {
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
Timber.d("onRequestPermissionsResult: req code = " + " perm = "
+ permissions + " grant =" + grantResults);
+ Arrays.toString(permissions) + " grant =" + Arrays.toString(grantResults));
switch (requestCode) {
// 1 = Storage allowed when gallery selected
@ -235,12 +248,17 @@ public class ContributionsListFragment extends Fragment {
menu.clear(); // See http://stackoverflow.com/a/8495697/17865
inflater.inflate(R.menu.fragment_contributions_list, menu);
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext();
if (!app.deviceHasCamera()) {
if (!deviceHasCamera()) {
menu.findItem(R.id.menu_from_camera).setEnabled(false);
}
}
public boolean deviceHasCamera() {
PackageManager pm = getContext().getPackageManager();
return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA) ||
pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

View file

@ -13,21 +13,27 @@ import android.os.RemoteException;
import android.text.TextUtils;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.mwapi.LogEventResult;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber;
import static android.content.Context.MODE_PRIVATE;
import static fr.free.nrw.commons.contributions.Contribution.STATE_COMPLETED;
import static fr.free.nrw.commons.contributions.Contribution.Table.COLUMN_FILENAME;
import static fr.free.nrw.commons.contributions.ContributionDao.Table.COLUMN_FILENAME;
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI;
@SuppressWarnings("WeakerAccess")
public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
private static final String[] existsQuery = {COLUMN_FILENAME};
@ -35,6 +41,10 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
private static final ContentValues[] EMPTY = {};
private static int COMMIT_THRESHOLD = 10;
@SuppressWarnings("WeakerAccess")
@Inject MediaWikiApi mwApi;
@Inject @Named("prefs") SharedPreferences prefs;
public ContributionsSyncAdapter(Context context, boolean autoInitialize) {
super(context, autoInitialize);
}
@ -47,6 +57,9 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
}
private boolean fileExists(ContentProviderClient client, String filename) {
if (filename == null) {
return false;
}
Cursor cursor = null;
try {
cursor = client.query(BASE_URI,
@ -68,19 +81,23 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
@Override
public void onPerformSync(Account account, Bundle bundle, String authority,
ContentProviderClient contentProviderClient, SyncResult syncResult) {
ApplicationlessInjection
.getInstance(getContext()
.getApplicationContext())
.getCommonsApplicationComponent()
.inject(this);
// This code is fraught with possibilities of race conditions, but lalalalala I can't hear you!
String user = account.name;
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
SharedPreferences prefs = getContext().getSharedPreferences("prefs", MODE_PRIVATE);
String lastModified = prefs.getString("lastSyncTimestamp", "");
Date curTime = new Date();
LogEventResult result;
Boolean done = false;
String queryContinue = null;
ContributionDao contributionDao = new ContributionDao(() -> contentProviderClient);
while (!done) {
try {
result = api.logEvents(user, lastModified, queryContinue, getLimit());
result = mwApi.logEvents(user, lastModified, queryContinue, getLimit());
} catch (IOException e) {
// There isn't really much we can do, eh?
// FIXME: Perhaps add EventLogging?
@ -109,7 +126,7 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
"", -1, dateUpdated, dateUpdated, user,
"", "");
contrib.setState(STATE_COMPLETED);
imageValues.add(contrib.toContentValues());
imageValues.add(contributionDao.toContentValues(contrib));
if (imageValues.size() % COMMIT_THRESHOLD == 0) {
try {
@ -134,8 +151,13 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
done = true;
}
}
prefs.edit().putString("lastSyncTimestamp", Utils.toMWDate(curTime)).apply();
prefs.edit().putString("lastSyncTimestamp", toMWDate(curTime)).apply();
Timber.d("Oh hai, everyone! Look, a kitty!");
}
private String toMWDate(Date date) {
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH); // Assuming MW always gives me UTC
isoFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
return isoFormat.format(date);
}
}

View file

@ -4,16 +4,18 @@ import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.modifications.ModifierSequence;
import fr.free.nrw.commons.category.CategoryDao;
import fr.free.nrw.commons.contributions.ContributionDao;
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
public class DBOpenHelper extends SQLiteOpenHelper{
public class DBOpenHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "commons.db";
private static final int DATABASE_VERSION = 6;
/**
* Do not use, please call CommonsApplication.getDBOpenHelper()
* Do not use directly - @Inject an instance where it's needed and let
* dependency injection take care of managing this as a singleton.
*/
public DBOpenHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
@ -21,15 +23,15 @@ public class DBOpenHelper extends SQLiteOpenHelper{
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
Contribution.Table.onCreate(sqLiteDatabase);
ModifierSequence.Table.onCreate(sqLiteDatabase);
Category.Table.onCreate(sqLiteDatabase);
ContributionDao.Table.onCreate(sqLiteDatabase);
ModifierSequenceDao.Table.onCreate(sqLiteDatabase);
CategoryDao.Table.onCreate(sqLiteDatabase);
}
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int from, int to) {
Contribution.Table.onUpdate(sqLiteDatabase, from, to);
ModifierSequence.Table.onUpdate(sqLiteDatabase, from, to);
Category.Table.onUpdate(sqLiteDatabase, from, to);
ContributionDao.Table.onUpdate(sqLiteDatabase, from, to);
ModifierSequenceDao.Table.onUpdate(sqLiteDatabase, from, to);
CategoryDao.Table.onUpdate(sqLiteDatabase, from, to);
}
}

View file

@ -0,0 +1,49 @@
package fr.free.nrw.commons.di;
import dagger.Module;
import dagger.android.ContributesAndroidInjector;
import fr.free.nrw.commons.AboutActivity;
import fr.free.nrw.commons.WelcomeActivity;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.auth.SignupActivity;
import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.nearby.NearbyActivity;
import fr.free.nrw.commons.notification.NotificationActivity;
import fr.free.nrw.commons.settings.SettingsActivity;
import fr.free.nrw.commons.upload.MultipleShareActivity;
import fr.free.nrw.commons.upload.ShareActivity;
@Module
@SuppressWarnings({"WeakerAccess", "unused"})
public abstract class ActivityBuilderModule {
@ContributesAndroidInjector
abstract LoginActivity bindLoginActivity();
@ContributesAndroidInjector
abstract WelcomeActivity bindWelcomeActivity();
@ContributesAndroidInjector
abstract ShareActivity bindShareActivity();
@ContributesAndroidInjector
abstract MultipleShareActivity bindMultipleShareActivity();
@ContributesAndroidInjector
abstract ContributionsActivity bindContributionsActivity();
@ContributesAndroidInjector
abstract SettingsActivity bindSettingsActivity();
@ContributesAndroidInjector
abstract AboutActivity bindAboutActivity();
@ContributesAndroidInjector
abstract SignupActivity bindSignupActivity();
@ContributesAndroidInjector
abstract NearbyActivity bindNearbyActivity();
@ContributesAndroidInjector
abstract NotificationActivity bindNotificationActivity();
}

View file

@ -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;
}
}

View file

@ -0,0 +1,49 @@
package fr.free.nrw.commons.di;
import javax.inject.Singleton;
import dagger.Component;
import dagger.android.AndroidInjectionModule;
import dagger.android.AndroidInjector;
import dagger.android.support.AndroidSupportInjectionModule;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.MediaWikiImageView;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.contributions.ContributionsSyncAdapter;
import fr.free.nrw.commons.modifications.ModificationsSyncAdapter;
import fr.free.nrw.commons.settings.SettingsFragment;
@Singleton
@Component(modules = {
CommonsApplicationModule.class,
AndroidInjectionModule.class,
AndroidSupportInjectionModule.class,
ActivityBuilderModule.class,
FragmentBuilderModule.class,
ServiceBuilderModule.class,
ContentProviderBuilderModule.class
})
public interface CommonsApplicationComponent extends AndroidInjector<ApplicationlessInjection> {
void inject(CommonsApplication application);
void inject(ContributionsSyncAdapter syncAdapter);
void inject(ModificationsSyncAdapter syncAdapter);
void inject(MediaWikiImageView mediaWikiImageView);
void inject(LoginActivity activity);
void inject(SettingsFragment fragment);
@Override
void inject(ApplicationlessInjection instance);
@Component.Builder
@SuppressWarnings({"WeakerAccess", "unused"})
interface Builder {
Builder appModule(CommonsApplicationModule applicationModule);
CommonsApplicationComponent build();
}
}

View file

@ -0,0 +1,139 @@
package fr.free.nrw.commons.di;
import android.content.ContentProviderClient;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.support.v4.util.LruCache;
import javax.inject.Named;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.caching.CacheController;
import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.nearby.NearbyPlaces;
import fr.free.nrw.commons.upload.UploadController;
import static android.content.Context.MODE_PRIVATE;
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.CONTRIBUTION_AUTHORITY;
import static fr.free.nrw.commons.modifications.ModificationsContentProvider.MODIFICATIONS_AUTHORITY;
@Module
@SuppressWarnings({"WeakerAccess", "unused"})
public class CommonsApplicationModule {
public static final String CATEGORY_AUTHORITY = "fr.free.nrw.commons.categories.contentprovider";
public static final long OK_HTTP_CACHE_SIZE = 10 * 1024 * 1024;
private CommonsApplication application;
private Context applicationContext;
public CommonsApplicationModule(Context applicationContext) {
this.applicationContext = applicationContext;
}
@Provides
public Context providesApplicationContext() {
return this.applicationContext;
}
@Provides
public AccountUtil providesAccountUtil(Context context) {
return new AccountUtil(context);
}
@Provides
@Named("category")
public ContentProviderClient provideCategoryContentProviderClient(Context context) {
return context.getContentResolver().acquireContentProviderClient(CATEGORY_AUTHORITY);
}
@Provides
@Named("contribution")
public ContentProviderClient provideContributionContentProviderClient(Context context) {
return context.getContentResolver().acquireContentProviderClient(CONTRIBUTION_AUTHORITY);
}
@Provides
@Named("modification")
public ContentProviderClient provideModificationContentProviderClient(Context context) {
return context.getContentResolver().acquireContentProviderClient(MODIFICATIONS_AUTHORITY);
}
@Provides
@Named("application_preferences")
public SharedPreferences providesApplicationSharedPreferences(Context context) {
return context.getSharedPreferences("fr.free.nrw.commons", MODE_PRIVATE);
}
@Provides
@Named("default_preferences")
public SharedPreferences providesDefaultSharedPreferences(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context);
}
@Provides
@Named("prefs")
public SharedPreferences providesOtherSharedPreferences(Context context) {
return context.getSharedPreferences("prefs", MODE_PRIVATE);
}
@Provides
public UploadController providesUploadController(Context context,
SessionManager sessionManager,
@Named("default_preferences") SharedPreferences sharedPreferences) {
return new UploadController(sessionManager, context, sharedPreferences);
}
@Provides
@Singleton
public SessionManager providesSessionManager(Context context,
MediaWikiApi mediaWikiApi,
@Named("default_preferences") SharedPreferences sharedPreferences) {
return new SessionManager(context, mediaWikiApi, sharedPreferences);
}
@Provides
@Singleton
public MediaWikiApi provideMediaWikiApi(Context context, @Named("default_preferences") SharedPreferences sharedPreferences) {
return new ApacheHttpClientMediaWikiApi(context, BuildConfig.WIKIMEDIA_API_HOST, sharedPreferences);
}
@Provides
@Singleton
public LocationServiceManager provideLocationServiceManager(Context context) {
return new LocationServiceManager(context);
}
@Provides
@Singleton
public CacheController provideCacheController() {
return new CacheController();
}
@Provides
@Singleton
public DBOpenHelper provideDBOpenHelper(Context context) {
return new DBOpenHelper(context);
}
@Provides
@Singleton
public NearbyPlaces provideNearbyPlaces() {
return new NearbyPlaces();
}
@Provides
@Singleton
public LruCache<String, String> provideLruCache() {
return new LruCache<>(1024);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,22 @@
package fr.free.nrw.commons.di;
import dagger.Module;
import dagger.android.ContributesAndroidInjector;
import fr.free.nrw.commons.category.CategoryContentProvider;
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
@Module
@SuppressWarnings({"WeakerAccess", "unused"})
public abstract class ContentProviderBuilderModule {
@ContributesAndroidInjector
abstract ContributionsContentProvider bindContributionsContentProvider();
@ContributesAndroidInjector
abstract ModificationsContentProvider bindModificationsContentProvider();
@ContributesAndroidInjector
abstract CategoryContentProvider bindCategoryContentProvider();
}

View file

@ -0,0 +1,46 @@
package fr.free.nrw.commons.di;
import dagger.Module;
import dagger.android.ContributesAndroidInjector;
import fr.free.nrw.commons.category.CategorizationFragment;
import fr.free.nrw.commons.contributions.ContributionsListFragment;
import fr.free.nrw.commons.media.MediaDetailFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.nearby.NearbyListFragment;
import fr.free.nrw.commons.nearby.NoPermissionsFragment;
import fr.free.nrw.commons.settings.SettingsFragment;
import fr.free.nrw.commons.upload.MultipleUploadListFragment;
import fr.free.nrw.commons.upload.SingleUploadFragment;
@Module
@SuppressWarnings({"WeakerAccess", "unused"})
public abstract class FragmentBuilderModule {
@ContributesAndroidInjector
abstract CategorizationFragment bindCategorizationFragment();
@ContributesAndroidInjector
abstract ContributionsListFragment bindContributionsListFragment();
@ContributesAndroidInjector
abstract MediaDetailFragment bindMediaDetailFragment();
@ContributesAndroidInjector
abstract MediaDetailPagerFragment bindMediaDetailPagerFragment();
@ContributesAndroidInjector
abstract NearbyListFragment bindNearbyListFragment();
@ContributesAndroidInjector
abstract NoPermissionsFragment bindNoPermissionsFragment();
@ContributesAndroidInjector
abstract SettingsFragment bindSettingsFragment();
@ContributesAndroidInjector
abstract MultipleUploadListFragment bindMultipleUploadListFragment();
@ContributesAndroidInjector
abstract SingleUploadFragment bindSingleUploadFragment();
}

View file

@ -0,0 +1,18 @@
package fr.free.nrw.commons.di;
import dagger.Module;
import dagger.android.ContributesAndroidInjector;
import fr.free.nrw.commons.auth.WikiAccountAuthenticatorService;
import fr.free.nrw.commons.upload.UploadService;
@Module
@SuppressWarnings({"WeakerAccess", "unused"})
public abstract class ServiceBuilderModule {
@ContributesAndroidInjector
abstract UploadService bindUploadService();
@ContributesAndroidInjector
abstract WikiAccountAuthenticatorService bindWikiAccountAuthenticatorService();
}

View file

@ -1,19 +1,32 @@
package fr.free.nrw.commons.location;
import android.location.Location;
import android.support.annotation.NonNull;
/**
* a latitude and longitude point with accuracy information, often of a picture
*/
public class LatLng {
private final double latitude;
private final double longitude;
private final float accuracy;
/** Accepts latitude and longitude.
/**
* Accepts latitude and longitude.
* North and South values are cut off at 90°
*
* @param latitude double value
* @param longitude double value
* @param latitude the latitude
* @param longitude the longitude
* @param accuracy the accuracy
*
* Examples:
* the Statue of Liberty is located at 40.69° N, 74.04° W
* The Statue of Liberty could be constructed as LatLng(40.69, -74.04, 1.0)
* where positive signifies north, east and negative signifies south, west.
*/
public LatLng(double latitude, double longitude, float accuracy) {
if(-180.0D <= longitude && longitude < 180.0D) {
if (-180.0D <= longitude && longitude < 180.0D) {
this.longitude = longitude;
} else {
this.longitude = ((longitude - 180.0D) % 360.0D + 360.0D) % 360.0D - 180.0D;
@ -22,20 +35,35 @@ public class LatLng {
this.accuracy = accuracy;
}
public int hashCode() {
boolean var1 = true;
byte var2 = 1;
long var3 = Double.doubleToLongBits(this.latitude);
int var5 = 31 * var2 + (int)(var3 ^ var3 >>> 32);
var3 = Double.doubleToLongBits(this.longitude);
var5 = 31 * var5 + (int)(var3 ^ var3 >>> 32);
return var5;
/**
* gets the latitude and longitude of a given non-null location
* @param location the non-null location of the user
* @return LatLng the Latitude and Longitude of a given location
*/
public static LatLng from(@NonNull Location location) {
return new LatLng(location.getLatitude(), location.getLongitude(), location.getAccuracy());
}
/**
* creates a hash code for the longitude and longitude
*/
public int hashCode() {
byte var1 = 1;
long var2 = Double.doubleToLongBits(this.latitude);
int var3 = 31 * var1 + (int)(var2 ^ var2 >>> 32);
var2 = Double.doubleToLongBits(this.longitude);
var3 = 31 * var3 + (int)(var2 ^ var2 >>> 32);
return var3;
}
/**
* checks for equality of two LatLng objects
* @param o the second LatLng object
*/
public boolean equals(Object o) {
if(this == o) {
if (this == o) {
return true;
} else if(!(o instanceof LatLng)) {
} else if (!(o instanceof LatLng)) {
return false;
} else {
LatLng var2 = (LatLng)o;
@ -43,6 +71,9 @@ public class LatLng {
}
}
/**
* returns a string representation of the latitude and longitude
*/
public String toString() {
return "lat/lng: (" + this.latitude + "," + this.longitude + ")";
}

View file

@ -1,62 +1,184 @@
package fr.free.nrw.commons.location;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.location.Criteria;
import android.content.pm.PackageManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import timber.log.Timber;
public class LocationServiceManager implements LocationListener {
public static final int LOCATION_REQUEST = 1;
private String provider;
private static final long MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS = 2 * 60 * 1000;
private static final long MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS = 10;
private Context context;
private LocationManager locationManager;
private LatLng latestLocation;
private Float latestLocationAccuracy;
private Location lastLocation;
private final List<LocationUpdateListener> locationListeners = new CopyOnWriteArrayList<>();
private boolean isLocationManagerRegistered = false;
/**
* Constructs a new instance of LocationServiceManager.
*
* @param context the context
*/
public LocationServiceManager(Context context) {
this.context = context;
this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
provider = locationManager.getBestProvider(new Criteria(), true);
}
public LatLng getLatestLocation() {
return latestLocation;
}
/**
* Returns the accuracy of the location. The measurement is
* given as a radius in meter of 68 % confidence.
* Returns the current status of the GPS provider.
*
* @return Float
* @return true if the GPS provider is enabled
*/
public Float getLatestLocationAccuracy() {
return latestLocationAccuracy;
public boolean isProviderEnabled() {
return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
}
/** Registers a LocationManager to listen for current location.
/**
* Returns whether the location permission is granted.
*
* @return true if the location permission is granted
*/
public boolean isLocationPermissionGranted() {
return ContextCompat.checkSelfPermission(context,
Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED;
}
/**
* Requests the location permission to be granted.
*
* @param activity the activity
*/
public void requestPermissions(Activity activity) {
if (activity.isFinishing()) {
return;
}
ActivityCompat.requestPermissions(activity,
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
LOCATION_REQUEST);
}
public boolean isPermissionExplanationRequired(Activity activity) {
return !activity.isFinishing() &&
ActivityCompat.shouldShowRequestPermissionRationale(activity,
Manifest.permission.ACCESS_FINE_LOCATION);
}
public LatLng getLastLocation() {
if (lastLocation == null) {
return null;
}
return LatLng.from(lastLocation);
}
/**
* Registers a LocationManager to listen for current location.
*/
public void registerLocationManager() {
if (!isLocationManagerRegistered)
isLocationManagerRegistered = requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER)
&& requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER);
}
/**
* Requests location updates from the specified provider.
*
* @param locationProvider the location provider
* @return true if successful
*/
private boolean requestLocationUpdatesFromProvider(String locationProvider) {
try {
locationManager.requestLocationUpdates(provider, 400, 1, this);
Location location = locationManager.getLastKnownLocation(provider);
//Location works, just need to 'send' GPS coords
// via emulator extended controls if testing on emulator
Timber.d("Checking for location...");
if (location != null) {
this.onLocationChanged(location);
}
locationManager.requestLocationUpdates(locationProvider,
MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS,
MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS,
this);
return true;
} catch (IllegalArgumentException e) {
Timber.e(e, "Illegal argument exception");
return false;
} catch (SecurityException e) {
Timber.e(e, "Security exception");
return false;
}
}
/** Unregisters location manager.
/**
* Returns whether a given location is better than the current best location.
*
* @param location the location to be tested
* @param currentBestLocation the current best location
* @return true if the given location is better
*/
protected boolean isBetterLocation(Location location, Location currentBestLocation) {
if (currentBestLocation == null) {
// A new location is always better than no location
return true;
}
// Check whether the new location fix is newer or older
long timeDelta = location.getTime() - currentBestLocation.getTime();
boolean isSignificantlyNewer = timeDelta > MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS;
boolean isSignificantlyOlder = timeDelta < -MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS;
boolean isNewer = timeDelta > 0;
// If it's been more than two minutes since the current location, use the new location
// because the user has likely moved
if (isSignificantlyNewer) {
return true;
// If the new location is more than two minutes older, it must be worse
} else if (isSignificantlyOlder) {
return false;
}
// Check whether the new location fix is more or less accurate
int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy());
boolean isLessAccurate = accuracyDelta > 0;
boolean isMoreAccurate = accuracyDelta < 0;
boolean isSignificantlyLessAccurate = accuracyDelta > 200;
// Check if the old and new location are from the same provider
boolean isFromSameProvider = isSameProvider(location.getProvider(),
currentBestLocation.getProvider());
// Determine location quality using a combination of timeliness and accuracy
if (isMoreAccurate) {
return true;
} else if (isNewer && !isLessAccurate) {
return true;
} else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) {
return true;
}
return false;
}
/**
* Checks whether two providers are the same
*/
private boolean isSameProvider(String provider1, String provider2) {
if (provider1 == null) {
return provider2 == null;
}
return provider1.equals(provider2);
}
/**
* Unregisters location manager.
*/
public void unregisterLocationManager() {
isLocationManagerRegistered = false;
try {
locationManager.removeUpdates(this);
} catch (SecurityException e) {
@ -64,15 +186,34 @@ public class LocationServiceManager implements LocationListener {
}
}
/**
* Adds a new listener to the list of location listeners.
*
* @param listener the new listener
*/
public void addLocationListener(LocationUpdateListener listener) {
if (!locationListeners.contains(listener)) {
locationListeners.add(listener);
}
}
/**
* Removes a listener from the list of location listeners.
*
* @param listener the listener to be removed
*/
public void removeLocationListener(LocationUpdateListener listener) {
locationListeners.remove(listener);
}
@Override
public void onLocationChanged(Location location) {
double currentLatitude = location.getLatitude();
double currentLongitude = location.getLongitude();
latestLocationAccuracy = location.getAccuracy();
Timber.d("Latitude: %f Longitude: %f Accuracy %f",
currentLatitude, currentLongitude, latestLocationAccuracy);
latestLocation = new LatLng(currentLatitude, currentLongitude, latestLocationAccuracy);
if (isBetterLocation(location, lastLocation)) {
lastLocation = location;
for (LocationUpdateListener listener : locationListeners) {
listener.onLocationChanged(LatLng.from(lastLocation));
}
}
}
@Override

View file

@ -0,0 +1,5 @@
package fr.free.nrw.commons.location;
public interface LocationUpdateListener {
void onLocationChanged(LatLng latLng);
}

View file

@ -6,7 +6,6 @@ import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
@ -22,6 +21,9 @@ import java.util.ArrayList;
import java.util.Date;
import java.util.Locale;
import javax.inject.Inject;
import javax.inject.Provider;
import fr.free.nrw.commons.License;
import fr.free.nrw.commons.LicenseList;
import fr.free.nrw.commons.Media;
@ -29,11 +31,12 @@ import fr.free.nrw.commons.MediaDataExtractor;
import fr.free.nrw.commons.MediaWikiImageView;
import fr.free.nrw.commons.PageTitle;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.ui.widget.CompatTextView;
import timber.log.Timber;
public class MediaDetailFragment extends Fragment {
public class MediaDetailFragment extends CommonsDaggerSupportFragment {
private boolean editable;
private MediaDetailPagerFragment.MediaDetailProvider detailProvider;
@ -53,6 +56,9 @@ public class MediaDetailFragment extends Fragment {
return mf;
}
@Inject
Provider<MediaDataExtractor> mediaDataExtractorProvider;
private MediaWikiImageView image;
private MediaDetailSpacer spacer;
private int initialListTop = 0;
@ -69,8 +75,8 @@ public class MediaDetailFragment extends Fragment {
private boolean categoriesPresent = false;
private ViewTreeObserver.OnGlobalLayoutListener layoutListener; // for layout stuff, only used once!
private ViewTreeObserver.OnScrollChangedListener scrollListener;
DataSetObserver dataObserver;
private AsyncTask<Void,Void,Boolean> detailFetchTask;
private DataSetObserver dataObserver;
private AsyncTask<Void, Void, Boolean> detailFetchTask;
private LicenseList licenseList;
@Override
@ -89,7 +95,7 @@ public class MediaDetailFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
detailProvider = (MediaDetailPagerFragment.MediaDetailProvider)getActivity();
detailProvider = (MediaDetailPagerFragment.MediaDetailProvider) getActivity();
if (savedInstanceState != null) {
editable = savedInstanceState.getBoolean("editable");
@ -150,7 +156,8 @@ public class MediaDetailFragment extends Fragment {
return view;
}
@Override public void onResume() {
@Override
public void onResume() {
super.onResume();
Media media = detailProvider.getMediaAtPosition(index);
if (media == null) {
@ -188,13 +195,13 @@ public class MediaDetailFragment extends Fragment {
@Override
protected void onPreExecute() {
extractor = new MediaDataExtractor(media.getFilename(), licenseList);
extractor = mediaDataExtractorProvider.get();
}
@Override
protected Boolean doInBackground(Void... voids) {
try {
extractor.fetch();
extractor.fetch(media.getFilename(), licenseList);
return Boolean.TRUE;
} catch (IOException e) {
Timber.d(e);
@ -232,13 +239,13 @@ public class MediaDetailFragment extends Fragment {
detailFetchTask.cancel(true);
detailFetchTask = null;
}
if (layoutListener != null) {
if (layoutListener != null && getView() != null) {
getView().getViewTreeObserver().removeGlobalOnLayoutListener(layoutListener); // old Android was on crack. CRACK IS WHACK
layoutListener = null;
}
if (scrollListener != null) {
if (scrollListener != null && getView() != null) {
getView().getViewTreeObserver().removeOnScrollChangedListener(scrollListener);
scrollListener = null;
scrollListener = null;
}
if (dataObserver != null) {
detailProvider.unregisterDataSetObserver(dataObserver);
@ -283,7 +290,7 @@ public class MediaDetailFragment extends Fragment {
private View buildCatLabel(final String catName, ViewGroup categoryContainer) {
final View item = LayoutInflater.from(getContext()).inflate(R.layout.detail_category_item, categoryContainer, false);
final CompatTextView textView = (CompatTextView)item.findViewById(R.id.mediaDetailCategoryItemText);
final CompatTextView textView = (CompatTextView) item.findViewById(R.id.mediaDetailCategoryItemText);
textView.setText(catName);
if (categoriesLoaded && categoriesPresent) {
@ -302,7 +309,7 @@ public class MediaDetailFragment extends Fragment {
// You must face the darkness alone
int scrollY = scrollView.getScrollY();
int scrollMax = getView().getHeight();
float scrollPercentage = (float)scrollY / (float)scrollMax;
float scrollPercentage = (float) scrollY / (float) scrollMax;
final float transparencyMax = 0.75f;
if (scrollPercentage > transparencyMax) {
scrollPercentage = transparencyMax;
@ -356,7 +363,8 @@ public class MediaDetailFragment extends Fragment {
}
private @Nullable String licenseLink(Media media) {
private @Nullable
String licenseLink(Media media) {
String licenseKey = media.getLicense();
if (licenseKey == null || licenseKey.equals("")) {
return null;
@ -377,7 +385,7 @@ public class MediaDetailFragment extends Fragment {
private void openMap(LatLng coordinates) {
//Open map app at given position
Uri gmmIntentUri = Uri.parse(
"geo:0,0?q=" + coordinates.getLatitude() + "," + coordinates.getLatitude());
"geo:0,0?q=" + coordinates.getLatitude() + "," + coordinates.getLongitude());
Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri);
if (mapIntent.resolveActivity(getActivity().getPackageManager()) != null) {

View file

@ -3,6 +3,7 @@ package fr.free.nrw.commons.media;
import android.annotation.SuppressLint;
import android.app.DownloadManager;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.DataSetObserver;
import android.net.Uri;
import android.os.Build;
@ -24,20 +25,31 @@ import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import fr.free.nrw.commons.CommonsApplication;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.mwapi.EventLog;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static android.content.Context.DOWNLOAD_SERVICE;
import static android.content.Intent.ACTION_VIEW;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static fr.free.nrw.commons.CommonsApplication.EVENT_SHARE_ATTEMPT;
public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPageChangeListener {
public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment implements ViewPager.OnPageChangeListener {
@Inject
MediaWikiApi mwApi;
@Inject
SessionManager sessionManager;
@Inject
@Named("default_preferences")
SharedPreferences prefs;
private ViewPager pager;
private Boolean editable;
@ -99,12 +111,7 @@ public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPa
Media m = provider.getMediaAtPosition(pager.getCurrentItem());
switch (item.getItemId()) {
case R.id.menu_share_current_image:
// Share - this is just logs it, intent set in onCreateOptionsMenu, around line 252
CommonsApplication app = (CommonsApplication) getActivity().getApplication();
EventLog.schema(EVENT_SHARE_ATTEMPT)
.param("username", app.getCurrentAccount().name)
.param("filename", m.getFilename())
.log();
// Share - intent set in onCreateOptionsMenu, around line 252
return true;
case R.id.menu_browser_current_image:
// View in browser
@ -141,8 +148,14 @@ public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPa
private void downloadMedia(Media m) {
String imageUrl = m.getImageUrl(),
fileName = m.getFilename();
if (imageUrl == null || fileName == null) {
return;
}
// Strip 'File:' from beginning of filename, we really shouldn't store it
fileName = fileName.replaceFirst("^File:", "");
Uri imageUri = Uri.parse(imageUrl);
DownloadManager.Request req = new DownloadManager.Request(imageUri);
@ -155,15 +168,19 @@ public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPa
req.allowScanningByMediaScanner();
req.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& !(ContextCompat.checkSelfPermission(getContext(),
READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
ContextCompat.checkSelfPermission(getContext(), READ_EXTERNAL_STORAGE)
!= PERMISSION_GRANTED
&& getView() != null) {
Snackbar.make(getView(), R.string.read_storage_permission_rationale,
Snackbar.LENGTH_INDEFINITE).setAction(R.string.ok,
view -> ActivityCompat.requestPermissions(getActivity(),
new String[]{READ_EXTERNAL_STORAGE}, 1)).show();
} else {
((DownloadManager) getActivity().getSystemService(DOWNLOAD_SERVICE)).enqueue(req);
DownloadManager systemService = (DownloadManager) getActivity().getSystemService(DOWNLOAD_SERVICE);
if (systemService != null) {
systemService.enqueue(req);
}
}
}

View file

@ -13,7 +13,7 @@ public class CategoryModifier extends PageModifier {
public CategoryModifier(String... categories) {
super(MODIFIER_NAME);
JSONArray categoriesArray = new JSONArray();
for(String category: categories) {
for (String category: categories) {
categoriesArray.put(category);
}
try {
@ -34,7 +34,7 @@ public class CategoryModifier extends PageModifier {
categories = params.optJSONArray(PARAM_CATEGORIES);
StringBuilder categoriesString = new StringBuilder();
for(int i=0; i < categories.length(); i++) {
for (int i = 0; i < categories.length(); i++) {
String category = categories.optString(i);
categoriesString.append("\n[[Category:").append(category).append("]]");
}

View file

@ -1,6 +1,5 @@
package fr.free.nrw.commons.modifications;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
@ -10,50 +9,51 @@ import android.net.Uri;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import fr.free.nrw.commons.CommonsApplication;
import javax.inject.Inject;
import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.di.CommonsDaggerContentProvider;
import timber.log.Timber;
public class ModificationsContentProvider extends ContentProvider{
import static fr.free.nrw.commons.modifications.ModifierSequenceDao.Table.TABLE_NAME;
public class ModificationsContentProvider extends CommonsDaggerContentProvider {
private static final int MODIFICATIONS = 1;
private static final int MODIFICATIONS_ID = 2;
public static final String AUTHORITY = "fr.free.nrw.commons.modifications.contentprovider";
private static final String BASE_PATH = "modifications";
public static final String MODIFICATIONS_AUTHORITY = "fr.free.nrw.commons.modifications.contentprovider";
public static final String BASE_PATH = "modifications";
public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY + "/" + BASE_PATH);
public static final Uri BASE_URI = Uri.parse("content://" + MODIFICATIONS_AUTHORITY + "/" + BASE_PATH);
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
uriMatcher.addURI(AUTHORITY, BASE_PATH, MODIFICATIONS);
uriMatcher.addURI(AUTHORITY, BASE_PATH + "/#", MODIFICATIONS_ID);
uriMatcher.addURI(MODIFICATIONS_AUTHORITY, BASE_PATH, MODIFICATIONS);
uriMatcher.addURI(MODIFICATIONS_AUTHORITY, BASE_PATH + "/#", MODIFICATIONS_ID);
}
public static Uri uriForId(int id) {
return Uri.parse(BASE_URI.toString() + "/" + id);
}
@Override
public boolean onCreate() {
return false;
}
@Inject DBOpenHelper dbOpenHelper;
@Override
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables(ModifierSequence.Table.TABLE_NAME);
queryBuilder.setTables(TABLE_NAME);
int uriType = uriMatcher.match(uri);
switch(uriType) {
switch (uriType) {
case MODIFICATIONS:
break;
default:
throw new IllegalArgumentException("Unknown URI" + uri);
}
SQLiteDatabase db = CommonsApplication.getInstance().getDBOpenHelper().getReadableDatabase();
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder);
cursor.setNotificationUri(getContext().getContentResolver(), uri);
@ -69,11 +69,11 @@ public class ModificationsContentProvider extends ContentProvider{
@Override
public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase();
long id = 0;
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
long id;
switch (uriType) {
case MODIFICATIONS:
id = sqlDB.insert(ModifierSequence.Table.TABLE_NAME, null, contentValues);
id = sqlDB.insert(TABLE_NAME, null, contentValues);
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
@ -85,11 +85,11 @@ public class ModificationsContentProvider extends ContentProvider{
@Override
public int delete(@NonNull Uri uri, String s, String[] strings) {
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase();
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
switch (uriType) {
case MODIFICATIONS_ID:
String id = uri.getLastPathSegment();
sqlDB.delete(ModifierSequence.Table.TABLE_NAME,
sqlDB.delete(TABLE_NAME,
"_id = ?",
new String[] { id }
);
@ -103,13 +103,13 @@ public class ModificationsContentProvider extends ContentProvider{
public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
Timber.d("Hello, bulk insert! (ModificationsContentProvider)");
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase();
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
sqlDB.beginTransaction();
switch (uriType) {
case MODIFICATIONS:
for(ContentValues value: values) {
for (ContentValues value: values) {
Timber.d("Inserting! %s", value);
sqlDB.insert(ModifierSequence.Table.TABLE_NAME, null, value);
sqlDB.insert(TABLE_NAME, null, value);
}
break;
default:
@ -131,11 +131,11 @@ public class ModificationsContentProvider extends ContentProvider{
In here, the only concat created argument is for id. It is cast to an int, and will error out otherwise.
*/
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase();
int rowsUpdated = 0;
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
int rowsUpdated;
switch (uriType) {
case MODIFICATIONS:
rowsUpdated = sqlDB.update(ModifierSequence.Table.TABLE_NAME,
rowsUpdated = sqlDB.update(TABLE_NAME,
contentValues,
selection,
selectionArgs);
@ -144,9 +144,9 @@ public class ModificationsContentProvider extends ContentProvider{
int id = Integer.valueOf(uri.getLastPathSegment());
if (TextUtils.isEmpty(selection)) {
rowsUpdated = sqlDB.update(ModifierSequence.Table.TABLE_NAME,
rowsUpdated = sqlDB.update(TABLE_NAME,
contentValues,
ModifierSequence.Table.COLUMN_ID + " = ?",
ModifierSequenceDao.Table.COLUMN_ID + " = ?",
new String[] { String.valueOf(id) } );
} else {
throw new IllegalArgumentException("Parameter `selection` should be empty when updating an ID");

View file

@ -1,9 +1,6 @@
package fr.free.nrw.commons.modifications;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.Context;
@ -14,15 +11,24 @@ import android.os.RemoteException;
import java.io.IOException;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Utils;
import javax.inject.Inject;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionDao;
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber;
public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
@Inject MediaWikiApi mwApi;
@Inject ContributionDao contributionDao;
@Inject ModifierSequenceDao modifierSequenceDao;
@Inject
SessionManager sessionManager;
public ModificationsSyncAdapter(Context context, boolean autoInitialize) {
super(context, autoInitialize);
}
@ -30,6 +36,11 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
@Override
public void onPerformSync(Account account, Bundle bundle, String s, ContentProviderClient contentProviderClient, SyncResult syncResult) {
// This code is fraught with possibilities of race conditions, but lalalalala I can't hear you!
ApplicationlessInjection
.getInstance(getContext()
.getApplicationContext())
.getCommonsApplicationComponent()
.inject(this);
Cursor allModifications;
try {
@ -44,27 +55,17 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
return;
}
String authCookie;
try {
authCookie = AccountManager.get(getContext()).blockingGetAuthToken(account, "", false);
} catch (OperationCanceledException | AuthenticatorException e) {
throw new RuntimeException(e);
} catch (IOException e) {
String authCookie = sessionManager.getAuthCookie();
if (isNullOrWhiteSpace(authCookie)) {
Timber.d("Could not authenticate :(");
return;
}
if (Utils.isNullOrWhiteSpace(authCookie)) {
Timber.d("Could not authenticate :(");
return;
}
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
api.setAuthCookie(authCookie);
mwApi.setAuthCookie(authCookie);
String editToken;
try {
editToken = api.getEditToken();
editToken = mwApi.getEditToken();
} catch (IOException e) {
Timber.d("Can not retreive edit token!");
return;
@ -76,28 +77,36 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
ContentProviderClient contributionsClient = null;
try {
contributionsClient = getContext().getContentResolver().acquireContentProviderClient(ContributionsContentProvider.AUTHORITY);
contributionsClient = getContext().getContentResolver().acquireContentProviderClient(ContributionsContentProvider.CONTRIBUTION_AUTHORITY);
while (!allModifications.isAfterLast()) {
ModifierSequence sequence = ModifierSequence.fromCursor(allModifications);
sequence.setContentProviderClient(contentProviderClient);
ModifierSequence sequence = modifierSequenceDao.fromCursor(allModifications);
Contribution contrib;
Cursor contributionCursor;
if (contributionsClient == null) {
Timber.e("ContributionsClient is null. This should not happen!");
return;
}
try {
contributionCursor = contributionsClient.query(sequence.getMediaUri(), null, null, null, null);
} catch (RemoteException e) {
throw new RuntimeException(e);
}
contributionCursor.moveToFirst();
contrib = Contribution.fromCursor(contributionCursor);
if (contrib.getState() == Contribution.STATE_COMPLETED) {
if (contributionCursor != null) {
contributionCursor.moveToFirst();
}
contrib = contributionDao.fromCursor(contributionCursor);
if (contrib != null && contrib.getState() == Contribution.STATE_COMPLETED) {
String pageContent;
try {
pageContent = api.revisionsByFilename(contrib.getFilename());
pageContent = mwApi.revisionsByFilename(contrib.getFilename());
} catch (IOException e) {
Timber.d("Network fuckup on modifications sync!");
Timber.d("Network messed up on modifications sync!");
continue;
}
@ -106,19 +115,19 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
String editResult;
try {
editResult = api.edit(editToken, processedPageContent, contrib.getFilename(), sequence.getEditSummary());
editResult = mwApi.edit(editToken, processedPageContent, contrib.getFilename(), sequence.getEditSummary());
} catch (IOException e) {
Timber.d("Network fuckup on modifications sync!");
Timber.d("Network messed up on modifications sync!");
continue;
}
Timber.d("Response is %s", editResult);
if (!editResult.equals("Success")) {
if (!"Success".equals(editResult)) {
// FIXME: Log this somewhere else
Timber.d("Non success result! %s", editResult);
} else {
sequence.delete();
modifierSequenceDao.delete(sequence);
}
}
allModifications.moveToNext();
@ -129,4 +138,8 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
}
}
}
private boolean isNullOrWhiteSpace(String value) {
return value == null || value.trim().isEmpty();
}
}

View file

@ -1,14 +1,8 @@
package fr.free.nrw.commons.modifications;
import android.content.ContentProviderClient;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.RemoteException;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
@ -17,22 +11,21 @@ public class ModifierSequence {
private Uri mediaUri;
private ArrayList<PageModifier> modifiers;
private Uri contentUri;
private ContentProviderClient client;
public ModifierSequence(Uri mediaUri) {
this.mediaUri = mediaUri;
modifiers = new ArrayList<>();
}
public ModifierSequence(Uri mediaUri, JSONObject data) {
ModifierSequence(Uri mediaUri, JSONObject data) {
this(mediaUri);
JSONArray modifiersJSON = data.optJSONArray("modifiers");
for (int i=0; i< modifiersJSON.length(); i++) {
for (int i = 0; i < modifiersJSON.length(); i++) {
modifiers.add(PageModifier.fromJSON(modifiersJSON.optJSONObject(i)));
}
}
public Uri getMediaUri() {
Uri getMediaUri() {
return mediaUri;
}
@ -40,113 +33,32 @@ public class ModifierSequence {
modifiers.add(modifier);
}
public String executeModifications(String pageName, String pageContents) {
String executeModifications(String pageName, String pageContents) {
for (PageModifier modifier: modifiers) {
pageContents = modifier.doModification(pageName, pageContents);
}
return pageContents;
}
public String getEditSummary() {
String getEditSummary() {
StringBuilder editSummary = new StringBuilder();
for(PageModifier modifier: modifiers) {
for (PageModifier modifier: modifiers) {
editSummary.append(modifier.getEditSumary()).append(" ");
}
editSummary.append("Via Commons Mobile App");
return editSummary.toString();
}
public JSONObject toJSON() {
JSONObject data = new JSONObject();
try {
JSONArray modifiersJSON = new JSONArray();
for (PageModifier modifier: modifiers) {
modifiersJSON.put(modifier.toJSON());
}
data.put("modifiers", modifiersJSON);
return data;
} catch (JSONException e) {
throw new RuntimeException(e);
}
ArrayList<PageModifier> getModifiers() {
return modifiers;
}
public ContentValues toContentValues() {
ContentValues cv = new ContentValues();
cv.put(Table.COLUMN_MEDIA_URI, mediaUri.toString());
cv.put(Table.COLUMN_DATA, toJSON().toString());
return cv;
Uri getContentUri() {
return contentUri;
}
public static ModifierSequence fromCursor(Cursor cursor) {
// Hardcoding column positions!
ModifierSequence ms = null;
try {
ms = new ModifierSequence(Uri.parse(cursor.getString(1)),
new JSONObject(cursor.getString(2)));
} catch (JSONException e) {
throw new RuntimeException(e);
}
ms.contentUri = ModificationsContentProvider.uriForId(cursor.getInt(0));
return ms;
void setContentUri(Uri contentUri) {
this.contentUri = contentUri;
}
public void save() {
try {
if(contentUri == null) {
contentUri = client.insert(ModificationsContentProvider.BASE_URI, this.toContentValues());
} else {
client.update(contentUri, toContentValues(), null, null);
}
} catch(RemoteException e) {
throw new RuntimeException(e);
}
}
public void delete() {
try {
client.delete(contentUri, null, null);
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
public void setContentProviderClient(ContentProviderClient client) {
this.client = client;
}
public static class Table {
public static final String TABLE_NAME = "modifications";
public static final String COLUMN_ID = "_id";
public static final String COLUMN_MEDIA_URI = "mediauri";
public static final String COLUMN_DATA = "data";
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
public static final String[] ALL_FIELDS = {
COLUMN_ID,
COLUMN_MEDIA_URI,
COLUMN_DATA
};
private static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
+ "_id INTEGER PRIMARY KEY,"
+ "mediauri STRING,"
+ "data STRING"
+ ");";
public static void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE_STATEMENT);
}
public static void onUpdate(SQLiteDatabase db, int from, int to) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
onCreate(db);
}
public static void onDelete(SQLiteDatabase db) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
onCreate(db);
}
}
}

View file

@ -0,0 +1,124 @@
package fr.free.nrw.commons.modifications;
import android.content.ContentProviderClient;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.RemoteException;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
public class ModifierSequenceDao {
private final Provider<ContentProviderClient> clientProvider;
@Inject
public ModifierSequenceDao(@Named("modification") Provider<ContentProviderClient> clientProvider) {
this.clientProvider = clientProvider;
}
public void save(ModifierSequence sequence) {
ContentProviderClient db = clientProvider.get();
try {
if (sequence.getContentUri() == null) {
sequence.setContentUri(db.insert(ModificationsContentProvider.BASE_URI, toContentValues(sequence)));
} else {
db.update(sequence.getContentUri(), toContentValues(sequence), null, null);
}
} catch (RemoteException e) {
throw new RuntimeException(e);
} finally {
db.release();
}
}
public void delete(ModifierSequence sequence) {
ContentProviderClient db = clientProvider.get();
try {
db.delete(sequence.getContentUri(), null, null);
} catch (RemoteException e) {
throw new RuntimeException(e);
} finally {
db.release();
}
}
ModifierSequence fromCursor(Cursor cursor) {
// Hardcoding column positions!
ModifierSequence ms;
try {
ms = new ModifierSequence(Uri.parse(cursor.getString(1)),
new JSONObject(cursor.getString(2)));
} catch (JSONException e) {
throw new RuntimeException(e);
}
ms.setContentUri( ModificationsContentProvider.uriForId(cursor.getInt(0)));
return ms;
}
private JSONObject toJSON(ModifierSequence sequence) {
JSONObject data = new JSONObject();
try {
JSONArray modifiersJSON = new JSONArray();
for (PageModifier modifier: sequence.getModifiers()) {
modifiersJSON.put(modifier.toJSON());
}
data.put("modifiers", modifiersJSON);
return data;
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
private ContentValues toContentValues(ModifierSequence sequence) {
ContentValues cv = new ContentValues();
cv.put(Table.COLUMN_MEDIA_URI, sequence.getMediaUri().toString());
cv.put(Table.COLUMN_DATA, toJSON(sequence).toString());
return cv;
}
public static class Table {
static final String TABLE_NAME = "modifications";
static final String COLUMN_ID = "_id";
static final String COLUMN_MEDIA_URI = "mediauri";
static final String COLUMN_DATA = "data";
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
public static final String[] ALL_FIELDS = {
COLUMN_ID,
COLUMN_MEDIA_URI,
COLUMN_DATA
};
static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME;
static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
+ "_id INTEGER PRIMARY KEY,"
+ "mediauri STRING,"
+ "data STRING"
+ ");";
public static void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE_STATEMENT);
}
public static void onUpdate(SQLiteDatabase db, int from, int to) {
db.execSQL(DROP_TABLE_STATEMENT);
onCreate(db);
}
public static void onDelete(SQLiteDatabase db) {
db.execSQL(DROP_TABLE_STATEMENT);
onCreate(db);
}
}
}

View file

@ -7,9 +7,9 @@ public abstract class PageModifier {
public static PageModifier fromJSON(JSONObject data) {
String name = data.optString("name");
if(name.equals(CategoryModifier.MODIFIER_NAME)) {
if (name.equals(CategoryModifier.MODIFIER_NAME)) {
return new CategoryModifier(data.optJSONObject("data"));
} else if(name.equals(TemplateRemoveModifier.MODIFIER_NAME)) {
} else if (name.equals(TemplateRemoveModifier.MODIFIER_NAME)) {
return new TemplateRemoveModifier(data.optJSONObject("data"));
}

View file

@ -41,18 +41,18 @@ public class TemplateRemoveModifier extends PageModifier {
Pattern templateStartPattern = Pattern.compile("\\{\\{" + templateNormalized, Pattern.CASE_INSENSITIVE);
Matcher matcher = templateStartPattern.matcher(pageContents);
while(matcher.find()) {
while (matcher.find()) {
int braceCount = 1;
int startIndex = matcher.start();
int curIndex = matcher.end();
Matcher openMatch = PATTERN_TEMPLATE_OPEN.matcher(pageContents);
Matcher closeMatch = PATTERN_TEMPLATE_CLOSE.matcher(pageContents);
while(curIndex < pageContents.length()) {
while (curIndex < pageContents.length()) {
boolean openFound = openMatch.find(curIndex);
boolean closeFound = closeMatch.find(curIndex);
if(openFound && (!closeFound || openMatch.start() < closeMatch.start())) {
if (openFound && (!closeFound || openMatch.start() < closeMatch.start())) {
braceCount++;
curIndex = openMatch.end();
} else if (closeFound) {
@ -71,8 +71,8 @@ public class TemplateRemoveModifier extends PageModifier {
}
// Strip trailing whitespace
while(curIndex < pageContents.length()) {
if(pageContents.charAt(curIndex) == ' ' || pageContents.charAt(curIndex) == '\n') {
while (curIndex < pageContents.length()) {
if (pageContents.charAt(curIndex) == ' ' || pageContents.charAt(curIndex) == '\n') {
curIndex++;
} else {
break;

View file

@ -1,5 +1,7 @@
package fr.free.nrw.commons.mwapi;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@ -21,10 +23,14 @@ import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.util.EntityUtils;
import org.mediawiki.api.ApiResult;
import org.mediawiki.api.MWApi;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
@ -34,12 +40,17 @@ import java.util.concurrent.Callable;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.PageTitle;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.notification.Notification;
import in.yuvi.http.fluent.Http;
import io.reactivex.Observable;
import io.reactivex.Single;
import timber.log.Timber;
import static fr.free.nrw.commons.notification.NotificationType.UNKNOWN;
import static fr.free.nrw.commons.notification.NotificationUtils.getNotificationFromApiResult;
import static fr.free.nrw.commons.notification.NotificationUtils.getNotificationType;
import static fr.free.nrw.commons.notification.NotificationUtils.isCommonsNotification;
/**
* @author Addshore
*/
@ -49,17 +60,27 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
private static final String THUMB_SIZE = "640";
private AbstractHttpClient httpClient;
private MWApi api;
private Context context;
private SharedPreferences sharedPreferences;
public ApacheHttpClientMediaWikiApi(String apiURL) {
public ApacheHttpClientMediaWikiApi(Context context, String apiURL, SharedPreferences sharedPreferences) {
this.context = context;
BasicHttpParams params = new BasicHttpParams();
SchemeRegistry schemeRegistry = new SchemeRegistry();
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
final SSLSocketFactory sslSocketFactory = SSLSocketFactory.getSocketFactory();
schemeRegistry.register(new Scheme("https", sslSocketFactory, 443));
ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry);
params.setParameter(CoreProtocolPNames.USER_AGENT, "Commons/" + BuildConfig.VERSION_NAME + " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE);
params.setParameter(CoreProtocolPNames.USER_AGENT, getUserAgent());
httpClient = new DefaultHttpClient(cm, params);
api = new MWApi(apiURL, httpClient);
this.sharedPreferences = sharedPreferences;
}
@Override
@NonNull
public String getUserAgent() {
return "Commons/" + BuildConfig.VERSION_NAME + " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE;
}
@VisibleForTesting
@ -74,11 +95,13 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
* @throws IOException On api request IO issue
*/
public String login(String username, String password) throws IOException {
String loginToken = getLoginToken();
Timber.d("Login token is %s", loginToken);
return getErrorCodeToReturn(api.action("clientlogin")
.param("rememberMe", "1")
.param("username", username)
.param("password", password)
.param("logintoken", getLoginToken())
.param("logintoken", loginToken)
.param("loginreturnurl", "https://commons.wikimedia.org")
.post());
}
@ -91,12 +114,14 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
* @throws IOException On api request IO issue
*/
public String login(String username, String password, String twoFactorCode) throws IOException {
String loginToken = getLoginToken();
Timber.d("Login token is %s", loginToken);
return getErrorCodeToReturn(api.action("clientlogin")
.param("rememberMe", "1")
.param("rememberMe", "true")
.param("username", username)
.param("password", password)
.param("logintoken", getLoginToken())
.param("logincontinue", "1")
.param("logintoken", loginToken)
.param("logincontinue", "true")
.param("OATHToken", twoFactorCode)
.post());
}
@ -121,14 +146,17 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
String status = loginApiResult.getString("/api/clientlogin/@status");
if (status.equals("PASS")) {
api.isLoggedIn = true;
setAuthCookieOnLogin(true);
return status;
} else if (status.equals("FAIL")) {
setAuthCookieOnLogin(false);
return loginApiResult.getString("/api/clientlogin/@messagecode");
} else if (
status.equals("UI")
&& loginApiResult.getString("/api/clientlogin/requests/_v/@id").equals("TOTPAuthenticationRequest")
&& loginApiResult.getString("/api/clientlogin/requests/_v/@provider").equals("Two-factor authentication (OATH).")
) {
setAuthCookieOnLogin(false);
return "2FA";
}
@ -136,6 +164,18 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
return "genericerror-" + status;
}
private void setAuthCookieOnLogin(boolean isLoggedIn) {
SharedPreferences.Editor editor = sharedPreferences.edit();
if (isLoggedIn) {
editor.putBoolean("isUserLoggedIn", true);
editor.putString("getAuthCookie", api.getAuthCookie());
} else {
editor.putBoolean("isUserLoggedIn", false);
editor.remove("getAuthCookie");
}
editor.apply();
}
@Override
public String getAuthCookie() {
return api.getAuthCookie();
@ -335,7 +375,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
logEvents.add(new LogEventResult.LogEvent(
image.getString("@pageid"),
image.getString("@title"),
Utils.parseMWDate(image.getString("@timestamp")))
parseMWDate(image.getString("@timestamp")))
);
}
return logEvents;
@ -352,6 +392,42 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
.getString("/api/query/pages/page/revisions/rev");
}
@Override
@NonNull
public List<Notification> getNotifications() {
ApiResult notificationNode = null;
try {
notificationNode = api.action("query")
.param("notprop", "list")
.param("format", "xml")
.param("meta", "notifications")
.param("notfilter", "!read")
.get()
.getNode("/api/query/notifications/list");
} catch (IOException e) {
Timber.e("Failed to obtain searchCategories", e);
}
if (notificationNode == null) {
return new ArrayList<>();
}
List<Notification> notifications = new ArrayList<>();
NodeList childNodes = notificationNode.getDocument().getChildNodes();
for (int i = 0; i < childNodes.getLength(); i++) {
Node node = childNodes.item(i);
if (isCommonsNotification(node)
&& !getNotificationType(node).equals(UNKNOWN)) {
notifications.add(getNotificationFromApiResult(context, node));
}
}
return notifications;
}
@Override
public boolean existingFile(String fileSha1) throws IOException {
return api.action("query")
@ -387,17 +463,22 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
@Override
@NonNull
public UploadResult uploadFile(String filename, InputStream file, long dataLength, String pageContents, String editSummary, final ProgressListener progressListener) throws IOException {
public UploadResult uploadFile(String filename,
@NonNull InputStream file,
long dataLength,
String pageContents,
String editSummary,
final ProgressListener progressListener) throws IOException {
ApiResult result = api.upload(filename, file, dataLength, pageContents, editSummary, progressListener::onProgress);
Log.e("WTF", "Result: " +result.toString());
Log.e("WTF", "Result: " + result.toString());
String resultStatus = result.getString("/api/upload/@result");
if (!resultStatus.equals("Success")) {
String errorCode = result.getString("/api/error/@code");
return new UploadResult(resultStatus, errorCode);
} else {
Date dateUploaded = Utils.parseMWDate(result.getString("/api/upload/imageinfo/@timestamp"));
Date dateUploaded = parseMWDate(result.getString("/api/upload/imageinfo/@timestamp"));
String canonicalFilename = "File:" + result.getString("/api/upload/@filename").replace("_", " "); // Title vs Filename
String imageUrl = result.getString("/api/upload/imageinfo/@url");
return new UploadResult(resultStatus, dateUploaded, canonicalFilename, imageUrl);
@ -423,4 +504,13 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
return Integer.parseInt(uploadCount);
});
}
private Date parseMWDate(String mwDate) {
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH); // Assuming MW always gives me UTC
try {
return isoFormat.parse(mwDate);
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -1,5 +1,6 @@
package fr.free.nrw.commons.mwapi;
import android.content.SharedPreferences;
import android.os.Build;
import fr.free.nrw.commons.Utils;
@ -15,14 +16,14 @@ public class EventLog {
}
}
private static LogBuilder schema(String schema, long revision) {
return new LogBuilder(schema, revision);
private static LogBuilder schema(String schema, long revision, MediaWikiApi mwApi, SharedPreferences prefs) {
return new LogBuilder(schema, revision, mwApi, prefs);
}
public static LogBuilder schema(Object[] scid) {
public static LogBuilder schema(Object[] scid, MediaWikiApi mwApi, SharedPreferences prefs) {
if (scid.length != 2) {
throw new IllegalArgumentException("Needs an object array with schema as first param and revision as second");
}
return schema((String) scid[0], (Long) scid[1]);
return schema((String) scid[0], (Long) scid[1], mwApi, prefs);
}
}

View file

@ -3,7 +3,6 @@ package fr.free.nrw.commons.mwapi;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Build;
import android.preference.PreferenceManager;
import org.json.JSONException;
import org.json.JSONObject;
@ -12,21 +11,39 @@ import java.net.MalformedURLException;
import java.net.URL;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.settings.Prefs;
@SuppressWarnings("WeakerAccess")
public class LogBuilder {
private JSONObject data;
private long rev;
private String schema;
private final MediaWikiApi mwApi;
private final JSONObject data;
private final long rev;
private final String schema;
private final SharedPreferences prefs;
LogBuilder(String schema, long revision) {
data = new JSONObject();
/**
* Main constructor of LogBuilder
*
* @param schema Log schema
* @param revision Log revision
* @param mwApi Wiki media API instance
* @param prefs Instance of SharedPreferences
*/
LogBuilder(String schema, long revision, MediaWikiApi mwApi, SharedPreferences prefs) {
this.prefs = prefs;
this.data = new JSONObject();
this.schema = schema;
this.rev = revision;
this.mwApi = mwApi;
}
/**
* Adds data to preferences
* @param key Log key
* @param value Log object value
* @return LogBuilder
*/
public LogBuilder param(String key, Object value) {
try {
data.put(key, value);
@ -36,6 +53,10 @@ public class LogBuilder {
return this;
}
/**
* Encodes JSON object to URL
* @return URL to JSON object
*/
URL toUrl() {
JSONObject fullData = new JSONObject();
try {
@ -56,11 +77,10 @@ public class LogBuilder {
// Use *only* for tracking the user preference change for EventLogging
// Attempting to use anywhere else will cause kitten explosions
public void log(boolean force) {
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(CommonsApplication.getInstance());
if (!settings.getBoolean(Prefs.TRACKING_ENABLED, true) && !force) {
if (!prefs.getBoolean(Prefs.TRACKING_ENABLED, true) && !force) {
return; // User has disabled tracking
}
LogTask logTask = new LogTask();
LogTask logTask = new LogTask(mwApi);
logTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, this);
}

View file

@ -2,11 +2,26 @@ package fr.free.nrw.commons.mwapi;
import android.os.AsyncTask;
import fr.free.nrw.commons.CommonsApplication;
class LogTask extends AsyncTask<LogBuilder, Void, Boolean> {
private final MediaWikiApi mwApi;
/**
* Main constructor of LogTask
*
* @param mwApi Media wiki API instance
*/
public LogTask(MediaWikiApi mwApi) {
this.mwApi = mwApi;
}
/**
* Logs events in background
* @param logBuilders LogBuilder instance
* @return Background success state ( TRUE or FALSE )
*/
@Override
protected Boolean doInBackground(LogBuilder... logBuilders) {
return CommonsApplication.getInstance().getMWApi().logEvents(logBuilders);
return mwApi.logEvents(logBuilders);
}
}

View file

@ -4,15 +4,29 @@ public class MediaResult {
private final String wikiSource;
private final String parseTreeXmlSource;
/**
* Full-fledged constructor of MediaResult
*
* @param wikiSource Media wiki source
* @param parseTreeXmlSource Media tree parsed in XML
*/
MediaResult(String wikiSource, String parseTreeXmlSource) {
this.wikiSource = wikiSource;
this.parseTreeXmlSource = parseTreeXmlSource;
}
/**
* Gets wiki source
* @return Wiki source
*/
public String getWikiSource() {
return wikiSource;
}
/**
* Gets tree parsed in XML
* @return XML parsed tree
*/
public String getParseTreeXmlSource() {
return parseTreeXmlSource;
}

View file

@ -5,11 +5,15 @@ import android.support.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import fr.free.nrw.commons.notification.Notification;
import io.reactivex.Observable;
import io.reactivex.Single;
public interface MediaWikiApi {
String getUserAgent();
String getAuthCookie();
void setAuthCookie(String authCookie);
@ -43,6 +47,9 @@ public interface MediaWikiApi {
@NonNull
Observable<String> allCategories(String filter, int searchCatsLimit);
@NonNull
List<Notification> getNotifications() throws IOException;
@NonNull
Observable<String> searchTitles(String title, int searchCatsLimit);
@ -51,6 +58,8 @@ public interface MediaWikiApi {
boolean existingFile(String fileSha1) throws IOException;
@NonNull
LogEventResult logEvents(String user, String lastModified, String queryContinue, int limit) throws IOException;

View file

@ -9,11 +9,24 @@ public class UploadResult {
private String imageUrl;
private String canonicalFilename;
/**
* Minimal constructor
*
* @param resultStatus Upload result status
* @param errorCode Upload error code
*/
UploadResult(String resultStatus, String errorCode) {
this.resultStatus = resultStatus;
this.errorCode = errorCode;
}
/**
* Full-fledged constructor
* @param resultStatus Upload result status
* @param dateUploaded Uploaded date
* @param canonicalFilename Uploaded file name
* @param imageUrl Uploaded image file name
*/
UploadResult(String resultStatus, Date dateUploaded, String canonicalFilename, String imageUrl) {
this.resultStatus = resultStatus;
this.dateUploaded = dateUploaded;
@ -21,22 +34,42 @@ public class UploadResult {
this.imageUrl = imageUrl;
}
/**
* Gets uploaded date
* @return Upload date
*/
public Date getDateUploaded() {
return dateUploaded;
}
/**
* Gets image url
* @return Uploaded image url
*/
public String getImageUrl() {
return imageUrl;
}
/**
* Gets canonical file name
* @return Uploaded file name
*/
public String getCanonicalFilename() {
return canonicalFilename;
}
/**
* Gets upload error code
* @return Error code
*/
public String getErrorCode() {
return errorCode;
}
/**
* Gets upload result status
* @return Upload result status
*/
public String getResultStatus() {
return resultStatus;
}

View file

@ -1,21 +1,15 @@
package fr.free.nrw.commons.nearby;
import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.location.LocationManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
import android.view.Menu;
import android.view.MenuInflater;
@ -29,30 +23,43 @@ import com.google.gson.GsonBuilder;
import java.util.List;
import javax.inject.Inject;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.location.LocationUpdateListener;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.utils.UriSerializer;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
public class NearbyActivity extends NavigationBaseActivity {
public class NearbyActivity extends NavigationBaseActivity implements LocationUpdateListener {
@BindView(R.id.progressBar)
ProgressBar progressBar;
private static final int LOCATION_REQUEST = 1;
private static final String MAP_LAST_USED_PREFERENCE = "mapLastUsed";
private LocationServiceManager locationManager;
@BindView(R.id.progressBar)
ProgressBar progressBar;
@Inject
LocationServiceManager locationManager;
@Inject
NearbyController nearbyController;
private LatLng curLatLang;
private Bundle bundle;
private NearbyAsyncTask nearbyAsyncTask;
private SharedPreferences sharedPreferences;
private NearbyActivityMode viewMode;
private Disposable placesDisposable;
private boolean lockNearbyView; //Determines if the nearby places needs to be refreshed
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -60,7 +67,6 @@ public class NearbyActivity extends NavigationBaseActivity {
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
setContentView(R.layout.activity_nearby);
ButterKnife.bind(this);
checkLocationPermission();
bundle = new Bundle();
initDrawer();
initViewState();
@ -92,7 +98,8 @@ public class NearbyActivity extends NavigationBaseActivity {
// Handle item selection
switch (item.getItemId()) {
case R.id.action_refresh:
refreshView();
lockNearbyView(false);
refreshView(true);
return true;
case R.id.action_toggle_view:
viewMode = viewMode.toggle();
@ -104,60 +111,9 @@ public class NearbyActivity extends NavigationBaseActivity {
}
}
private void startLookingForNearby() {
locationManager = new LocationServiceManager(this);
locationManager.registerLocationManager();
curLatLang = locationManager.getLatestLocation();
nearbyAsyncTask = new NearbyAsyncTask(this);
nearbyAsyncTask.execute();
}
private void checkLocationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
startLookingForNearby();
} else {
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
// Should we show an explanation?
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.ACCESS_FINE_LOCATION)) {
// Show an explanation to the user *asynchronously* -- don't block
// this thread waiting for the user's response! After the user
// sees the explanation, try again to request the permission.
new AlertDialog.Builder(this)
.setMessage(getString(R.string.location_permission_rationale))
.setPositiveButton("OK", (dialog, which) -> {
ActivityCompat.requestPermissions(NearbyActivity.this,
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
LOCATION_REQUEST);
dialog.dismiss();
})
.setNegativeButton("Cancel", null)
.create()
.show();
} else {
// No explanation needed, we can request the permission.
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
LOCATION_REQUEST);
// MY_PERMISSIONS_REQUEST_READ_CONTACTS is an
// app-defined int constant. The callback method gets the
// result of the request.
}
}
}
} else {
startLookingForNearby();
private void requestLocationPermissions() {
if (!isFinishing()) {
locationManager.requestPermissions(this);
}
}
@ -166,16 +122,10 @@ public class NearbyActivity extends NavigationBaseActivity {
switch (requestCode) {
case LOCATION_REQUEST: {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startLookingForNearby();
refreshView(false);
} else {
//If permission not granted, go to page that says Nearby Places cannot be displayed
if (nearbyAsyncTask != null) {
nearbyAsyncTask.cancel(true);
}
if (progressBar != null) {
progressBar.setVisibility(View.GONE);
}
hideProgressBar();
showLocationPermissionDeniedErrorDialog();
}
}
@ -188,7 +138,7 @@ public class NearbyActivity extends NavigationBaseActivity {
.setCancelable(false)
.setPositiveButton(R.string.give_permission, (dialog, which) -> {
//will ask for the location permission again
checkLocationPermission();
checkGps();
})
.setNegativeButton(R.string.cancel, (dialog, which) -> {
//dismiss dialog and finish activity
@ -200,8 +150,7 @@ public class NearbyActivity extends NavigationBaseActivity {
}
private void checkGps() {
LocationManager manager = (LocationManager) getSystemService(LOCATION_SERVICE);
if (!manager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
if (!locationManager.isProviderEnabled()) {
Timber.d("GPS is not enabled");
new AlertDialog.Builder(this)
.setMessage(R.string.gps_disabled)
@ -213,11 +162,48 @@ public class NearbyActivity extends NavigationBaseActivity {
Timber.d("Loaded settings page");
startActivityForResult(callGPSSettingIntent, 1);
})
.setNegativeButton(R.string.menu_cancel_upload, (dialog, id) -> dialog.cancel())
.setNegativeButton(R.string.menu_cancel_upload, (dialog, id) -> {
showLocationPermissionDeniedErrorDialog();
dialog.cancel();
})
.create()
.show();
} else {
Timber.d("GPS is enabled");
checkLocationPermission();
}
}
private void checkLocationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (locationManager.isLocationPermissionGranted()) {
refreshView(false);
} else {
// Should we show an explanation?
if (locationManager.isPermissionExplanationRequired(this)) {
// Show an explanation to the user *asynchronously* -- don't block
// this thread waiting for the user's response! After the user
// sees the explanation, try again to request the permission.
new AlertDialog.Builder(this)
.setMessage(getString(R.string.location_permission_rationale_nearby))
.setPositiveButton("OK", (dialog, which) -> {
requestLocationPermissions();
dialog.dismiss();
})
.setNegativeButton("Cancel", (dialog, id) -> {
showLocationPermissionDeniedErrorDialog();
dialog.cancel();
})
.create()
.show();
} else {
// No explanation needed, we can request the permission.
requestLocationPermissions();
}
}
} else {
refreshView(false);
}
}
@ -226,104 +212,122 @@ public class NearbyActivity extends NavigationBaseActivity {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 1) {
Timber.d("User is back from Settings page");
refreshView();
refreshView(false);
}
}
private void toggleView() {
if (nearbyAsyncTask != null) {
if (nearbyAsyncTask.getStatus() == AsyncTask.Status.FINISHED) {
if (viewMode.isMap()) {
setMapFragment();
} else {
setListFragment();
}
}
sharedPreferences.edit().putBoolean(MAP_LAST_USED_PREFERENCE, viewMode.isMap()).apply();
if (viewMode.isMap()) {
setMapFragment();
} else {
setListFragment();
}
sharedPreferences.edit().putBoolean(MAP_LAST_USED_PREFERENCE, viewMode.isMap()).apply();
}
@Override
protected void onStart() {
super.onStart();
locationManager.addLocationListener(this);
}
@Override
protected void onStop() {
super.onStop();
locationManager.removeLocationListener(this);
locationManager.unregisterLocationManager();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (placesDisposable != null) {
placesDisposable.dispose();
}
}
@Override
protected void onResume() {
super.onResume();
lockNearbyView = false;
checkGps();
}
@Override
protected void onPause() {
super.onPause();
if (nearbyAsyncTask != null) {
nearbyAsyncTask.cancel(true);
/**
* This method should be the single point to load/refresh nearby places
*
* @param isHardRefresh Should display a toast if the location hasn't changed
*/
private void refreshView(boolean isHardRefresh) {
if (lockNearbyView) {
return;
}
locationManager.registerLocationManager();
LatLng lastLocation = locationManager.getLastLocation();
if (curLatLang != null && curLatLang.equals(lastLocation)) { //refresh view only if location has changed
if (isHardRefresh) {
ViewUtil.showLongToast(this, R.string.nearby_location_has_not_changed);
}
return;
}
curLatLang = lastLocation;
if (curLatLang == null) {
Timber.d("Skipping update of nearby places as location is unavailable");
return;
}
progressBar.setVisibility(View.VISIBLE);
placesDisposable = Observable.fromCallable(() -> nearbyController
.loadAttractionsFromLocation(curLatLang, this))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::populatePlaces);
}
private void refreshView() {
nearbyAsyncTask = new NearbyAsyncTask(this);
nearbyAsyncTask.execute();
private void populatePlaces(List<Place> placeList) {
Gson gson = new GsonBuilder()
.registerTypeAdapter(Uri.class, new UriSerializer())
.create();
String gsonPlaceList = gson.toJson(placeList);
String gsonCurLatLng = gson.toJson(curLatLang);
if (placeList.size() == 0) {
int duration = Toast.LENGTH_SHORT;
Toast toast = Toast.makeText(this, R.string.no_nearby, duration);
toast.show();
}
bundle.clear();
bundle.putString("PlaceList", gsonPlaceList);
bundle.putString("CurLatLng", gsonCurLatLng);
lockNearbyView(true);
// Begin the transaction
if (viewMode.isMap()) {
setMapFragment();
} else {
setListFragment();
}
hideProgressBar();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (locationManager != null) {
private void lockNearbyView(boolean lock) {
if (lock) {
lockNearbyView = true;
locationManager.unregisterLocationManager();
locationManager.removeLocationListener(this);
} else {
lockNearbyView = false;
locationManager.registerLocationManager();
locationManager.addLocationListener(this);
}
}
private class NearbyAsyncTask extends AsyncTask<Void, Integer, List<Place>> {
private final Context mContext;
private NearbyAsyncTask(Context context) {
mContext = context;
}
@Override
protected void onProgressUpdate(Integer... values) {
super.onProgressUpdate(values);
}
@Override
protected List<Place> doInBackground(Void... params) {
return NearbyController
.loadAttractionsFromLocation(curLatLang, CommonsApplication.getInstance()
);
}
@Override
protected void onPostExecute(List<Place> placeList) {
super.onPostExecute(placeList);
if (isCancelled()) {
return;
}
Gson gson = new GsonBuilder()
.registerTypeAdapter(Uri.class, new UriSerializer())
.create();
String gsonPlaceList = gson.toJson(placeList);
String gsonCurLatLng = gson.toJson(curLatLang);
if (placeList.size() == 0) {
int duration = Toast.LENGTH_SHORT;
Toast toast = Toast.makeText(mContext, R.string.no_nearby, duration);
toast.show();
}
bundle.clear();
bundle.putString("PlaceList", gsonPlaceList);
bundle.putString("CurLatLng", gsonCurLatLng);
// Begin the transaction
if (viewMode.isMap()) {
setMapFragment();
} else {
setListFragment();
}
if (progressBar != null) {
progressBar.setVisibility(View.GONE);
}
private void hideProgressBar() {
if (progressBar != null) {
progressBar.setVisibility(View.GONE);
}
}
@ -334,7 +338,7 @@ public class NearbyActivity extends NavigationBaseActivity {
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
Fragment fragment = new NearbyMapFragment();
fragment.setArguments(bundle);
fragmentTransaction.replace(R.id.container, fragment);
fragmentTransaction.replace(R.id.container, fragment, fragment.getClass().getSimpleName());
fragmentTransaction.commitAllowingStateLoss();
}
@ -345,12 +349,12 @@ public class NearbyActivity extends NavigationBaseActivity {
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
Fragment fragment = new NearbyListFragment();
fragment.setArguments(bundle);
fragmentTransaction.replace(R.id.container, fragment);
fragmentTransaction.replace(R.id.container, fragment, fragment.getClass().getSimpleName());
fragmentTransaction.commitAllowingStateLoss();
}
public static void startYourself(Context context) {
Intent settingsIntent = new Intent(context, NearbyActivity.class);
context.startActivity(settingsIntent);
@Override
public void onLocationChanged(LatLng latLng) {
refreshView(false);
}
}

View file

@ -3,7 +3,6 @@ package fr.free.nrw.commons.nearby;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.preference.PreferenceManager;
import android.support.graphics.drawable.VectorDrawableCompat;
import com.mapbox.mapboxsdk.annotations.IconFactory;
@ -15,7 +14,9 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import fr.free.nrw.commons.CommonsApplication;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.utils.UiUtils;
@ -24,45 +25,51 @@ import timber.log.Timber;
import static fr.free.nrw.commons.utils.LengthUtils.computeDistanceBetween;
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
public class NearbyController {
private static final int MAX_RESULTS = 1000;
private final NearbyPlaces nearbyPlaces;
private final SharedPreferences prefs;
@Inject
public NearbyController(NearbyPlaces nearbyPlaces,
@Named("default_preferences") SharedPreferences prefs) {
this.nearbyPlaces = nearbyPlaces;
this.prefs = prefs;
}
/**
* Prepares Place list to make their distance information update later.
*
* @param curLatLng current location for user
* @param context context
* @param context context
* @return Place list without distance information
*/
public static List<Place> loadAttractionsFromLocation(LatLng curLatLng, Context context) {
public List<Place> loadAttractionsFromLocation(LatLng curLatLng, Context context) {
Timber.d("Loading attractions near %s", curLatLng);
if (curLatLng == null) {
return Collections.emptyList();
}
NearbyPlaces nearbyPlaces = CommonsApplication.getInstance().getNearbyPlaces();
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
List<Place> places = prefs.getBoolean("useWikidata", true)
? nearbyPlaces.getFromWikidataQuery(curLatLng, Locale.getDefault().getLanguage())
: nearbyPlaces.getFromWikiNeedsPictures();
if (curLatLng != null) {
Timber.d("Sorting places by distance...");
final Map<Place, Double> distances = new HashMap<>();
for (Place place: places) {
distances.put(place, computeDistanceBetween(place.location, curLatLng));
}
Collections.sort(places,
(lhs, rhs) -> {
double lhsDistance = distances.get(lhs);
double rhsDistance = distances.get(rhs);
return (int) (lhsDistance - rhsDistance);
}
);
Timber.d("Sorting places by distance...");
final Map<Place, Double> distances = new HashMap<>();
for (Place place : places) {
distances.put(place, computeDistanceBetween(place.location, curLatLng));
}
Collections.sort(places,
(lhs, rhs) -> {
double lhsDistance = distances.get(lhs);
double rhsDistance = distances.get(rhs);
return (int) (lhsDistance - rhsDistance);
}
);
return places;
}
/**
* Loads attractions from location for list view, we need to return Place data type.
*
* @param curLatLng users current location
* @param placeList list of nearby places in Place data type
* @return Place list that holds nearby places
@ -71,7 +78,7 @@ public class NearbyController {
LatLng curLatLng,
List<Place> placeList) {
placeList = placeList.subList(0, Math.min(placeList.size(), MAX_RESULTS));
for (Place place: placeList) {
for (Place place : placeList) {
String distance = formatDistanceBetween(curLatLng, place.location);
place.setDistance(distance);
}
@ -79,7 +86,8 @@ public class NearbyController {
}
/**
*Loads attractions from location for map view, we need to return BaseMarkerOption data type.
* Loads attractions from location for map view, we need to return BaseMarkerOption data type.
*
* @param curLatLng users current location
* @param placeList list of nearby places in Place data type
* @return BaseMarkerOptions list that holds nearby places
@ -96,26 +104,28 @@ public class NearbyController {
placeList = placeList.subList(0, Math.min(placeList.size(), MAX_RESULTS));
Bitmap icon = UiUtils.getBitmap(
VectorDrawableCompat.create(
context.getResources(), R.drawable.ic_custom_map_marker, context.getTheme()
));
VectorDrawableCompat vectorDrawable = VectorDrawableCompat.create(
context.getResources(), R.drawable.ic_custom_map_marker, context.getTheme()
);
if (vectorDrawable != null) {
Bitmap icon = UiUtils.getBitmap(vectorDrawable);
for (Place place: placeList) {
String distance = formatDistanceBetween(curLatLng, place.location);
place.setDistance(distance);
for (Place place : placeList) {
String distance = formatDistanceBetween(curLatLng, place.location);
place.setDistance(distance);
NearbyBaseMarker nearbyBaseMarker = new NearbyBaseMarker();
nearbyBaseMarker.title(place.name);
nearbyBaseMarker.position(
new com.mapbox.mapboxsdk.geometry.LatLng(
place.location.getLatitude(),
place.location.getLongitude()));
nearbyBaseMarker.place(place);
nearbyBaseMarker.icon(IconFactory.getInstance(context)
.fromBitmap(icon));
NearbyBaseMarker nearbyBaseMarker = new NearbyBaseMarker();
nearbyBaseMarker.title(place.name);
nearbyBaseMarker.position(
new com.mapbox.mapboxsdk.geometry.LatLng(
place.location.getLatitude(),
place.location.getLongitude()));
nearbyBaseMarker.place(place);
nearbyBaseMarker.icon(IconFactory.getInstance(context)
.fromBitmap(icon));
baseMarkerOptions.add(nearbyBaseMarker);
baseMarkerOptions.add(nearbyBaseMarker);
}
}
return baseMarkerOptions;
}

View file

@ -109,7 +109,7 @@ public class NearbyInfoDialog extends OverlayDialog {
NearbyInfoDialog mDialog = new NearbyInfoDialog();
Bundle bundle = new Bundle();
bundle.putString(ARG_TITLE, place.name);
bundle.putString(ARG_DESC, place.getDescription().getText());
bundle.putString(ARG_DESC, place.getLongDescription());
bundle.putDouble(ARG_LATITUDE, place.location.getLatitude());
bundle.putDouble(ARG_LONGITUDE, place.location.getLongitude());
bundle.putParcelable(ARG_SITE_LINK, place.siteLinks);

View file

@ -1,5 +1,6 @@
package fr.free.nrw.commons.nearby;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.Fragment;
@ -17,6 +18,7 @@ import java.lang.reflect.Type;
import java.util.Collections;
import java.util.List;
import dagger.android.support.AndroidSupportInjection;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.utils.UriDeserializer;
@ -40,6 +42,12 @@ public class NearbyListFragment extends Fragment {
setRetainInstance(true);
}
@Override
public void onAttach(Context context) {
AndroidSupportInjection.inject(this);
super.onAttach(context);
}
@Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container,
@ -60,7 +68,7 @@ public class NearbyListFragment extends Fragment {
Bundle bundle = this.getArguments();
if (bundle != null) {
String gsonPlaceList = bundle.getString("PlaceList");
String gsonPlaceList = bundle.getString("PlaceList", "[]");
placeList = gson.fromJson(gsonPlaceList, LIST_TYPE);
String gsonLatLng = bundle.getString("CurLatLng");

View file

@ -3,7 +3,6 @@ package fr.free.nrw.commons.nearby;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -77,6 +76,8 @@ public class NearbyMapFragment extends android.support.v4.app.Fragment {
private void setupMapView(Bundle savedInstanceState) {
MapboxMapOptions options = new MapboxMapOptions()
.styleUrl(Style.OUTDOORS)
.logoEnabled(false)
.attributionEnabled(false)
.camera(new CameraPosition.Builder()
.target(new LatLng(curLatLng.getLatitude(), curLatLng.getLongitude()))
.zoom(11)
@ -99,11 +100,8 @@ public class NearbyMapFragment extends android.support.v4.app.Fragment {
addCurrentLocationMarker(mapboxMap);
});
if (PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean("theme",false)) {
mapView.setStyleUrl(getResources().getString(R.string.map_theme_dark));
} else {
mapView.setStyleUrl(getResources().getString(R.string.map_theme_light));
}
mapView.setStyleUrl("asset://mapstyle.json");
}
/**

View file

@ -34,7 +34,7 @@ public class NearbyPlaces {
public NearbyPlaces() {
try {
wikidataQuery = FileUtils.readFromResource("/assets/queries/nearby_query.rq");
wikidataQuery = FileUtils.readFromResource("/queries/nearby_query.rq");
Timber.v(wikidataQuery);
} catch (IOException e) {
throw new RuntimeException(e);
@ -46,7 +46,7 @@ public class NearbyPlaces {
try {
// increase the radius gradually to find a satisfactory number of nearby places
while (radius < MAX_RADIUS) {
while (radius <= MAX_RADIUS) {
places = getFromWikidataQuery(curLatLng, lang, radius);
Timber.d("%d results at radius: %f", places.size(), radius);
if (places.size() >= MIN_RESULTS) {
@ -62,6 +62,11 @@ public class NearbyPlaces {
Timber.d("back to initial radius: %f", radius);
radius = INITIAL_RADIUS;
}
// make sure we will be able to send at least one request next time
if (radius > MAX_RADIUS) {
radius = MAX_RADIUS;
}
return places;
}
@ -121,7 +126,7 @@ public class NearbyPlaces {
places.add(new Place(
name,
Place.Description.fromText(type), // list
Place.Label.fromText(type), // list
type, // details
Uri.parse(icon),
new LatLng(latitude, longitude, 0),
@ -183,7 +188,7 @@ public class NearbyPlaces {
places.add(new Place(
name,
Place.Description.fromText(type), // list
Place.Label.fromText(type), // list
type, // details
null,
new LatLng(latitude, longitude, 0),

View file

@ -1,5 +1,6 @@
package fr.free.nrw.commons.nearby;
import android.content.Context;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
@ -7,6 +8,7 @@ import android.view.View;
import android.view.ViewGroup;
import butterknife.ButterKnife;
import dagger.android.support.AndroidSupportInjection;
import fr.free.nrw.commons.R;
import timber.log.Timber;
@ -18,6 +20,12 @@ public class NoPermissionsFragment extends Fragment {
public NoPermissionsFragment() {
}
@Override
public void onAttach(Context context) {
AndroidSupportInjection.inject(this);
super.onAttach(context);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {

View file

@ -13,7 +13,7 @@ import fr.free.nrw.commons.location.LatLng;
public class Place {
public final String name;
private final Description description;
private final Label label;
private final String longDescription;
private final Uri secondaryImageUrl;
public final LatLng location;
@ -24,18 +24,22 @@ public class Place {
public final Sitelinks siteLinks;
public Place(String name, Description description, String longDescription,
public Place(String name, Label label, String longDescription,
Uri secondaryImageUrl, LatLng location, Sitelinks siteLinks) {
this.name = name;
this.description = description;
this.label = label;
this.longDescription = longDescription;
this.secondaryImageUrl = secondaryImageUrl;
this.location = location;
this.siteLinks = siteLinks;
}
public Description getDescription() {
return description;
public Label getLabel() {
return label;
}
public String getLongDescription() {
return longDescription;
}
public void setDistance(String distance) {
@ -67,10 +71,8 @@ public class Place {
* Most common types of desc: building, house, cottage, farmhouse,
* village, civil parish, church, railway station,
* gatehouse, milestone, inn, secondary school, hotel
*
* TODO Give a more accurate class name (see issue #742).
*/
public enum Description {
public enum Label {
BUILDING("building", R.drawable.round_icon_generic_building),
HOUSE("house", R.drawable.round_icon_house),
@ -95,19 +97,19 @@ public class Place {
WATERFALL("waterfall", R.drawable.round_icon_waterfall),
UNKNOWN("?", R.drawable.round_icon_unknown);
private static final Map<String, Description> TEXT_TO_DESCRIPTION
= new HashMap<>(Description.values().length);
private static final Map<String, Label> TEXT_TO_DESCRIPTION
= new HashMap<>(Label.values().length);
static {
for (Description description : values()) {
TEXT_TO_DESCRIPTION.put(description.text, description);
for (Label label : values()) {
TEXT_TO_DESCRIPTION.put(label.text, label);
}
}
private final String text;
@DrawableRes private final int icon;
Description(String text, @DrawableRes int icon) {
Label(String text, @DrawableRes int icon) {
this.text = text;
this.icon = icon;
}
@ -121,9 +123,9 @@ public class Place {
return icon;
}
public static Description fromText(String text) {
Description description = TEXT_TO_DESCRIPTION.get(text);
return description == null ? UNKNOWN : description;
public static Label fromText(String text) {
Label label = TEXT_TO_DESCRIPTION.get(text);
return label == null ? UNKNOWN : label;
}
}
}

View file

@ -43,13 +43,13 @@ class PlaceRenderer extends Renderer<Place> {
public void render() {
Place place = getContent();
tvName.setText(place.name);
String descriptionText = place.getDescription().getText();
String descriptionText = place.getLongDescription();
if (descriptionText.equals("?")) {
descriptionText = getContext().getString(R.string.no_description_found);
}
tvDesc.setText(descriptionText);
distance.setText(place.distance);
icon.setImageResource(place.getDescription().getIcon());
icon.setImageResource(place.getLabel().getIcon());
}
interface PlaceClickedListener {

View file

@ -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;
}
}

View file

@ -0,0 +1,21 @@
package fr.free.nrw.commons.notification;
/**
* Created by root on 18.12.2017.
*/
public class Notification {
public NotificationType notificationType;
public String notificationText;
public String date;
public String description;
public String link;
public Notification(NotificationType notificationType, String notificationText, String date, String description, String link) {
this.notificationType = notificationType;
this.notificationText = notificationText;
this.date = date;
this.description = description;
this.link = link;
}
}

View file

@ -0,0 +1,86 @@
package fr.free.nrw.commons.notification;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import com.pedrogomez.renderers.RVRendererAdapter;
import java.util.List;
import javax.inject.Inject;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
/**
* Created by root on 18.12.2017.
*/
public class NotificationActivity extends NavigationBaseActivity {
NotificationAdapterFactory notificationAdapterFactory;
@BindView(R.id.listView) RecyclerView recyclerView;
@Inject NotificationController controller;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_notification);
ButterKnife.bind(this);
initListView();
initDrawer();
}
private void initListView() {
recyclerView = findViewById(R.id.listView);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
addNotifications();
}
@SuppressLint("CheckResult")
private void addNotifications() {
Timber.d("Add notifications");
Observable.fromCallable(() -> controller.getNotifications())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(notificationList -> {
Timber.d("Number of notifications is %d", notificationList.size());
setAdapter(notificationList);
}, throwable -> Timber.e(throwable, "Error occurred while loading notifications"));
}
private void handleUrl(String url) {
if (url == null || url.equals("")) {
return;
}
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
}
private void setAdapter(List<Notification> notificationList) {
notificationAdapterFactory = new NotificationAdapterFactory(notification -> {
Timber.d("Notification clicked %s", notification.link);
handleUrl(notification.link);
});
RVRendererAdapter<Notification> adapter = notificationAdapterFactory.create(notificationList);
recyclerView.setAdapter(adapter);
}
public static void startYourself(Context context) {
Intent intent = new Intent(context, NotificationActivity.class);
context.startActivity(intent);
}
}

View file

@ -0,0 +1,30 @@
package fr.free.nrw.commons.notification;
import android.support.annotation.NonNull;
import com.pedrogomez.renderers.ListAdapteeCollection;
import com.pedrogomez.renderers.RVRendererAdapter;
import com.pedrogomez.renderers.RendererBuilder;
import java.util.Collections;
import java.util.List;
/**
* Created by root on 19.12.2017.
*/
class NotificationAdapterFactory {
private NotificationRenderer.NotificationClicked listener;
NotificationAdapterFactory(@NonNull NotificationRenderer.NotificationClicked listener) {
this.listener = listener;
}
public RVRendererAdapter<Notification> create(List<Notification> notifications) {
RendererBuilder<Notification> builder = new RendererBuilder<Notification>()
.bind(Notification.class, new NotificationRenderer(listener));
ListAdapteeCollection<Notification> collection = new ListAdapteeCollection<>(
notifications != null ? notifications : Collections.<Notification>emptyList());
return new RVRendererAdapter<>(builder, collection);
}
}

View file

@ -0,0 +1,39 @@
package fr.free.nrw.commons.notification;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
/**
* Created by root on 19.12.2017.
*/
@Singleton
public class NotificationController {
private MediaWikiApi mediaWikiApi;
private SessionManager sessionManager;
@Inject
public NotificationController(MediaWikiApi mediaWikiApi, SessionManager sessionManager) {
this.mediaWikiApi = mediaWikiApi;
this.sessionManager = sessionManager;
}
public List<Notification> getNotifications() throws IOException {
if (mediaWikiApi.validateLogin()) {
return mediaWikiApi.getNotifications();
} else {
Boolean authTokenValidated = sessionManager.revalidateAuthToken();
if (authTokenValidated != null && authTokenValidated) {
return mediaWikiApi.getNotifications();
}
}
return new ArrayList<>();
}
}

View file

@ -0,0 +1,64 @@
package fr.free.nrw.commons.notification;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.pedrogomez.renderers.Renderer;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
/**
* Created by root on 19.12.2017.
*/
public class NotificationRenderer extends Renderer<Notification> {
@BindView(R.id.title) TextView title;
@BindView(R.id.description) TextView description;
@BindView(R.id.time) TextView time;
@BindView(R.id.icon) ImageView icon;
private NotificationClicked listener;
NotificationRenderer(NotificationClicked listener) {
this.listener = listener;
}
@Override
protected void setUpView(View view) { }
@Override
protected void hookListeners(View rootView) {
rootView.setOnClickListener(v -> listener.notificationClicked(getContent()));
}
@Override
protected View inflate(LayoutInflater layoutInflater, ViewGroup viewGroup) {
View inflatedView = layoutInflater.inflate(R.layout.item_notification, viewGroup, false);
ButterKnife.bind(this, inflatedView);
return inflatedView;
}
@Override
public void render() {
Notification notification = getContent();
title.setText(notification.notificationText);
time.setText(notification.date);
description.setText(notification.description);
switch (notification.notificationType) {
case THANK_YOU_EDIT:
icon.setImageResource(R.drawable.ic_edit_black_24dp);
break;
default:
icon.setImageResource(R.drawable.round_icon_unknown);
}
}
public interface NotificationClicked{
void notificationClicked(Notification notification);
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -1,7 +1,5 @@
package fr.free.nrw.commons.settings;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v7.app.AppCompatDelegate;
@ -11,9 +9,16 @@ import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
/**
* allows the user to change the settings
*/
public class SettingsActivity extends NavigationBaseActivity {
private AppCompatDelegate settingsDelegate;
/**
* to be called when the activity starts
* @param savedInstanceState the previously saved state
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
// Check prefs on every activity starts
@ -31,6 +36,10 @@ public class SettingsActivity extends NavigationBaseActivity {
}
// Get an action bar
/**
* takes care of actions taken after the creation has happened
* @param savedInstanceState the saved state
*/
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
@ -43,7 +52,11 @@ public class SettingsActivity extends NavigationBaseActivity {
//settingsDelegate.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
//Handle action-bar clicks
/**
* Handle action-bar clicks
* @param item the selected item
* @return true on success, false on failure
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
@ -54,9 +67,4 @@ public class SettingsActivity extends NavigationBaseActivity {
return super.onOptionsItemSelected(item);
}
}
public static void startYourself(Context context) {
Intent settingsIntent = new Intent(context, SettingsActivity.class);
context.startActivity(settingsIntent);
}
}

View file

@ -1,22 +1,50 @@
package fr.free.nrw.commons.settings;
import android.Manifest;
import android.app.AlertDialog;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.EditTextPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.FileProvider;
import android.widget.Toast;
import java.io.File;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.utils.FileUtils;
public class SettingsFragment extends PreferenceFragment {
private static final int REQUEST_CODE_WRITE_EXTERNAL_STORAGE = 100;
@Inject @Named("default_preferences") SharedPreferences prefs;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ApplicationlessInjection
.getInstance(getActivity().getApplicationContext())
.getCommonsApplicationComponent()
.inject(this);
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.preferences);
@ -38,14 +66,17 @@ public class SettingsFragment extends PreferenceFragment {
});
final EditTextPreference uploadLimit = (EditTextPreference) findPreference("uploads");
final SharedPreferences sharedPref = PreferenceManager
.getDefaultSharedPreferences(CommonsApplication.getInstance());
int uploads = sharedPref.getInt(Prefs.UPLOADS_SHOWING, 100);
int uploads = prefs.getInt(Prefs.UPLOADS_SHOWING, 100);
uploadLimit.setText(uploads + "");
uploadLimit.setSummary(uploads + "");
uploadLimit.setOnPreferenceChangeListener((preference, newValue) -> {
int value = Integer.parseInt(newValue.toString());
final SharedPreferences.Editor editor = sharedPref.edit();
int value;
try {
value = Integer.parseInt(newValue.toString());
} catch(Exception e) {
value = 100; //Default number
}
final SharedPreferences.Editor editor = prefs.edit();
if (value > 500) {
new AlertDialog.Builder(getActivity())
.setTitle(R.string.maximum_limit)
@ -58,14 +89,71 @@ public class SettingsFragment extends PreferenceFragment {
uploadLimit.setSummary(500 + "");
uploadLimit.setText(500 + "");
} else {
editor.putInt(Prefs.UPLOADS_SHOWING, Integer.parseInt(newValue.toString()));
editor.putInt(Prefs.UPLOADS_SHOWING, value);
editor.putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED,true);
uploadLimit.setSummary(newValue.toString());
uploadLimit.setSummary(String.valueOf(value));
}
editor.apply();
return true;
});
Preference sendLogsPreference = findPreference("sendLogFile");
sendLogsPreference.setOnPreferenceClickListener(preference -> {
//first we need to check if we have the necessary permissions
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ContextCompat.checkSelfPermission(
getActivity(),
Manifest.permission.WRITE_EXTERNAL_STORAGE)
==
PackageManager.PERMISSION_GRANTED) {
sendAppLogsViaEmail();
} else {
//first get the necessary permission
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
REQUEST_CODE_WRITE_EXTERNAL_STORAGE);
}
} else {
sendAppLogsViaEmail();
}
return true;
});
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CODE_WRITE_EXTERNAL_STORAGE) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
sendAppLogsViaEmail();
}
}
}
private void sendAppLogsViaEmail() {
String appLogs = Utils.getAppLogs();
File appLogsFile = FileUtils.createAndGetAppLogsFile(appLogs);
Context applicationContext = getActivity().getApplicationContext();
Uri appLogsFilePath = FileProvider.getUriForFile(
getActivity(),
applicationContext.getPackageName() + ".provider",
appLogsFile
);
Intent feedbackIntent = new Intent(Intent.ACTION_SEND);
feedbackIntent.setType("message/rfc822");
feedbackIntent.putExtra(Intent.EXTRA_EMAIL,
new String[]{CommonsApplication.LOGS_PRIVATE_EMAIL});
feedbackIntent.putExtra(Intent.EXTRA_SUBJECT,
String.format(CommonsApplication.FEEDBACK_EMAIL_SUBJECT,
BuildConfig.VERSION_NAME));
feedbackIntent.putExtra(Intent.EXTRA_STREAM,appLogsFilePath);
try {
startActivity(feedbackIntent);
} catch (ActivityNotFoundException e) {
Toast.makeText(getActivity(), R.string.no_email_client, Toast.LENGTH_SHORT).show();
}
}
}

View file

@ -3,18 +3,17 @@ package fr.free.nrw.commons.theme;
import android.content.Intent;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v7.app.AppCompatActivity;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.di.CommonsDaggerAppCompatActivity;
public class BaseActivity extends AppCompatActivity {
public abstract class BaseActivity extends CommonsDaggerAppCompatActivity {
boolean currentTheme;
@Override
protected void onCreate(Bundle savedInstanceState) {
if(Utils.isDarkTheme(this)){
boolean currentThemeIsDark = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme", false);
if (currentThemeIsDark){
currentTheme = true;
setTheme(R.style.DarkAppTheme);
} else {
@ -27,8 +26,8 @@ public class BaseActivity extends AppCompatActivity {
@Override
protected void onResume() {
// Restart activity if theme is changed
boolean newTheme = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme",false);
if(currentTheme!=newTheme){ //is activity theme changed
boolean newTheme = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme", false);
if (currentTheme != newTheme) { //is activity theme changed
Intent intent = getIntent();
finish();
startActivity(intent);

View file

@ -1,6 +1,9 @@
package fr.free.nrw.commons.theme;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.NonNull;
import android.support.design.widget.NavigationView;
@ -9,7 +12,9 @@ import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.Toolbar;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import butterknife.BindView;
@ -18,9 +23,11 @@ import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.WelcomeActivity;
import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.nearby.NearbyActivity;
import fr.free.nrw.commons.notification.NotificationActivity;
import fr.free.nrw.commons.settings.SettingsActivity;
import timber.log.Timber;
@ -47,6 +54,22 @@ public abstract class NavigationBaseActivity extends BaseActivity
toggle.setDrawerIndicatorEnabled(true);
toggle.syncState();
setDrawerPaneWidth();
setUserName();
}
/**
* Set the username in navigationHeader.
*/
private void setUserName() {
View navHeaderView = navigationView.getHeaderView(0);
TextView username = navHeaderView.findViewById(R.id.username);
AccountManager accountManager = AccountManager.get(this);
Account[] allAccounts = accountManager.getAccountsByType(AccountUtil.ACCOUNT_TYPE);
if (allAccounts.length != 0) {
username.setText(allAccounts[0].name);
}
}
public void initBackButton() {
@ -70,30 +93,25 @@ public abstract class NavigationBaseActivity extends BaseActivity
@Override
public boolean onNavigationItemSelected(@NonNull final MenuItem item) {
switch (item.getItemId()) {
final int itemId = item.getItemId();
switch (itemId) {
case R.id.action_home:
drawerLayout.closeDrawer(navigationView);
if (!(this instanceof ContributionsActivity)) {
ContributionsActivity.startYourself(this);
}
startActivityWithFlags(
this, ContributionsActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP,
Intent.FLAG_ACTIVITY_SINGLE_TOP);
return true;
case R.id.action_nearby:
drawerLayout.closeDrawer(navigationView);
if (!(this instanceof NearbyActivity)) {
NearbyActivity.startYourself(this);
}
startActivityWithFlags(this, NearbyActivity.class, Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
return true;
case R.id.action_about:
drawerLayout.closeDrawer(navigationView);
if (!(this instanceof AboutActivity)) {
AboutActivity.startYourself(this);
}
startActivityWithFlags(this, AboutActivity.class, Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
return true;
case R.id.action_settings:
drawerLayout.closeDrawer(navigationView);
if (!(this instanceof SettingsActivity)) {
SettingsActivity.startYourself(this);
}
startActivityWithFlags(this, SettingsActivity.class, Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
return true;
case R.id.action_introduction:
drawerLayout.closeDrawer(navigationView);
@ -126,7 +144,12 @@ public abstract class NavigationBaseActivity extends BaseActivity
.setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel())
.show();
return true;
case R.id.action_notifications:
drawerLayout.closeDrawer(navigationView);
NotificationActivity.startYourself(this);
return true;
default:
Timber.e("Unknown option [%s] selected from the navigation menu", itemId);
return false;
}
}
@ -143,4 +166,12 @@ public abstract class NavigationBaseActivity extends BaseActivity
finish();
}
}
public static <T> void startActivityWithFlags(Context context, Class<T> cls, int... flags) {
Intent intent = new Intent(context, cls);
for (int flag: flags) {
intent.addFlags(flag);
}
context.startActivity(intent);
}
}

View file

@ -1,7 +1,7 @@
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;
@ -16,22 +16,49 @@ import android.util.AttributeSet;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.utils.UiUtils;
/**
* a text view compatible with older versions of the platform
*/
public class CompatTextView extends AppCompatTextView {
/**
* Constructs a new instance of CompatTextView
*
* @param context the view context
*/
public CompatTextView(Context context) {
super(context);
init(null);
}
/**
* Constructs a new instance of CompatTextView
*
* @param context the view context
* @param attrs the set of attributes for the view
*/
public CompatTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs);
}
/**
* Constructs a new instance of CompatTextView
*
* @param context
* @param attrs
* @param defStyleAttr
*/
public CompatTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs);
}
/**
* initializes the view
*
* @param attrs the attribute set of the view, which can be null
*/
private void init(@Nullable AttributeSet attrs) {
if (attrs != null) {
Context context = getContext();

View file

@ -1,26 +1,51 @@
package fr.free.nrw.commons.ui.widget;
import android.content.Context;
import android.os.Build;
import android.support.v7.widget.AppCompatTextView;
import android.text.Html;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.util.AttributeSet;
import fr.free.nrw.commons.Utils;
/**
* An {@link AppCompatTextView} which formats the text to HTML displayable text and makes any
* links clickable.
*/
public class HtmlTextView extends AppCompatTextView {
/**
* Constructs a new instance of HtmlTextView
* @param context the context of the view
* @param attrs the set of attributes for the view
*/
public HtmlTextView(Context context, AttributeSet attrs) {
super(context, attrs);
setMovementMethod(LinkMovementMethod.getInstance());
setText(Utils.fromHtml(getText().toString()));
setText(fromHtml(getText().toString()));
}
/**
* Sets the text to be displayed
* @param newText the text to be displayed
*/
public void setHtmlText(String newText) {
setText(Utils.fromHtml(newText));
setText(fromHtml(newText));
}
/**
* Fix Html.fromHtml is deprecated problem
*
* @param source provided Html string
* @return returned Spanned of appropriate method according to version check
*/
private static Spanned fromHtml(String source) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return Html.fromHtml(source, Html.FROM_HTML_MODE_LEGACY);
} else {
//noinspection deprecation
return Html.fromHtml(source);
}
}
}

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