Merge branch 'master' into Bug#954

This commit is contained in:
harisankerPradeep 2018-02-15 19:25:02 +05:30 committed by GitHub
commit 2eab64eb7a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
359 changed files with 9275 additions and 3359 deletions

View file

@ -35,7 +35,7 @@ before_script:
- android-wait-for-emulator - android-wait-for-emulator
script: script:
- ./gradlew clean check connectedCheck jacocoTestReport --stacktrace - ./gradlew clean check connectedCheck jacocoTestReport
after_success: after_success:
- bash <(curl -s https://codecov.io/bash) - bash <(curl -s https://codecov.io/bash)

View file

@ -1,5 +1,34 @@
# Wikimedia Commons for Android # Wikimedia Commons for Android
## v2.6.7
- Added null checks to prevent frequent crashes in ModificationsSyncAdapter
## v2.6.6
- Refactored Dagger to fix crashes encountered in production
- Fixed "?" displaying in description of Nearby places
- Database-related cleanup and tests
- Optimized dimens.xml
- Fixed issue where map opens with incorrect coordinates
## v2.6.5 beta
- Changed "send log" feature to only send logs to private Google group forum
- Switched to using Wikimedia maps server instead of Mapbox for privacy reasons
- Removed event logging from app for privacy reasons
- Fixed crash caused by rapidly switching from Nearby map to list while loading
## v2.6.4 beta
- Excluded httpclient and commons-logging to fix release build errors
- Fixed crashes caused by Fresco and Dagger
## v2.6.3 beta
- Same as 2.6.2 except with localizations added for Google Code-In
## v2.6.2 beta
- Reverted temporarily to last stable version while working on crash fix
## v2.6.1 beta
- Failed attempt to fix crashes in release build with the previous beta release
## v2.6.0 beta ## v2.6.0 beta
- Multiple bugfixes for location updates and list/map loading in Nearby - Multiple bugfixes for location updates and list/map loading in Nearby
- Multiple fixes for various crashes and memory leaks - Multiple fixes for various crashes and memory leaks

1
CONTRIBUTING.md Normal file
View file

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

950
CREDITS
View file

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

33
ISSUE_TEMPLATE.md Normal file
View file

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

View file

@ -18,16 +18,16 @@ dependencies {
implementation 'com.google.code.gson:gson:2.8.1' implementation 'com.google.code.gson:gson:2.8.1'
implementation 'com.jakewharton.timber:timber:4.5.1' implementation 'com.jakewharton.timber:timber:4.5.1'
implementation 'info.debatty:java-string-similarity:0.24' implementation 'info.debatty:java-string-similarity:0.24'
implementation ('com.mapbox.mapboxsdk:mapbox-android-sdk:5.1.0@aar'){ implementation ('com.mapbox.mapboxsdk:mapbox-android-sdk:5.2.1@aar'){
transitive=true transitive=true
} }
implementation "com.android.support:support-v4:${project.supportLibVersion}" implementation "com.android.support:support-v4:$SUPPORT_LIB_VERSION"
implementation "com.android.support:appcompat-v7:${project.supportLibVersion}" implementation "com.android.support:appcompat-v7:$SUPPORT_LIB_VERSION"
implementation "com.android.support:design:${project.supportLibVersion}" implementation "com.android.support:design:$SUPPORT_LIB_VERSION"
implementation "com.android.support:cardview-v7:${project.supportLibVersion}" implementation "com.android.support:cardview-v7:$SUPPORT_LIB_VERSION"
implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION" implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION"
kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION" kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
@ -44,7 +44,7 @@ dependencies {
implementation 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.0.0' implementation 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.0.0'
implementation 'com.jakewharton.rxbinding2:rxbinding-design:2.0.0' implementation 'com.jakewharton.rxbinding2:rxbinding-design:2.0.0'
implementation 'com.facebook.fresco:fresco:1.3.0' implementation 'com.facebook.fresco:fresco:1.5.0'
implementation 'com.facebook.stetho:stetho:1.5.0' implementation 'com.facebook.stetho:stetho:1.5.0'
implementation "com.google.dagger:dagger:$DAGGER_VERSION" implementation "com.google.dagger:dagger:$DAGGER_VERSION"
@ -62,12 +62,17 @@ dependencies {
testImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1' testImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1'
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1' androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1'
androidTestImplementation "com.android.support:support-annotations:${project.supportLibVersion}" androidTestImplementation "com.android.support:support-annotations:$SUPPORT_LIB_VERSION"
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.1' debugImplementation "com.squareup.leakcanary:leakcanary-android:$LEAK_CANARY"
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1' releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY"
testImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1' testImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY"
implementation "com.google.dagger:dagger:$DAGGER_VERSION"
implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
} }
android { android {
@ -78,8 +83,8 @@ android {
defaultConfig { defaultConfig {
applicationId 'fr.free.nrw.commons' applicationId 'fr.free.nrw.commons'
versionCode 76 versionCode 82
versionName '2.6.1' versionName '2.6.7'
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName()) setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
minSdkVersion project.minSdkVersion minSdkVersion project.minSdkVersion
@ -89,7 +94,12 @@ android {
} }
sourceSets { sourceSets {
// use kotlin only in tests (for now)
test.java.srcDirs += 'src/test/kotlin' test.java.srcDirs += 'src/test/kotlin'
// use main assets and resources in test
test.assets.srcDirs += 'src/main/assets'
test.resources.srcDirs += 'src/main/resoures'
} }
buildTypes { buildTypes {

View file

@ -7,6 +7,8 @@ import android.support.test.runner.AndroidJUnit4;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import fr.free.nrw.commons.BuildConfig;
import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
@ -14,7 +16,7 @@ import static org.junit.Assert.assertThat;
public class FileUtilsTest { public class FileUtilsTest {
@Test @Test
public void isSelfOwned() throws Exception { public void isSelfOwned() throws Exception {
Uri uri = Uri.parse("content://fr.free.nrw.commons.provider/document/1"); Uri uri = Uri.parse("content://" + BuildConfig.APPLICATION_ID + ".provider/document/1");
boolean selfOwned = FileUtils.isSelfOwned(InstrumentationRegistry.getTargetContext(), uri); boolean selfOwned = FileUtils.isSelfOwned(InstrumentationRegistry.getTargetContext(), uri);
assertThat(selfOwned, is(true)); assertThat(selfOwned, is(true));
} }

View file

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

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; package fr.free.nrw.commons;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.view.View;
import android.widget.TextView; import android.widget.TextView;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import butterknife.OnClick;
import fr.free.nrw.commons.theme.NavigationBaseActivity; import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.ui.widget.HtmlTextView; import fr.free.nrw.commons.ui.widget.HtmlTextView;
/**
* Represents about screen of this app
*/
public class AboutActivity extends NavigationBaseActivity { public class AboutActivity extends NavigationBaseActivity {
@BindView(R.id.about_version) TextView versionText; @BindView(R.id.about_version) TextView versionText;
@BindView(R.id.about_license) HtmlTextView aboutLicenseText; @BindView(R.id.about_license) HtmlTextView aboutLicenseText;
/**
* This method helps in the creation About screen
*
* @param savedInstanceState Data bundle
*/
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -27,4 +37,32 @@ public class AboutActivity extends NavigationBaseActivity {
versionText.setText(BuildConfig.VERSION_NAME); versionText.setText(BuildConfig.VERSION_NAME);
initDrawer(); initDrawer();
} }
@OnClick(R.id.facebook_launch_icon)
public void launchFacebook(View view) {
Intent intent;
try {
intent = new Intent(Intent.ACTION_VIEW, Uri.parse("fb://page/" + "1921335171459985"));
intent.setPackage("com.facebook.katana");
startActivity(intent);
} catch (Exception e) {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://www.facebook.com/" + "1921335171459985")));
}
}
@OnClick(R.id.github_launch_icon)
public void launchGithub(View view) {
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/commons-app/apps-android-commons\\"));
startActivity(browserIntent);
}
@OnClick(R.id.website_launch_icon)
public void launchWebsite(View view) {
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://commons-app.github.io/\\"));
startActivity(browserIntent);
}
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -11,12 +11,12 @@ import org.xml.sax.SAXException;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.ParserConfigurationException;
@ -33,45 +33,39 @@ import timber.log.Timber;
* which are not intrinsic to the media and may change due to editing. * which are not intrinsic to the media and may change due to editing.
*/ */
public class MediaDataExtractor { public class MediaDataExtractor {
private final MediaWikiApi mediaWikiApi;
private boolean fetched; private boolean fetched;
private String filename;
private ArrayList<String> categories; private ArrayList<String> categories;
private Map<String, String> descriptions; private Map<String, String> descriptions;
private String license; private String license;
private @Nullable LatLng coordinates; private @Nullable LatLng coordinates;
private LicenseList licenseList;
/** @Inject
* @param filename of the target media object, should include 'File:' prefix public MediaDataExtractor(MediaWikiApi mwApi) {
*/ this.categories = new ArrayList<>();
public MediaDataExtractor(String filename, LicenseList licenseList) { this.descriptions = new HashMap<>();
this.filename = filename; this.fetched = false;
categories = new ArrayList<>(); this.mediaWikiApi = mwApi;
descriptions = new HashMap<>();
fetched = false;
this.licenseList = licenseList;
} }
/** /*
* Actually fetch the data over the network. * Actually fetch the data over the network.
* todo: use local caching? * todo: use local caching?
* *
* Warning: synchronous i/o, call on a background thread * Warning: synchronous i/o, call on a background thread
*/ */
public void fetch() throws IOException { public void fetch(String filename, LicenseList licenseList) throws IOException {
if (fetched) { if (fetched) {
throw new IllegalStateException("Tried to call MediaDataExtractor.fetch() again."); throw new IllegalStateException("Tried to call MediaDataExtractor.fetch() again.");
} }
MediaWikiApi api = CommonsApplication.getInstance().getMWApi(); MediaResult result = mediaWikiApi.fetchMediaByFilename(filename);
MediaResult result = api.fetchMediaByFilename(filename);
// In-page category links are extracted from source, as XML doesn't cover [[links]] // In-page category links are extracted from source, as XML doesn't cover [[links]]
extractCategories(result.getWikiSource()); extractCategories(result.getWikiSource());
// Description template info is extracted from preprocessor XML // Description template info is extracted from preprocessor XML
processWikiParseTree(result.getParseTreeXmlSource()); processWikiParseTree(result.getParseTreeXmlSource(), licenseList);
fetched = true; fetched = true;
} }
@ -90,7 +84,7 @@ public class MediaDataExtractor {
} }
} }
private void processWikiParseTree(String source) throws IOException { private void processWikiParseTree(String source, LicenseList licenseList) throws IOException {
Document doc; Document doc;
try { try {
DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,9 @@
package fr.free.nrw.commons.auth; package fr.free.nrw.commons.auth;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorActivity; import android.accounts.AccountAuthenticatorActivity;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
@ -8,6 +11,7 @@ import android.os.Bundle;
import android.support.annotation.ColorRes; import android.support.annotation.ColorRes;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.StringRes; import android.support.annotation.StringRes;
import android.support.design.widget.TextInputLayout;
import android.support.v4.app.NavUtils; import android.support.v4.app.NavUtils;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatDelegate; import android.support.v7.app.AppCompatDelegate;
@ -21,25 +25,44 @@ import android.widget.Button;
import android.widget.EditText; import android.widget.EditText;
import android.widget.TextView; import android.widget.TextView;
import java.io.IOException;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import dagger.android.AndroidInjection;
import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.PageTitle; import fr.free.nrw.commons.PageTitle;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.WelcomeActivity; import fr.free.nrw.commons.WelcomeActivity;
import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.theme.NavigationBaseActivity; import fr.free.nrw.commons.theme.NavigationBaseActivity;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber; import timber.log.Timber;
import static android.view.KeyEvent.KEYCODE_ENTER; import static android.view.KeyEvent.KEYCODE_ENTER;
import static android.view.View.VISIBLE;
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE; import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE;
import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE;
public class LoginActivity extends AccountAuthenticatorActivity { public class LoginActivity extends AccountAuthenticatorActivity {
public static final String PARAM_USERNAME = "fr.free.nrw.commons.login.username"; public static final String PARAM_USERNAME = "fr.free.nrw.commons.login.username";
@Inject MediaWikiApi mwApi;
@Inject AccountUtil accountUtil;
@Inject SessionManager sessionManager;
@Inject @Named("application_preferences") SharedPreferences prefs;
@Inject @Named("default_preferences") SharedPreferences defaultPrefs;
@BindView(R.id.loginButton) Button loginButton; @BindView(R.id.loginButton) Button loginButton;
@BindView(R.id.signupButton) Button signupButton; @BindView(R.id.signupButton) Button signupButton;
@BindView(R.id.loginUsername) EditText usernameEdit; @BindView(R.id.loginUsername) EditText usernameEdit;
@ -47,11 +70,9 @@ public class LoginActivity extends AccountAuthenticatorActivity {
@BindView(R.id.loginTwoFactor) EditText twoFactorEdit; @BindView(R.id.loginTwoFactor) EditText twoFactorEdit;
@BindView(R.id.error_message_container) ViewGroup errorMessageContainer; @BindView(R.id.error_message_container) ViewGroup errorMessageContainer;
@BindView(R.id.error_message) TextView errorMessage; @BindView(R.id.error_message) TextView errorMessage;
@BindView(R.id.two_factor_container)TextInputLayout twoFactorContainer;
private CommonsApplication app;
ProgressDialog progressDialog; ProgressDialog progressDialog;
private AppCompatDelegate delegate; private AppCompatDelegate delegate;
private SharedPreferences prefs = null;
private LoginTextWatcher textWatcher = new LoginTextWatcher(); private LoginTextWatcher textWatcher = new LoginTextWatcher();
@Override @Override
@ -59,16 +80,17 @@ public class LoginActivity extends AccountAuthenticatorActivity {
setTheme(Utils.isDarkTheme(this) ? R.style.DarkAppTheme : R.style.LightAppTheme); setTheme(Utils.isDarkTheme(this) ? R.style.DarkAppTheme : R.style.LightAppTheme);
getDelegate().installViewFactory(); getDelegate().installViewFactory();
getDelegate().onCreate(savedInstanceState); getDelegate().onCreate(savedInstanceState);
super.onCreate(savedInstanceState);
app = CommonsApplication.getInstance(); super.onCreate(savedInstanceState);
ApplicationlessInjection
.getInstance(this.getApplicationContext())
.getCommonsApplicationComponent()
.inject(this);
setContentView(R.layout.activity_login); setContentView(R.layout.activity_login);
ButterKnife.bind(this); ButterKnife.bind(this);
prefs = getSharedPreferences("fr.free.nrw.commons", MODE_PRIVATE);
usernameEdit.addTextChangedListener(textWatcher); usernameEdit.addTextChangedListener(textWatcher);
passwordEdit.addTextChangedListener(textWatcher); passwordEdit.addTextChangedListener(textWatcher);
twoFactorEdit.addTextChangedListener(textWatcher); twoFactorEdit.addTextChangedListener(textWatcher);
@ -91,7 +113,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
WelcomeActivity.startYourself(this); WelcomeActivity.startYourself(this);
prefs.edit().putBoolean("firstrun", false).apply(); prefs.edit().putBoolean("firstrun", false).apply();
} }
if (app.getCurrentAccount() != null) { if (sessionManager.getCurrentAccount() != null) {
startMainActivity(); startMainActivity();
} }
} }
@ -113,6 +135,120 @@ public class LoginActivity extends AccountAuthenticatorActivity {
super.onDestroy(); super.onDestroy();
} }
private void performLogin() {
Timber.d("Login to start!");
final String username = canonicializeUsername(usernameEdit.getText().toString());
final String password = passwordEdit.getText().toString();
String twoFactorCode = twoFactorEdit.getText().toString();
showLoggingProgressBar();
Observable.fromCallable(() -> login(username, password, twoFactorCode))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> handleLogin(username, password, result));
}
private String login(String username, String password, String twoFactorCode) {
try {
if (twoFactorCode.isEmpty()) {
return mwApi.login(username, password);
} else {
return mwApi.login(username, password, twoFactorCode);
}
} catch (IOException e) {
// Do something better!
return "NetworkFailure";
}
}
private void handleLogin(String username, String password, String result) {
Timber.d("Login done!");
if (result.equals("PASS")) {
handlePassResult(username, password);
} else {
handleOtherResults(result);
}
}
private void showLoggingProgressBar() {
progressDialog = new ProgressDialog(this);
progressDialog.setIndeterminate(true);
progressDialog.setTitle(getString(R.string.logging_in_title));
progressDialog.setMessage(getString(R.string.logging_in_message));
progressDialog.setCanceledOnTouchOutside(false);
progressDialog.show();
}
private void handlePassResult(String username, String password) {
showSuccessAndDismissDialog();
requestAuthToken();
AccountAuthenticatorResponse response = null;
Bundle extras = getIntent().getExtras();
if (extras != null) {
Timber.d("Bundle of extras: %s", extras);
response = extras.getParcelable(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE);
if (response != null) {
Bundle authResult = new Bundle();
authResult.putString(AccountManager.KEY_ACCOUNT_NAME, username);
authResult.putString(AccountManager.KEY_ACCOUNT_TYPE, ACCOUNT_TYPE);
response.onResult(authResult);
}
}
accountUtil.createAccount(response, username, password);
startMainActivity();
}
protected void requestAuthToken() {
AccountManager accountManager = AccountManager.get(this);
Account curAccount = sessionManager.getCurrentAccount();
if (curAccount != null) {
accountManager.setAuthToken(curAccount, AUTH_TOKEN_TYPE, mwApi.getAuthCookie());
}
}
/**
* Match known failure message codes and provide messages.
*
* @param result String
*/
private void handleOtherResults(String result) {
if (result.equals("NetworkFailure")) {
// Matches NetworkFailure which is created by the doInBackground method
showMessageAndCancelDialog(R.string.login_failed_network);
} else if (result.toLowerCase().contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) {
// Matches nosuchuser, nosuchusershort, noname
showMessageAndCancelDialog(R.string.login_failed_username);
emptySensitiveEditFields();
} else if (result.toLowerCase().contains("wrongpassword".toLowerCase())) {
// Matches wrongpassword, wrongpasswordempty
showMessageAndCancelDialog(R.string.login_failed_password);
emptySensitiveEditFields();
} else if (result.toLowerCase().contains("throttle".toLowerCase())) {
// Matches unknown throttle error codes
showMessageAndCancelDialog(R.string.login_failed_throttled);
} else if (result.toLowerCase().contains("userblocked".toLowerCase())) {
// Matches login-userblocked
showMessageAndCancelDialog(R.string.login_failed_blocked);
} else if (result.equals("2FA")) {
askUserForTwoFactorAuth();
} else {
// Occurs with unhandled login failure codes
Timber.d("Login failed with reason: %s", result);
showMessageAndCancelDialog(R.string.login_failed_generic);
}
}
/**
* Because Mediawiki is upercase-first-char-then-case-sensitive :)
* @param username String
* @return String canonicial username
*/
private String canonicializeUsername(String username) {
return new PageTitle(username).getText();
}
@Override @Override
protected void onStart() { protected void onStart() {
super.onStart(); super.onStart();
@ -153,12 +289,10 @@ public class LoginActivity extends AccountAuthenticatorActivity {
} }
public void askUserForTwoFactorAuth() { public void askUserForTwoFactorAuth() {
if (BuildConfig.DEBUG) { progressDialog.dismiss();
twoFactorEdit.setVisibility(View.VISIBLE); twoFactorContainer.setVisibility(VISIBLE);
showMessageAndCancelDialog(R.string.login_failed_2fa_needed); twoFactorEdit.setVisibility(VISIBLE);
} else { showMessageAndCancelDialog(R.string.login_failed_2fa_needed);
showMessageAndCancelDialog(R.string.login_failed_2fa_not_supported);
}
} }
public void showMessageAndCancelDialog(@StringRes int resId) { public void showMessageAndCancelDialog(@StringRes int resId) {
@ -181,12 +315,6 @@ public class LoginActivity extends AccountAuthenticatorActivity {
finish(); finish();
} }
private void performLogin() {
Timber.d("Login to start!");
LoginTask task = getLoginTask();
task.execute();
}
private void signUp() { private void signUp() {
Intent intent = new Intent(this, SignupActivity.class); Intent intent = new Intent(this, SignupActivity.class);
startActivity(intent); startActivity(intent);
@ -207,28 +335,10 @@ public class LoginActivity extends AccountAuthenticatorActivity {
}; };
} }
private LoginTask getLoginTask() {
return new LoginTask(
this,
canonicializeUsername(usernameEdit.getText().toString()),
passwordEdit.getText().toString(),
twoFactorEdit.getText().toString()
);
}
/**
* Because Mediawiki is upercase-first-char-then-case-sensitive :)
* @param username String
* @return String canonicial username
*/
private String canonicializeUsername(String username) {
return new PageTitle(username).getText();
}
private void showMessage(@StringRes int resId, @ColorRes int colorResId) { private void showMessage(@StringRes int resId, @ColorRes int colorResId) {
errorMessage.setText(getString(resId)); errorMessage.setText(getString(resId));
errorMessage.setTextColor(ContextCompat.getColor(this, colorResId)); errorMessage.setTextColor(ContextCompat.getColor(this, colorResId));
errorMessageContainer.setVisibility(View.VISIBLE); errorMessageContainer.setVisibility(VISIBLE);
} }
private AppCompatDelegate getDelegate() { private AppCompatDelegate getDelegate() {
@ -250,7 +360,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
@Override @Override
public void afterTextChanged(Editable editable) { public void afterTextChanged(Editable editable) {
boolean enabled = usernameEdit.getText().length() != 0 && passwordEdit.getText().length() != 0 boolean enabled = usernameEdit.getText().length() != 0 && passwordEdit.getText().length() != 0
&& (BuildConfig.DEBUG || twoFactorEdit.getText().length() != 0 || twoFactorEdit.getVisibility() != View.VISIBLE); && (BuildConfig.DEBUG || twoFactorEdit.getText().length() != 0 || twoFactorEdit.getVisibility() != VISIBLE);
loginButton.setEnabled(enabled); loginButton.setEnabled(enabled);
} }
} }

View file

@ -1,32 +1,40 @@
package fr.free.nrw.commons.auth; package fr.free.nrw.commons.auth;
import android.accounts.AccountAuthenticatorResponse; import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.content.SharedPreferences;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import java.io.IOException; import java.io.IOException;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.mwapi.EventLog; import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber; import timber.log.Timber;
import static android.accounts.AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE;
import static android.accounts.AccountManager.KEY_ACCOUNT_NAME;
import static android.accounts.AccountManager.KEY_ACCOUNT_TYPE;
import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE;
class LoginTask extends AsyncTask<String, String, String> { class LoginTask extends AsyncTask<String, String, String> {
private LoginActivity loginActivity; private LoginActivity loginActivity;
private String username; private String username;
private String password; private String password;
private String twoFactorCode = ""; private String twoFactorCode = "";
private CommonsApplication app; private AccountUtil accountUtil;
private MediaWikiApi mwApi;
public LoginTask(LoginActivity loginActivity, String username, String password, String twoFactorCode) { public LoginTask(LoginActivity loginActivity, String username, String password,
String twoFactorCode, AccountUtil accountUtil,
MediaWikiApi mwApi, SharedPreferences prefs) {
this.loginActivity = loginActivity; this.loginActivity = loginActivity;
this.username = username; this.username = username;
this.password = password; this.password = password;
this.twoFactorCode = twoFactorCode; this.twoFactorCode = twoFactorCode;
app = CommonsApplication.getInstance(); this.accountUtil = accountUtil;
this.mwApi = mwApi;
} }
@Override @Override
@ -44,9 +52,9 @@ class LoginTask extends AsyncTask<String, String, String> {
protected String doInBackground(String... params) { protected String doInBackground(String... params) {
try { try {
if (twoFactorCode.isEmpty()) { if (twoFactorCode.isEmpty()) {
return app.getMWApi().login(username, password); return mwApi.login(username, password);
} else { } else {
return app.getMWApi().login(username, password, twoFactorCode); return mwApi.login(username, password, twoFactorCode);
} }
} catch (IOException e) { } catch (IOException e) {
// Do something better! // Do something better!
@ -59,11 +67,6 @@ class LoginTask extends AsyncTask<String, String, String> {
super.onPostExecute(result); super.onPostExecute(result);
Timber.d("Login done!"); Timber.d("Login done!");
EventLog.schema(CommonsApplication.EVENT_LOGIN_ATTEMPT)
.param("username", username)
.param("result", result)
.log();
if (result.equals("PASS")) { if (result.equals("PASS")) {
handlePassResult(); handlePassResult();
} else { } else {
@ -79,16 +82,16 @@ class LoginTask extends AsyncTask<String, String, String> {
Bundle extras = loginActivity.getIntent().getExtras(); Bundle extras = loginActivity.getIntent().getExtras();
if (extras != null) { if (extras != null) {
Timber.d("Bundle of extras: %s", extras); Timber.d("Bundle of extras: %s", extras);
response = extras.getParcelable(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE); response = extras.getParcelable(KEY_ACCOUNT_AUTHENTICATOR_RESPONSE);
if (response != null) { if (response != null) {
Bundle authResult = new Bundle(); Bundle authResult = new Bundle();
authResult.putString(AccountManager.KEY_ACCOUNT_NAME, username); authResult.putString(KEY_ACCOUNT_NAME, username);
authResult.putString(AccountManager.KEY_ACCOUNT_TYPE, AccountUtil.accountType()); authResult.putString(KEY_ACCOUNT_TYPE, ACCOUNT_TYPE);
response.onResult(authResult); response.onResult(authResult);
} }
} }
AccountUtil.createAccount(response, username, password); accountUtil.createAccount(response, username, password);
loginActivity.startMainActivity(); loginActivity.startMainActivity();
} }

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

View file

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

View file

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

View file

@ -1,10 +1,8 @@
package fr.free.nrw.commons.category; package fr.free.nrw.commons.category;
import android.content.ContentProviderClient; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.app.Fragment;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
@ -31,11 +29,14 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.data.Category; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.upload.MwVolleyApi; import fr.free.nrw.commons.upload.MwVolleyApi;
import fr.free.nrw.commons.utils.StringSortingUtils; import fr.free.nrw.commons.utils.StringSortingUtils;
import io.reactivex.Observable; import io.reactivex.Observable;
@ -45,12 +46,11 @@ import timber.log.Timber;
import static android.view.KeyEvent.ACTION_UP; import static android.view.KeyEvent.ACTION_UP;
import static android.view.KeyEvent.KEYCODE_BACK; import static android.view.KeyEvent.KEYCODE_BACK;
import static fr.free.nrw.commons.category.CategoryContentProvider.AUTHORITY;
/** /**
* Displays the category suggestion and selection screen. Category search is initiated here. * Displays the category suggestion and selection screen. Category search is initiated here.
*/ */
public class CategorizationFragment extends Fragment { public class CategorizationFragment extends CommonsDaggerSupportFragment {
public static final int SEARCH_CATS_LIMIT = 25; public static final int SEARCH_CATS_LIMIT = 25;
@ -65,16 +65,19 @@ public class CategorizationFragment extends Fragment {
@BindView(R.id.categoriesExplanation) @BindView(R.id.categoriesExplanation)
TextView categoriesSkip; TextView categoriesSkip;
@Inject MediaWikiApi mwApi;
@Inject @Named("default_preferences") SharedPreferences prefs;
@Inject CategoryDao categoryDao;
private RVRendererAdapter<CategoryItem> categoriesAdapter; private RVRendererAdapter<CategoryItem> categoriesAdapter;
private OnCategoriesSaveHandler onCategoriesSaveHandler; private OnCategoriesSaveHandler onCategoriesSaveHandler;
private HashMap<String, ArrayList<String>> categoriesCache; private HashMap<String, ArrayList<String>> categoriesCache;
private List<CategoryItem> selectedCategories = new ArrayList<>(); private List<CategoryItem> selectedCategories = new ArrayList<>();
private ContentProviderClient databaseClient;
private final CategoriesAdapterFactory adapterFactory = new CategoriesAdapterFactory(item -> { private final CategoriesAdapterFactory adapterFactory = new CategoriesAdapterFactory(item -> {
if (item.isSelected()) { if (item.isSelected()) {
selectedCategories.add(item); selectedCategories.add(item);
updateCategoryCount(item, databaseClient); updateCategoryCount(item);
} else { } else {
selectedCategories.remove(item); selectedCategories.remove(item);
} }
@ -88,13 +91,6 @@ public class CategorizationFragment extends Fragment {
categoriesList.setLayoutManager(new LinearLayoutManager(getContext())); categoriesList.setLayoutManager(new LinearLayoutManager(getContext()));
RxView.clicks(categoriesSkip)
.takeUntil(RxView.detaches(categoriesSkip))
.subscribe(o -> {
getActivity().onBackPressed();
getActivity().finish();
});
ArrayList<CategoryItem> items = new ArrayList<>(); ArrayList<CategoryItem> items = new ArrayList<>();
categoriesCache = new HashMap<>(); categoriesCache = new HashMap<>();
if (savedInstanceState != null) { if (savedInstanceState != null) {
@ -139,12 +135,6 @@ public class CategorizationFragment extends Fragment {
} }
} }
@Override
public void onDestroy() {
super.onDestroy();
databaseClient.release();
}
@Override @Override
public void onSaveInstanceState(Bundle outState) { public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
@ -180,7 +170,6 @@ public class CategorizationFragment extends Fragment {
setHasOptionsMenu(true); setHasOptionsMenu(true);
onCategoriesSaveHandler = (OnCategoriesSaveHandler) getActivity(); onCategoriesSaveHandler = (OnCategoriesSaveHandler) getActivity();
getActivity().setTitle(R.string.categories_activity_title); getActivity().setTitle(R.string.categories_activity_title);
databaseClient = getActivity().getContentResolver().acquireContentProviderClient(AUTHORITY);
} }
private void updateCategoryList(String filter) { private void updateCategoryList(String filter) {
@ -205,7 +194,9 @@ public class CategorizationFragment extends Fragment {
.sorted(sortBySimilarity(filter)) .sorted(sortBySimilarity(filter))
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe( .subscribe(
s -> categoriesAdapter.add(s), Timber::e, () -> { s -> categoriesAdapter.add(s),
Timber::e,
() -> {
categoriesAdapter.notifyDataSetChanged(); categoriesAdapter.notifyDataSetChanged();
categoriesSearchInProgress.setVisibility(View.GONE); categoriesSearchInProgress.setVisibility(View.GONE);
@ -253,16 +244,15 @@ public class CategorizationFragment extends Fragment {
private Observable<CategoryItem> titleCategories() { private Observable<CategoryItem> titleCategories() {
//Retrieve the title that was saved when user tapped submit icon //Retrieve the title that was saved when user tapped submit icon
SharedPreferences titleDesc = PreferenceManager.getDefaultSharedPreferences(getActivity()); String title = prefs.getString("Title", "");
String title = titleDesc.getString("Title", "");
return CommonsApplication.getInstance().getMWApi() return mwApi
.searchTitles(title, SEARCH_CATS_LIMIT) .searchTitles(title, SEARCH_CATS_LIMIT)
.map(name -> new CategoryItem(name, false)); .map(name -> new CategoryItem(name, false));
} }
private Observable<CategoryItem> recentCategories() { private Observable<CategoryItem> recentCategories() {
return Observable.fromIterable(Category.recentCategories(databaseClient, SEARCH_CATS_LIMIT)) return Observable.fromIterable(categoryDao.recentCategories(SEARCH_CATS_LIMIT))
.map(s -> new CategoryItem(s, false)); .map(s -> new CategoryItem(s, false));
} }
@ -279,7 +269,7 @@ public class CategorizationFragment extends Fragment {
} }
//otherwise, search API for matching categories //otherwise, search API for matching categories
return CommonsApplication.getInstance().getMWApi() return mwApi
.allCategories(term, SEARCH_CATS_LIMIT) .allCategories(term, SEARCH_CATS_LIMIT)
.map(name -> new CategoryItem(name, false)); .map(name -> new CategoryItem(name, false));
} }
@ -290,7 +280,7 @@ public class CategorizationFragment extends Fragment {
return Observable.empty(); return Observable.empty();
} }
return CommonsApplication.getInstance().getMWApi() return mwApi
.searchCategories(term, SEARCH_CATS_LIMIT) .searchCategories(term, SEARCH_CATS_LIMIT)
.map(s -> new CategoryItem(s, false)); .map(s -> new CategoryItem(s, false));
} }
@ -312,24 +302,16 @@ public class CategorizationFragment extends Fragment {
|| item.matches("(.*)needing(.*)") || item.matches("(.*)taken on(.*)")); || item.matches("(.*)needing(.*)") || item.matches("(.*)taken on(.*)"));
} }
private void updateCategoryCount(CategoryItem item, ContentProviderClient client) { private void updateCategoryCount(CategoryItem item) {
Category cat = lookupCategory(item.getName()); Category category = categoryDao.find(item.getName());
cat.incTimesUsed();
cat.save(client);
}
private Category lookupCategory(String name) { // Newly used category...
Category cat = Category.find(databaseClient, name); if (category == null) {
category = new Category(null, item.getName(), new Date(), 0);
if (cat == null) {
// Newly used category...
cat = new Category();
cat.setName(name);
cat.setLastUsed(new Date());
cat.setTimesUsed(0);
} }
return cat; category.incTimesUsed();
categoryDao.save(category);
} }
public int getCurrentSelectedCount() { public int getCurrentSelectedCount() {

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

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

View file

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

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

View file

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

View file

@ -11,8 +11,11 @@ import fr.free.nrw.commons.R;
class ContributionsListAdapter extends CursorAdapter { class ContributionsListAdapter extends CursorAdapter {
public ContributionsListAdapter(Context context, Cursor c, int flags) { private final ContributionDao contributionDao;
public ContributionsListAdapter(Context context, Cursor c, int flags, ContributionDao contributionDao) {
super(context, c, flags); super(context, c, flags);
this.contributionDao = contributionDao;
} }
@Override @Override
@ -26,7 +29,7 @@ class ContributionsListAdapter extends CursorAdapter {
@Override @Override
public void bindView(View view, Context context, Cursor cursor) { public void bindView(View view, Context context, Cursor cursor) {
final ContributionViewHolder views = (ContributionViewHolder)view.getTag(); final ContributionViewHolder views = (ContributionViewHolder)view.getTag();
final Contribution contribution = Contribution.fromCursor(cursor); final Contribution contribution = contributionDao.fromCursor(cursor);
views.imageView.setMedia(contribution); views.imageView.setMedia(contribution);
views.titleView.setText(contribution.getDisplayTitle()); views.titleView.setText(contribution.getDisplayTitle());

View file

@ -5,9 +5,7 @@ import android.content.SharedPreferences;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -22,21 +20,23 @@ import android.widget.ListAdapter;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.nearby.NearbyActivity; import fr.free.nrw.commons.nearby.NearbyActivity;
import timber.log.Timber; import timber.log.Timber;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE; import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
import static android.app.Activity.RESULT_OK; import static android.app.Activity.RESULT_OK;
import static android.content.Context.MODE_PRIVATE;
import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.view.View.GONE; import static android.view.View.GONE;
public class ContributionsListFragment extends Fragment { public class ContributionsListFragment extends CommonsDaggerSupportFragment {
@BindView(R.id.contributionsList) @BindView(R.id.contributionsList)
GridView contributionsList; GridView contributionsList;
@ -45,6 +45,9 @@ public class ContributionsListFragment extends Fragment {
@BindView(R.id.loadingContributionsProgressBar) @BindView(R.id.loadingContributionsProgressBar)
ProgressBar progressBar; ProgressBar progressBar;
@Inject @Named("prefs") SharedPreferences prefs;
@Inject @Named("default_preferences") SharedPreferences defaultPrefs;
private ContributionController controller; private ContributionController controller;
@Override @Override
@ -59,7 +62,6 @@ public class ContributionsListFragment extends Fragment {
} }
//TODO: Should this be in onResume? //TODO: Should this be in onResume?
SharedPreferences prefs = getActivity().getSharedPreferences("prefs", MODE_PRIVATE);
String lastModified = prefs.getString("lastSyncTimestamp", ""); String lastModified = prefs.getString("lastSyncTimestamp", "");
Timber.d("Last Sync Timestamp: %s", lastModified); Timber.d("Last Sync Timestamp: %s", lastModified);
@ -162,9 +164,7 @@ public class ContributionsListFragment extends Fragment {
return true; return true;
case R.id.menu_from_camera: case R.id.menu_from_camera:
SharedPreferences sharedPref = PreferenceManager boolean useExtStorage = defaultPrefs.getBoolean("useExternalStorage", true);
.getDefaultSharedPreferences(CommonsApplication.getInstance());
boolean useExtStorage = sharedPref.getBoolean("useExternalStorage", true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && useExtStorage) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && useExtStorage) {
// Here, thisActivity is the current activity // Here, thisActivity is the current activity
if (ContextCompat.checkSelfPermission(getActivity(), WRITE_EXTERNAL_STORAGE) if (ContextCompat.checkSelfPermission(getActivity(), WRITE_EXTERNAL_STORAGE)
@ -242,12 +242,17 @@ public class ContributionsListFragment extends Fragment {
menu.clear(); // See http://stackoverflow.com/a/8495697/17865 menu.clear(); // See http://stackoverflow.com/a/8495697/17865
inflater.inflate(R.menu.fragment_contributions_list, menu); inflater.inflate(R.menu.fragment_contributions_list, menu);
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext(); if (!deviceHasCamera()) {
if (!app.deviceHasCamera()) {
menu.findItem(R.id.menu_from_camera).setEnabled(false); menu.findItem(R.id.menu_from_camera).setEnabled(false);
} }
} }
public boolean deviceHasCamera() {
PackageManager pm = getContext().getPackageManager();
return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA) ||
pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT);
}
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);

View file

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

View file

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

View file

@ -1,16 +0,0 @@
package fr.free.nrw.commons.di;
import dagger.Module;
import dagger.android.ContributesAndroidInjector;
import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.nearby.NearbyActivity;
@Module
public abstract class ActivityBuilder {
@ContributesAndroidInjector()
abstract ContributionsActivity bindSplashScreenActivity();
@ContributesAndroidInjector()
abstract NearbyActivity bindNearbyActivity();
}

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

@ -1,29 +0,0 @@
package fr.free.nrw.commons.di;
import android.app.Application;
import javax.inject.Singleton;
import dagger.BindsInstance;
import dagger.Component;
import dagger.android.support.AndroidSupportInjectionModule;
import fr.free.nrw.commons.CommonsApplication;
@Singleton
@Component(modules = {
AndroidSupportInjectionModule.class,
AppModule.class,
ActivityBuilder.class
})
public interface AppComponent {
@Component.Builder
interface Builder {
@BindsInstance
Builder application(Application application);
AppComponent build();
}
void inject(CommonsApplication application);
}

View file

@ -1,29 +0,0 @@
package fr.free.nrw.commons.di;
import android.app.Application;
import android.content.Context;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
@Module
public class AppModule {
@Provides
@Singleton
Context provideContext(Application application) {
return application;
}
@Provides
@Singleton
public MediaWikiApi getMWApi() {
return new ApacheHttpClientMediaWikiApi(BuildConfig.WIKIMEDIA_API_HOST);
}
}

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

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,132 +1,156 @@
package fr.free.nrw.commons.location; package fr.free.nrw.commons.location;
import android.location.Location; import android.location.Location;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
public class LatLng { /**
* a latitude and longitude point with accuracy information, often of a picture
private final double latitude; */
private final double longitude; public class LatLng {
private final float accuracy;
private final double latitude;
/** Accepts latitude and longitude. private final double longitude;
* North and South values are cut off at 90° private final float accuracy;
*
* @param latitude double value /**
* @param longitude double value * Accepts latitude and longitude.
*/ * North and South values are cut off at 90°
public LatLng(double latitude, double longitude, float accuracy) { *
if (-180.0D <= longitude && longitude < 180.0D) { * @param latitude the latitude
this.longitude = longitude; * @param longitude the longitude
} else { * @param accuracy the accuracy
this.longitude = ((longitude - 180.0D) % 360.0D + 360.0D) % 360.0D - 180.0D; *
} * Examples:
this.latitude = Math.max(-90.0D, Math.min(90.0D, latitude)); * the Statue of Liberty is located at 40.69° N, 74.04° W
this.accuracy = accuracy; * The Statue of Liberty could be constructed as LatLng(40.69, -74.04, 1.0)
} * where positive signifies north, east and negative signifies south, west.
*/
public static LatLng from(@NonNull Location location) { public LatLng(double latitude, double longitude, float accuracy) {
return new LatLng(location.getLatitude(), location.getLongitude(), location.getAccuracy()); if (-180.0D <= longitude && longitude < 180.0D) {
} this.longitude = longitude;
} else {
public int hashCode() { this.longitude = ((longitude - 180.0D) % 360.0D + 360.0D) % 360.0D - 180.0D;
boolean var1 = true; }
byte var2 = 1; this.latitude = Math.max(-90.0D, Math.min(90.0D, latitude));
long var3 = Double.doubleToLongBits(this.latitude); this.accuracy = accuracy;
int var5 = 31 * var2 + (int)(var3 ^ var3 >>> 32); }
var3 = Double.doubleToLongBits(this.longitude);
var5 = 31 * var5 + (int)(var3 ^ var3 >>> 32); /**
return var5; * gets the latitude and longitude of a given non-null location
} * @param location the non-null location of the user
* @return LatLng the Latitude and Longitude of a given location
public boolean equals(Object o) { */
if (this == o) { public static LatLng from(@NonNull Location location) {
return true; return new LatLng(location.getLatitude(), location.getLongitude(), location.getAccuracy());
} else if (!(o instanceof LatLng)) { }
return false;
} else { /**
LatLng var2 = (LatLng)o; * creates a hash code for the longitude and longitude
return Double.doubleToLongBits(this.latitude) == Double.doubleToLongBits(var2.latitude) && Double.doubleToLongBits(this.longitude) == Double.doubleToLongBits(var2.longitude); */
} public int hashCode() {
} byte var1 = 1;
long var2 = Double.doubleToLongBits(this.latitude);
public String toString() { int var3 = 31 * var1 + (int)(var2 ^ var2 >>> 32);
return "lat/lng: (" + this.latitude + "," + this.longitude + ")"; var2 = Double.doubleToLongBits(this.longitude);
} var3 = 31 * var3 + (int)(var2 ^ var2 >>> 32);
return var3;
/** }
* Rounds the float to 4 digits and returns absolute value.
* /**
* @param coordinate A coordinate value as string. * checks for equality of two LatLng objects
* @return String of the rounded number. * @param o the second LatLng object
*/ */
private String formatCoordinate(double coordinate) { public boolean equals(Object o) {
double roundedNumber = Math.round(coordinate * 10000d) / 10000d; if (this == o) {
double absoluteNumber = Math.abs(roundedNumber); return true;
return String.valueOf(absoluteNumber); } else if (!(o instanceof LatLng)) {
} return false;
} else {
/** LatLng var2 = (LatLng)o;
* Returns "N" or "S" depending on the latitude. return Double.doubleToLongBits(this.latitude) == Double.doubleToLongBits(var2.latitude) && Double.doubleToLongBits(this.longitude) == Double.doubleToLongBits(var2.longitude);
* }
* @return "N" or "S". }
*/
private String getNorthSouth() { /**
if (this.latitude < 0) { * returns a string representation of the latitude and longitude
return "S"; */
} public String toString() {
return "lat/lng: (" + this.latitude + "," + this.longitude + ")";
return "N"; }
}
/**
/** * Rounds the float to 4 digits and returns absolute value.
* Returns "E" or "W" depending on the longitude. *
* * @param coordinate A coordinate value as string.
* @return "E" or "W". * @return String of the rounded number.
*/ */
private String getEastWest() { private String formatCoordinate(double coordinate) {
if (this.longitude >= 0 && this.longitude < 180) { double roundedNumber = Math.round(coordinate * 10000d) / 10000d;
return "E"; double absoluteNumber = Math.abs(roundedNumber);
} return String.valueOf(absoluteNumber);
}
return "W";
} /**
* Returns "N" or "S" depending on the latitude.
/** *
* Returns a nicely formatted coordinate string. Used e.g. in * @return "N" or "S".
* the detail view. */
* private String getNorthSouth() {
* @return The formatted string. if (this.latitude < 0) {
*/ return "S";
public String getPrettyCoordinateString() { }
return formatCoordinate(this.latitude) + " " + this.getNorthSouth() + ", "
+ formatCoordinate(this.longitude) + " " + this.getEastWest(); return "N";
} }
/** /**
* Return the location accuracy in meter. * Returns "E" or "W" depending on the longitude.
* *
* @return float * @return "E" or "W".
*/ */
public float getAccuracy() { private String getEastWest() {
return accuracy; if (this.longitude >= 0 && this.longitude < 180) {
} return "E";
}
/**
* Return the longitude in degrees. return "W";
* }
* @return double
*/ /**
public double getLongitude() { * Returns a nicely formatted coordinate string. Used e.g. in
return longitude; * the detail view.
} *
* @return The formatted string.
/** */
* Return the latitude in degrees. public String getPrettyCoordinateString() {
* return formatCoordinate(this.latitude) + " " + this.getNorthSouth() + ", "
* @return double + formatCoordinate(this.longitude) + " " + this.getEastWest();
*/ }
public double getLatitude() {
return latitude; /**
} * Return the location accuracy in meter.
} *
* @return float
*/
public float getAccuracy() {
return accuracy;
}
/**
* Return the longitude in degrees.
*
* @return double
*/
public double getLongitude() {
return longitude;
}
/**
* Return the latitude in degrees.
*
* @return double
*/
public double getLatitude() {
return latitude;
}
}

View file

@ -19,7 +19,6 @@ import javax.inject.Singleton;
import timber.log.Timber; import timber.log.Timber;
@Singleton
public class LocationServiceManager implements LocationListener { public class LocationServiceManager implements LocationListener {
public static final int LOCATION_REQUEST = 1; public static final int LOCATION_REQUEST = 1;
@ -32,21 +31,36 @@ public class LocationServiceManager implements LocationListener {
private final List<LocationUpdateListener> locationListeners = new CopyOnWriteArrayList<>(); private final List<LocationUpdateListener> locationListeners = new CopyOnWriteArrayList<>();
private boolean isLocationManagerRegistered = false; private boolean isLocationManagerRegistered = false;
@Inject /**
* Constructs a new instance of LocationServiceManager.
* @param context the context
*/
public LocationServiceManager(Context context) { public LocationServiceManager(Context context) {
this.context = context; this.context = context;
this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
} }
/**
* Returns the current status of the GPS provider.
* @return true if the GPS provider is enabled
*/
public boolean isProviderEnabled() { public boolean isProviderEnabled() {
return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER); return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
} }
/**
* Returns whether the location permission is granted.
* @return true if the location permission is granted
*/
public boolean isLocationPermissionGranted() { public boolean isLocationPermissionGranted() {
return ContextCompat.checkSelfPermission(context, return ContextCompat.checkSelfPermission(context,
Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED; Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED;
} }
/**
* Requests the location permission to be granted.
* @param activity the activity
*/
public void requestPermissions(Activity activity) { public void requestPermissions(Activity activity) {
if (activity.isFinishing()) { if (activity.isFinishing()) {
return; return;
@ -79,6 +93,11 @@ public class LocationServiceManager implements LocationListener {
&& requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER); && requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER);
} }
/**
* Requests location updates from the specified provider.
* @param locationProvider the location provider
* @return true if successful
*/
private boolean requestLocationUpdatesFromProvider(String locationProvider) { private boolean requestLocationUpdatesFromProvider(String locationProvider) {
try { try {
locationManager.requestLocationUpdates(locationProvider, locationManager.requestLocationUpdates(locationProvider,
@ -95,6 +114,12 @@ public class LocationServiceManager implements LocationListener {
} }
} }
/**
* Returns whether a given location is better than the current best location.
* @param location the location to be tested
* @param currentBestLocation the current best location
* @return true if the given location is better
*/
protected boolean isBetterLocation(Location location, Location currentBestLocation) { protected boolean isBetterLocation(Location location, Location currentBestLocation) {
if (currentBestLocation == null) { if (currentBestLocation == null) {
// A new location is always better than no location // A new location is always better than no location
@ -158,12 +183,20 @@ public class LocationServiceManager implements LocationListener {
} }
} }
/**
* Adds a new listener to the list of location listeners.
* @param listener the new listener
*/
public void addLocationListener(LocationUpdateListener listener) { public void addLocationListener(LocationUpdateListener listener) {
if (!locationListeners.contains(listener)) { if (!locationListeners.contains(listener)) {
locationListeners.add(listener); locationListeners.add(listener);
} }
} }
/**
* Removes a listener from the list of location listeners.
* @param listener the listener to be removed
*/
public void removeLocationListener(LocationUpdateListener listener) { public void removeLocationListener(LocationUpdateListener listener) {
locationListeners.remove(listener); locationListeners.remove(listener);
} }

View file

@ -6,7 +6,6 @@ import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@ -22,6 +21,9 @@ import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.Locale; import java.util.Locale;
import javax.inject.Inject;
import javax.inject.Provider;
import fr.free.nrw.commons.License; import fr.free.nrw.commons.License;
import fr.free.nrw.commons.LicenseList; import fr.free.nrw.commons.LicenseList;
import fr.free.nrw.commons.Media; import fr.free.nrw.commons.Media;
@ -29,11 +31,12 @@ import fr.free.nrw.commons.MediaDataExtractor;
import fr.free.nrw.commons.MediaWikiImageView; import fr.free.nrw.commons.MediaWikiImageView;
import fr.free.nrw.commons.PageTitle; import fr.free.nrw.commons.PageTitle;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.ui.widget.CompatTextView; import fr.free.nrw.commons.ui.widget.CompatTextView;
import timber.log.Timber; import timber.log.Timber;
public class MediaDetailFragment extends Fragment { public class MediaDetailFragment extends CommonsDaggerSupportFragment {
private boolean editable; private boolean editable;
private MediaDetailPagerFragment.MediaDetailProvider detailProvider; private MediaDetailPagerFragment.MediaDetailProvider detailProvider;
@ -53,6 +56,9 @@ public class MediaDetailFragment extends Fragment {
return mf; return mf;
} }
@Inject
Provider<MediaDataExtractor> mediaDataExtractorProvider;
private MediaWikiImageView image; private MediaWikiImageView image;
private MediaDetailSpacer spacer; private MediaDetailSpacer spacer;
private int initialListTop = 0; private int initialListTop = 0;
@ -69,7 +75,7 @@ public class MediaDetailFragment extends Fragment {
private boolean categoriesPresent = false; private boolean categoriesPresent = false;
private ViewTreeObserver.OnGlobalLayoutListener layoutListener; // for layout stuff, only used once! private ViewTreeObserver.OnGlobalLayoutListener layoutListener; // for layout stuff, only used once!
private ViewTreeObserver.OnScrollChangedListener scrollListener; private ViewTreeObserver.OnScrollChangedListener scrollListener;
DataSetObserver dataObserver; private DataSetObserver dataObserver;
private AsyncTask<Void,Void,Boolean> detailFetchTask; private AsyncTask<Void,Void,Boolean> detailFetchTask;
private LicenseList licenseList; private LicenseList licenseList;
@ -188,13 +194,13 @@ public class MediaDetailFragment extends Fragment {
@Override @Override
protected void onPreExecute() { protected void onPreExecute() {
extractor = new MediaDataExtractor(media.getFilename(), licenseList); extractor = mediaDataExtractorProvider.get();
} }
@Override @Override
protected Boolean doInBackground(Void... voids) { protected Boolean doInBackground(Void... voids) {
try { try {
extractor.fetch(); extractor.fetch(media.getFilename(), licenseList);
return Boolean.TRUE; return Boolean.TRUE;
} catch (IOException e) { } catch (IOException e) {
Timber.d(e); Timber.d(e);
@ -377,7 +383,7 @@ public class MediaDetailFragment extends Fragment {
private void openMap(LatLng coordinates) { private void openMap(LatLng coordinates) {
//Open map app at given position //Open map app at given position
Uri gmmIntentUri = Uri.parse( Uri gmmIntentUri = Uri.parse(
"geo:0,0?q=" + coordinates.getLatitude() + "," + coordinates.getLatitude()); "geo:0,0?q=" + coordinates.getLatitude() + "," + coordinates.getLongitude());
Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri); Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri);
if (mapIntent.resolveActivity(getActivity().getPackageManager()) != null) { if (mapIntent.resolveActivity(getActivity().getPackageManager()) != null) {

View file

@ -3,6 +3,7 @@ package fr.free.nrw.commons.media;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.DownloadManager; import android.app.DownloadManager;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences;
import android.database.DataSetObserver; import android.database.DataSetObserver;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
@ -24,20 +25,27 @@ import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import fr.free.nrw.commons.CommonsApplication; import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.Media; import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.mwapi.EventLog; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE; import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static android.content.Context.DOWNLOAD_SERVICE; import static android.content.Context.DOWNLOAD_SERVICE;
import static android.content.Intent.ACTION_VIEW; import static android.content.Intent.ACTION_VIEW;
import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static fr.free.nrw.commons.CommonsApplication.EVENT_SHARE_ATTEMPT;
public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPageChangeListener { public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment implements ViewPager.OnPageChangeListener {
@Inject MediaWikiApi mwApi;
@Inject SessionManager sessionManager;
@Inject @Named("default_preferences") SharedPreferences prefs;
private ViewPager pager; private ViewPager pager;
private Boolean editable; private Boolean editable;
@ -99,12 +107,7 @@ public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPa
Media m = provider.getMediaAtPosition(pager.getCurrentItem()); Media m = provider.getMediaAtPosition(pager.getCurrentItem());
switch (item.getItemId()) { switch (item.getItemId()) {
case R.id.menu_share_current_image: case R.id.menu_share_current_image:
// Share - this is just logs it, intent set in onCreateOptionsMenu, around line 252 // Share - intent set in onCreateOptionsMenu, around line 252
CommonsApplication app = (CommonsApplication) getActivity().getApplication();
EventLog.schema(EVENT_SHARE_ATTEMPT)
.param("username", app.getCurrentAccount().name)
.param("filename", m.getFilename())
.log();
return true; return true;
case R.id.menu_browser_current_image: case R.id.menu_browser_current_image:
// View in browser // View in browser
@ -161,9 +164,7 @@ public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPa
req.allowScanningByMediaScanner(); req.allowScanningByMediaScanner();
req.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); req.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !(ContextCompat.checkSelfPermission(getContext(), READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED)) {
&& !(ContextCompat.checkSelfPermission(getContext(),
READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED)) {
Snackbar.make(getView(), R.string.read_storage_permission_rationale, Snackbar.make(getView(), R.string.read_storage_permission_rationale,
Snackbar.LENGTH_INDEFINITE).setAction(R.string.ok, Snackbar.LENGTH_INDEFINITE).setAction(R.string.ok,
view -> ActivityCompat.requestPermissions(getActivity(), view -> ActivityCompat.requestPermissions(getActivity(),

View file

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

View file

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

View file

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

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 = null;
try {
ms = new ModifierSequence(Uri.parse(cursor.getString(1)),
new JSONObject(cursor.getString(2)));
} catch (JSONException e) {
throw new RuntimeException(e);
}
ms.setContentUri( ModificationsContentProvider.uriForId(cursor.getInt(0)));
return ms;
}
private JSONObject toJSON(ModifierSequence sequence) {
JSONObject data = new JSONObject();
try {
JSONArray modifiersJSON = new JSONArray();
for (PageModifier modifier: sequence.getModifiers()) {
modifiersJSON.put(modifier.toJSON());
}
data.put("modifiers", modifiersJSON);
return data;
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
private ContentValues toContentValues(ModifierSequence sequence) {
ContentValues cv = new ContentValues();
cv.put(Table.COLUMN_MEDIA_URI, sequence.getMediaUri().toString());
cv.put(Table.COLUMN_DATA, toJSON(sequence).toString());
return cv;
}
public static class Table {
static final String TABLE_NAME = "modifications";
static final String COLUMN_ID = "_id";
static final String COLUMN_MEDIA_URI = "mediauri";
static final String COLUMN_DATA = "data";
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
public static final String[] ALL_FIELDS = {
COLUMN_ID,
COLUMN_MEDIA_URI,
COLUMN_DATA
};
static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME;
static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
+ "_id INTEGER PRIMARY KEY,"
+ "mediauri STRING,"
+ "data STRING"
+ ");";
public static void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE_STATEMENT);
}
public static void onUpdate(SQLiteDatabase db, int from, int to) {
db.execSQL(DROP_TABLE_STATEMENT);
onCreate(db);
}
public static void onDelete(SQLiteDatabase db) {
db.execSQL(DROP_TABLE_STATEMENT);
onCreate(db);
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -28,8 +28,6 @@ import javax.inject.Inject;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import dagger.android.AndroidInjection;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.location.LocationServiceManager; import fr.free.nrw.commons.location.LocationServiceManager;
@ -48,12 +46,17 @@ import static fr.free.nrw.commons.location.LocationServiceManager.LOCATION_REQUE
public class NearbyActivity extends NavigationBaseActivity implements LocationUpdateListener { public class NearbyActivity extends NavigationBaseActivity implements LocationUpdateListener {
private static final int LOCATION_REQUEST = 1;
private static final String MAP_LAST_USED_PREFERENCE = "mapLastUsed";
@BindView(R.id.progressBar) @BindView(R.id.progressBar)
ProgressBar progressBar; ProgressBar progressBar;
private static final String MAP_LAST_USED_PREFERENCE = "mapLastUsed";
@Inject @Inject
LocationServiceManager locationManager; LocationServiceManager locationManager;
@Inject
NearbyController nearbyController;
private LatLng curLatLang; private LatLng curLatLang;
private Bundle bundle; private Bundle bundle;
private SharedPreferences sharedPreferences; private SharedPreferences sharedPreferences;
@ -64,7 +67,6 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
AndroidInjection.inject(this);
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
setContentView(R.layout.activity_nearby); setContentView(R.layout.activity_nearby);
ButterKnife.bind(this); ButterKnife.bind(this);
@ -279,20 +281,14 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
} }
progressBar.setVisibility(View.VISIBLE); progressBar.setVisibility(View.VISIBLE);
setupPlaceList(this); placesDisposable = Observable.fromCallable(() -> nearbyController
} .loadAttractionsFromLocation(curLatLang, this))
private void setupPlaceList(Context context) {
placesDisposable = Observable.fromCallable(() -> NearbyController
.loadAttractionsFromLocation(curLatLang, CommonsApplication.getInstance()))
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe((result) -> { .subscribe(this::populatePlaces);
populatePlaces(context, result);
});
} }
private void populatePlaces(Context context, List<Place> placeList) { private void populatePlaces(List<Place> placeList) {
Gson gson = new GsonBuilder() Gson gson = new GsonBuilder()
.registerTypeAdapter(Uri.class, new UriSerializer()) .registerTypeAdapter(Uri.class, new UriSerializer())
.create(); .create();
@ -301,7 +297,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
if (placeList.size() == 0) { if (placeList.size() == 0) {
int duration = Toast.LENGTH_SHORT; int duration = Toast.LENGTH_SHORT;
Toast toast = Toast.makeText(context, R.string.no_nearby, duration); Toast toast = Toast.makeText(this, R.string.no_nearby, duration);
toast.show(); toast.show();
} }

View file

@ -3,7 +3,6 @@ package fr.free.nrw.commons.nearby;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.preference.PreferenceManager;
import android.support.graphics.drawable.VectorDrawableCompat; import android.support.graphics.drawable.VectorDrawableCompat;
import com.mapbox.mapboxsdk.annotations.IconFactory; import com.mapbox.mapboxsdk.annotations.IconFactory;
@ -15,7 +14,9 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import fr.free.nrw.commons.CommonsApplication; import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.utils.UiUtils; import fr.free.nrw.commons.utils.UiUtils;
@ -24,9 +25,17 @@ import timber.log.Timber;
import static fr.free.nrw.commons.utils.LengthUtils.computeDistanceBetween; import static fr.free.nrw.commons.utils.LengthUtils.computeDistanceBetween;
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween; import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
public class NearbyController { public class NearbyController {
private static final int MAX_RESULTS = 1000; private static final int MAX_RESULTS = 1000;
private final NearbyPlaces nearbyPlaces;
private final SharedPreferences prefs;
@Inject
public NearbyController(NearbyPlaces nearbyPlaces,
@Named("default_preferences") SharedPreferences prefs) {
this.nearbyPlaces = nearbyPlaces;
this.prefs = prefs;
}
/** /**
* Prepares Place list to make their distance information update later. * Prepares Place list to make their distance information update later.
@ -34,13 +43,11 @@ public class NearbyController {
* @param context context * @param context context
* @return Place list without distance information * @return Place list without distance information
*/ */
public static List<Place> loadAttractionsFromLocation(LatLng curLatLng, Context context) { public List<Place> loadAttractionsFromLocation(LatLng curLatLng, Context context) {
Timber.d("Loading attractions near %s", curLatLng); Timber.d("Loading attractions near %s", curLatLng);
if (curLatLng == null) { if (curLatLng == null) {
return Collections.emptyList(); return Collections.emptyList();
} }
NearbyPlaces nearbyPlaces = CommonsApplication.getInstance().getNearbyPlaces();
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
List<Place> places = prefs.getBoolean("useWikidata", true) List<Place> places = prefs.getBoolean("useWikidata", true)
? nearbyPlaces.getFromWikidataQuery(curLatLng, Locale.getDefault().getLanguage()) ? nearbyPlaces.getFromWikidataQuery(curLatLng, Locale.getDefault().getLanguage())
: nearbyPlaces.getFromWikiNeedsPictures(); : nearbyPlaces.getFromWikiNeedsPictures();

View file

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

View file

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

View file

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

View file

@ -34,7 +34,7 @@ public class NearbyPlaces {
public NearbyPlaces() { public NearbyPlaces() {
try { try {
wikidataQuery = FileUtils.readFromResource("/assets/queries/nearby_query.rq"); wikidataQuery = FileUtils.readFromResource("/queries/nearby_query.rq");
Timber.v(wikidataQuery); Timber.v(wikidataQuery);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
@ -126,7 +126,7 @@ public class NearbyPlaces {
places.add(new Place( places.add(new Place(
name, name,
Place.Description.fromText(type), // list Place.Label.fromText(type), // list
type, // details type, // details
Uri.parse(icon), Uri.parse(icon),
new LatLng(latitude, longitude, 0), new LatLng(latitude, longitude, 0),
@ -188,7 +188,7 @@ public class NearbyPlaces {
places.add(new Place( places.add(new Place(
name, name,
Place.Description.fromText(type), // list Place.Label.fromText(type), // list
type, // details type, // details
null, null,
new LatLng(latitude, longitude, 0), new LatLng(latitude, longitude, 0),

View file

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

View file

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

View file

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

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

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

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,57 +1,70 @@
package fr.free.nrw.commons.settings; package fr.free.nrw.commons.settings;
import android.content.Context; import android.os.Bundle;
import android.content.Intent; import android.preference.PreferenceManager;
import android.os.Bundle; import android.support.v7.app.AppCompatDelegate;
import android.preference.PreferenceManager; import android.view.MenuItem;
import android.support.v7.app.AppCompatDelegate;
import android.view.MenuItem; import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
import butterknife.ButterKnife; import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.theme.NavigationBaseActivity; /**
* allows the user to change the settings
public class SettingsActivity extends NavigationBaseActivity { */
private AppCompatDelegate settingsDelegate; public class SettingsActivity extends NavigationBaseActivity {
private AppCompatDelegate settingsDelegate;
@Override
protected void onCreate(Bundle savedInstanceState) { /**
// Check prefs on every activity starts * to be called when the activity starts
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme",false)) { * @param savedInstanceState the previously saved state
setTheme(R.style.DarkAppTheme); */
} else { @Override
setTheme(R.style.LightAppTheme); protected void onCreate(Bundle savedInstanceState) {
} // Check prefs on every activity starts
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme",false)) {
super.onCreate(savedInstanceState); setTheme(R.style.DarkAppTheme);
setContentView(R.layout.activity_settings); } else {
setTheme(R.style.LightAppTheme);
ButterKnife.bind(this); }
initDrawer();
} super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
// Get an action bar
@Override ButterKnife.bind(this);
protected void onPostCreate(Bundle savedInstanceState) { initDrawer();
super.onPostCreate(savedInstanceState); }
if (settingsDelegate == null) {
settingsDelegate = AppCompatDelegate.create(this, null); // Get an action bar
} /**
settingsDelegate.onPostCreate(savedInstanceState); * takes care of actions taken after the creation has happened
* @param savedInstanceState the saved state
//Get an up button */
//settingsDelegate.getSupportActionBar().setDisplayHomeAsUpEnabled(true); @Override
} protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
//Handle action-bar clicks if (settingsDelegate == null) {
@Override settingsDelegate = AppCompatDelegate.create(this, null);
public boolean onOptionsItemSelected(MenuItem item) { }
switch (item.getItemId()) { settingsDelegate.onPostCreate(savedInstanceState);
case android.R.id.home:
finish(); //Get an up button
return true; //settingsDelegate.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
default: }
return super.onOptionsItemSelected(item);
} /**
} * Handle action-bar clicks
* @param item the selected item
* @return true on success, false on failure
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
} }

View file

@ -15,28 +15,37 @@ import android.preference.EditTextPreference;
import android.preference.ListPreference; import android.preference.ListPreference;
import android.preference.Preference; import android.preference.Preference;
import android.preference.PreferenceFragment; import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.support.v4.content.FileProvider; import android.support.v4.content.FileProvider;
import android.widget.Toast; import android.widget.Toast;
import java.io.File; import java.io.File;
import javax.inject.Inject;
import javax.inject.Named;
import dagger.android.AndroidInjection;
import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.utils.FileUtils; import fr.free.nrw.commons.utils.FileUtils;
public class SettingsFragment extends PreferenceFragment { public class SettingsFragment extends PreferenceFragment {
private static final int REQUEST_CODE_WRITE_EXTERNAL_STORAGE = 100; private static final int REQUEST_CODE_WRITE_EXTERNAL_STORAGE = 100;
@Inject @Named("default_preferences") SharedPreferences prefs;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
ApplicationlessInjection
.getInstance(getActivity().getApplicationContext())
.getCommonsApplicationComponent()
.inject(this);
// Load the preferences from an XML resource // Load the preferences from an XML resource
addPreferencesFromResource(R.xml.preferences); addPreferencesFromResource(R.xml.preferences);
@ -58,14 +67,12 @@ public class SettingsFragment extends PreferenceFragment {
}); });
final EditTextPreference uploadLimit = (EditTextPreference) findPreference("uploads"); final EditTextPreference uploadLimit = (EditTextPreference) findPreference("uploads");
final SharedPreferences sharedPref = PreferenceManager int uploads = prefs.getInt(Prefs.UPLOADS_SHOWING, 100);
.getDefaultSharedPreferences(CommonsApplication.getInstance());
int uploads = sharedPref.getInt(Prefs.UPLOADS_SHOWING, 100);
uploadLimit.setText(uploads + ""); uploadLimit.setText(uploads + "");
uploadLimit.setSummary(uploads + ""); uploadLimit.setSummary(uploads + "");
uploadLimit.setOnPreferenceChangeListener((preference, newValue) -> { uploadLimit.setOnPreferenceChangeListener((preference, newValue) -> {
int value = Integer.parseInt(newValue.toString()); int value = Integer.parseInt(newValue.toString());
final SharedPreferences.Editor editor = sharedPref.edit(); final SharedPreferences.Editor editor = prefs.edit();
if (value > 500) { if (value > 500) {
new AlertDialog.Builder(getActivity()) new AlertDialog.Builder(getActivity())
.setTitle(R.string.maximum_limit) .setTitle(R.string.maximum_limit)
@ -133,7 +140,7 @@ public class SettingsFragment extends PreferenceFragment {
Intent feedbackIntent = new Intent(Intent.ACTION_SEND); Intent feedbackIntent = new Intent(Intent.ACTION_SEND);
feedbackIntent.setType("message/rfc822"); feedbackIntent.setType("message/rfc822");
feedbackIntent.putExtra(Intent.EXTRA_EMAIL, feedbackIntent.putExtra(Intent.EXTRA_EMAIL,
new String[]{CommonsApplication.FEEDBACK_EMAIL}); new String[]{CommonsApplication.LOGS_PRIVATE_EMAIL});
feedbackIntent.putExtra(Intent.EXTRA_SUBJECT, feedbackIntent.putExtra(Intent.EXTRA_SUBJECT,
String.format(CommonsApplication.FEEDBACK_EMAIL_SUBJECT, String.format(CommonsApplication.FEEDBACK_EMAIL_SUBJECT,
BuildConfig.VERSION_NAME)); BuildConfig.VERSION_NAME));

View file

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

View file

@ -27,6 +27,7 @@ import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.nearby.NearbyActivity; import fr.free.nrw.commons.nearby.NearbyActivity;
import fr.free.nrw.commons.notification.NotificationActivity;
import fr.free.nrw.commons.settings.SettingsActivity; import fr.free.nrw.commons.settings.SettingsActivity;
import timber.log.Timber; import timber.log.Timber;
@ -62,10 +63,10 @@ public abstract class NavigationBaseActivity extends BaseActivity
private void setUserName() { private void setUserName() {
View navHeaderView = navigationView.getHeaderView(0); View navHeaderView = navigationView.getHeaderView(0);
TextView username = (TextView) navHeaderView.findViewById(R.id.username); TextView username = navHeaderView.findViewById(R.id.username);
AccountManager accountManager = AccountManager.get(this); AccountManager accountManager = AccountManager.get(this);
Account[] allAccounts = accountManager.getAccountsByType(AccountUtil.accountType()); Account[] allAccounts = accountManager.getAccountsByType(AccountUtil.ACCOUNT_TYPE);
if (allAccounts.length != 0) { if (allAccounts.length != 0) {
username.setText(allAccounts[0].name); username.setText(allAccounts[0].name);
} }
@ -143,6 +144,10 @@ public abstract class NavigationBaseActivity extends BaseActivity
.setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel()) .setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel())
.show(); .show();
return true; return true;
case R.id.action_notifications:
drawerLayout.closeDrawer(navigationView);
NotificationActivity.startYourself(this);
return true;
default: default:
Timber.e("Unknown option [%s] selected from the navigation menu", itemId); Timber.e("Unknown option [%s] selected from the navigation menu", itemId);
return false; return false;

View file

@ -1,73 +1,96 @@
package fr.free.nrw.commons.ui.widget; package fr.free.nrw.commons.ui.widget;
/** /**
* Created by mikel on 07/08/2017. * Created by mikel on 07/08/2017.
*/ */
import android.content.Context; import android.content.Context;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewCompat;
import android.support.v7.widget.AppCompatDrawableManager; import android.support.v7.widget.AppCompatDrawableManager;
import android.support.v7.widget.AppCompatTextView; import android.support.v7.widget.AppCompatTextView;
import android.util.AttributeSet; import android.util.AttributeSet;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.utils.UiUtils; import fr.free.nrw.commons.utils.UiUtils;
public class CompatTextView extends AppCompatTextView { /**
public CompatTextView(Context context) { * a text view compatible with older versions of the platform
super(context); */
init(null); public class CompatTextView extends AppCompatTextView {
}
/**
public CompatTextView(Context context, AttributeSet attrs) { * Constructs a new instance of CompatTextView
super(context, attrs); * @param context the view context
init(attrs); */
} public CompatTextView(Context context) {
super(context);
public CompatTextView(Context context, AttributeSet attrs, int defStyleAttr) { init(null);
super(context, attrs, defStyleAttr); }
init(attrs);
} /**
* Constructs a new instance of CompatTextView
private void init(@Nullable AttributeSet attrs) { * @param context the view context
if (attrs != null) { * @param attrs the set of attributes for the view
Context context = getContext(); */
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CompatTextView); public CompatTextView(Context context, AttributeSet attrs) {
super(context, attrs);
// Obtain DrawableManager used to pull Drawables safely, and check if we're in RTL init(attrs);
AppCompatDrawableManager dm = AppCompatDrawableManager.get(); }
boolean rtl = ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL;
/**
// Grab the compat drawable padding from the XML * Constructs a new instance of CompatTextView
float drawablePadding = a.getDimension(R.styleable.CompatTextView_drawablePadding, 0); * @param context
* @param attrs
// Grab the compat drawable resources from the XML * @param defStyleAttr
int startDrawableRes = a.getResourceId(R.styleable.CompatTextView_drawableStart, 0); */
int topDrawableRes = a.getResourceId(R.styleable.CompatTextView_drawableTop, 0); public CompatTextView(Context context, AttributeSet attrs, int defStyleAttr) {
int endDrawableRes = a.getResourceId(R.styleable.CompatTextView_drawableEnd, 0); super(context, attrs, defStyleAttr);
int bottomDrawableRes = a.getResourceId(R.styleable.CompatTextView_drawableBottom, 0); init(attrs);
}
// Load the used drawables, fall back to whatever was set in an "android:"
Drawable[] currentDrawables = getCompoundDrawables(); /**
Drawable left = startDrawableRes != 0 * initializes the view
? dm.getDrawable(context, startDrawableRes) : currentDrawables[0]; * @param attrs the attribute set of the view, which can be null
Drawable right = endDrawableRes != 0 */
? dm.getDrawable(context, endDrawableRes) : currentDrawables[1]; private void init(@Nullable AttributeSet attrs) {
Drawable top = topDrawableRes != 0 if (attrs != null) {
? dm.getDrawable(context, topDrawableRes) : currentDrawables[2]; Context context = getContext();
Drawable bottom = bottomDrawableRes != 0 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CompatTextView);
? dm.getDrawable(context, bottomDrawableRes) : currentDrawables[3];
// Obtain DrawableManager used to pull Drawables safely, and check if we're in RTL
// Account for RTL and apply the compound Drawables AppCompatDrawableManager dm = AppCompatDrawableManager.get();
Drawable start = rtl ? right : left; boolean rtl = ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL;
Drawable end = rtl ? left : right;
setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom); // Grab the compat drawable padding from the XML
setCompoundDrawablePadding((int) UiUtils.convertDpToPixel(drawablePadding, getContext())); float drawablePadding = a.getDimension(R.styleable.CompatTextView_drawablePadding, 0);
a.recycle(); // Grab the compat drawable resources from the XML
} int startDrawableRes = a.getResourceId(R.styleable.CompatTextView_drawableStart, 0);
} int topDrawableRes = a.getResourceId(R.styleable.CompatTextView_drawableTop, 0);
} int endDrawableRes = a.getResourceId(R.styleable.CompatTextView_drawableEnd, 0);
int bottomDrawableRes = a.getResourceId(R.styleable.CompatTextView_drawableBottom, 0);
// Load the used drawables, fall back to whatever was set in an "android:"
Drawable[] currentDrawables = getCompoundDrawables();
Drawable left = startDrawableRes != 0
? dm.getDrawable(context, startDrawableRes) : currentDrawables[0];
Drawable right = endDrawableRes != 0
? dm.getDrawable(context, endDrawableRes) : currentDrawables[1];
Drawable top = topDrawableRes != 0
? dm.getDrawable(context, topDrawableRes) : currentDrawables[2];
Drawable bottom = bottomDrawableRes != 0
? dm.getDrawable(context, bottomDrawableRes) : currentDrawables[3];
// Account for RTL and apply the compound Drawables
Drawable start = rtl ? right : left;
Drawable end = rtl ? left : right;
setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom);
setCompoundDrawablePadding((int) UiUtils.convertDpToPixel(drawablePadding, getContext()));
a.recycle();
}
}
}

View file

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

View file

@ -1,46 +1,69 @@
package fr.free.nrw.commons.ui.widget; package fr.free.nrw.commons.ui.widget;
import android.app.Dialog; import android.app.Dialog;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.ColorDrawable;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.v4.app.DialogFragment; import android.support.v4.app.DialogFragment;
import android.view.Gravity; import android.view.Gravity;
import android.view.View; import android.view.View;
import android.view.Window; import android.view.Window;
import android.view.WindowManager; import android.view.WindowManager;
public abstract class OverlayDialog extends DialogFragment { /**
* a formatted dialog fragment
@Override * This class is used by NearbyInfoDialog
public void onCreate(Bundle savedInstanceState) { */
super.onCreate(savedInstanceState); public abstract class OverlayDialog extends DialogFragment {
setStyle(STYLE_NO_FRAME, android.R.style.Theme_Holo_Light);
} /**
* creates a DialogFragment with the correct style and theme
@Override * @param savedInstanceState
public void onViewCreated(View view, Bundle savedInstanceState) { */
setDialogLayoutToFullScreen(); @Override
super.onViewCreated(view, savedInstanceState); public void onCreate(Bundle savedInstanceState) {
} super.onCreate(savedInstanceState);
setStyle(STYLE_NO_FRAME, android.R.style.Theme_Holo_Light);
private void setDialogLayoutToFullScreen() { }
Window window = getDialog().getWindow();
WindowManager.LayoutParams wlp = window.getAttributes(); /**
window.requestFeature(Window.FEATURE_NO_TITLE); * When the view is created, sets the dialog layout to full screen
wlp.gravity = Gravity.BOTTOM; *
wlp.width = WindowManager.LayoutParams.MATCH_PARENT; * @param view the view being used
wlp.height = WindowManager.LayoutParams.MATCH_PARENT; * @param savedInstanceState bundle re-constructed from a previous saved state
window.setAttributes(wlp); */
} @Override
public void onViewCreated(View view, Bundle savedInstanceState) {
@NonNull setDialogLayoutToFullScreen();
@Override super.onViewCreated(view, savedInstanceState);
public Dialog onCreateDialog(Bundle savedInstanceState) { }
Dialog dialog = super.onCreateDialog(savedInstanceState);
Window window = dialog.getWindow(); /**
window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); * sets the dialog layout to fullscreen
return dialog; */
} private void setDialogLayoutToFullScreen() {
} Window window = getDialog().getWindow();
WindowManager.LayoutParams wlp = window.getAttributes();
window.requestFeature(Window.FEATURE_NO_TITLE);
wlp.gravity = Gravity.BOTTOM;
wlp.width = WindowManager.LayoutParams.MATCH_PARENT;
wlp.height = WindowManager.LayoutParams.MATCH_PARENT;
window.setAttributes(wlp);
}
/**
* builds custom dialog container
*
* @param savedInstanceState the previously saved state
* @return the dialog
*/
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Dialog dialog = super.onCreateDialog(savedInstanceState);
Window window = dialog.getWindow();
window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
return dialog;
}
}

View file

@ -7,7 +7,6 @@ import android.support.v7.app.AlertDialog;
import java.io.IOException; import java.io.IOException;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.MediaWikiApi;
@ -18,6 +17,7 @@ import timber.log.Timber;
* Displays a warning to the user if the file already exists on Commons * Displays a warning to the user if the file already exists on Commons
*/ */
public class ExistingFileAsync extends AsyncTask<Void, Void, Boolean> { public class ExistingFileAsync extends AsyncTask<Void, Void, Boolean> {
interface Callback { interface Callback {
void onResult(Result result); void onResult(Result result);
} }
@ -28,14 +28,16 @@ public class ExistingFileAsync extends AsyncTask<Void, Void, Boolean> {
DUPLICATE_CANCELLED DUPLICATE_CANCELLED
} }
private final MediaWikiApi api;
private final String fileSha1; private final String fileSha1;
private final Context context; private final Context context;
private final Callback callback; private final Callback callback;
public ExistingFileAsync(String fileSha1, Context context, Callback callback) { public ExistingFileAsync(String fileSha1, Context context, Callback callback, MediaWikiApi mwApi) {
this.fileSha1 = fileSha1; this.fileSha1 = fileSha1;
this.context = context; this.context = context;
this.callback = callback; this.callback = callback;
this.api = mwApi;
} }
@Override @Override
@ -45,7 +47,6 @@ public class ExistingFileAsync extends AsyncTask<Void, Void, Boolean> {
@Override @Override
protected Boolean doInBackground(Void... voids) { protected Boolean doInBackground(Void... voids) {
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
// https://commons.wikimedia.org/w/api.php?action=query&list=allimages&format=xml&aisha1=801957214aba50cb63bb6eb1b0effa50188900ba // https://commons.wikimedia.org/w/api.php?action=query&list=allimages&format=xml&aisha1=801957214aba50cb63bb6eb1b0effa50188900ba
boolean fileExists; boolean fileExists;

View file

@ -49,18 +49,14 @@ public class FileUtils {
if ("primary".equalsIgnoreCase(type)) { if ("primary".equalsIgnoreCase(type)) {
return Environment.getExternalStorageDirectory() + "/" + split[1]; return Environment.getExternalStorageDirectory() + "/" + split[1];
} }
} } else if (isDownloadsDocument(uri)) { // DownloadsProvider
// DownloadsProvider
else if (isDownloadsDocument(uri)) {
final String id = DocumentsContract.getDocumentId(uri); final String id = DocumentsContract.getDocumentId(uri);
final Uri contentUri = ContentUris.withAppendedId( final Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
return getDataColumn(context, contentUri, null, null); return getDataColumn(context, contentUri, null, null);
} } else if (isMediaDocument(uri)) { // MediaProvider
// MediaProvider
else if (isMediaDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri); final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":"); final String[] split = docId.split(":");
final String type = split[0]; final String type = split[0];

View file

@ -8,7 +8,6 @@ import android.location.LocationListener;
import android.location.LocationManager; import android.location.LocationManager;
import android.media.ExifInterface; import android.media.ExifInterface;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi; import android.support.annotation.RequiresApi;
@ -16,7 +15,6 @@ import android.support.annotation.RequiresApi;
import java.io.FileDescriptor; import java.io.FileDescriptor;
import java.io.IOException; import java.io.IOException;
import fr.free.nrw.commons.CommonsApplication;
import timber.log.Timber; import timber.log.Timber;
/** /**
@ -26,6 +24,8 @@ import timber.log.Timber;
*/ */
public class GPSExtractor { public class GPSExtractor {
private final Context context;
private SharedPreferences prefs;
private ExifInterface exif; private ExifInterface exif;
private double decLatitude; private double decLatitude;
private double decLongitude; private double decLongitude;
@ -38,9 +38,12 @@ public class GPSExtractor {
/** /**
* Construct from the file descriptor of the image (only for API 24 or newer). * Construct from the file descriptor of the image (only for API 24 or newer).
* @param fileDescriptor the file descriptor of the image * @param fileDescriptor the file descriptor of the image
* @param context the context
*/ */
@RequiresApi(24) @RequiresApi(24)
public GPSExtractor(@NonNull FileDescriptor fileDescriptor) { public GPSExtractor(@NonNull FileDescriptor fileDescriptor, Context context, SharedPreferences prefs) {
this.context = context;
this.prefs = prefs;
try { try {
exif = new ExifInterface(fileDescriptor); exif = new ExifInterface(fileDescriptor);
} catch (IOException | IllegalArgumentException e) { } catch (IOException | IllegalArgumentException e) {
@ -51,13 +54,16 @@ public class GPSExtractor {
/** /**
* Construct from the file path of the image. * Construct from the file path of the image.
* @param path file path of the image * @param path file path of the image
* @param context the context
*/ */
public GPSExtractor(@NonNull String path) { public GPSExtractor(@NonNull String path, Context context, SharedPreferences prefs) {
this.prefs = prefs;
try { try {
exif = new ExifInterface(path); exif = new ExifInterface(path);
} catch (IOException | IllegalArgumentException e) { } catch (IOException | IllegalArgumentException e) {
Timber.w(e); Timber.w(e);
} }
this.context = context;
} }
/** /**
@ -65,9 +71,7 @@ public class GPSExtractor {
* @return true if enabled, false if disabled * @return true if enabled, false if disabled
*/ */
private boolean gpsPreferenceEnabled() { private boolean gpsPreferenceEnabled() {
SharedPreferences sharedPref boolean gpsPref = prefs.getBoolean("allowGps", false);
= PreferenceManager.getDefaultSharedPreferences(CommonsApplication.getInstance());
boolean gpsPref = sharedPref.getBoolean("allowGps", false);
Timber.d("Gps pref set to: %b", gpsPref); Timber.d("Gps pref set to: %b", gpsPref);
return gpsPref; return gpsPref;
} }
@ -76,8 +80,7 @@ public class GPSExtractor {
* Registers a LocationManager to listen for current location * Registers a LocationManager to listen for current location
*/ */
protected void registerLocationManager() { protected void registerLocationManager() {
locationManager = (LocationManager) CommonsApplication.getInstance() locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
.getSystemService(Context.LOCATION_SERVICE);
Criteria criteria = new Criteria(); Criteria criteria = new Criteria();
String provider = locationManager.getBestProvider(criteria, true); String provider = locationManager.getBestProvider(criteria, true);
myLocationListener = new MyLocationListener(); myLocationListener = new MyLocationListener();

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