├── .github └── workflows │ └── android.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── keystore.properties ├── src ├── androidTest │ └── java │ │ └── com │ │ └── gimranov │ │ └── zandy │ │ └── app │ │ ├── ApiTest.java │ │ ├── MainActivityLoggedInTest.java │ │ ├── MainActivityLoggedOutTest.java │ │ └── SyncTest.java └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── gimranov │ │ └── zandy │ │ └── app │ │ ├── AmazonZxingGlue.java │ │ ├── Application.java │ │ ├── AttachmentActivity.java │ │ ├── CollectionActivity.java │ │ ├── CollectionAdapter.kt │ │ ├── CollectionMembershipActivity.java │ │ ├── CreatorActivity.java │ │ ├── DrawerNavigationActivity.kt │ │ ├── FakeDataUtil.kt │ │ ├── ItemAction.kt │ │ ├── ItemActivity.java │ │ ├── ItemAdapter.kt │ │ ├── ItemDataActivity.java │ │ ├── ItemDisplayUtil.kt │ │ ├── ItemListingRule.kt │ │ ├── LookupActivity.java │ │ ├── MainActivity.java │ │ ├── NoteActivity.java │ │ ├── Persistence.java │ │ ├── Query.java │ │ ├── RequestActivity.java │ │ ├── ServerCredentials.java │ │ ├── SettingsActivity.java │ │ ├── SyncEvent.java │ │ ├── TagActivity.java │ │ ├── Util.kt │ │ ├── XMLResponseParser.java │ │ ├── data │ │ ├── Attachment.java │ │ ├── CollectionAdapter.java │ │ ├── Creator.java │ │ ├── Database.java │ │ ├── DatabaseAccess.kt │ │ ├── Item.java │ │ ├── ItemAdapter.java │ │ └── ItemCollection.java │ │ ├── storage │ │ └── StorageManager.java │ │ ├── task │ │ ├── APIEvent.java │ │ ├── APIException.java │ │ ├── APIRequest.java │ │ └── ZoteroAPITask.java │ │ ├── view │ │ └── CardViewModel.kt │ │ └── webdav │ │ └── WebDavTrust.java │ └── res │ ├── drawable-v21 │ ├── ic_menu_camera.xml │ ├── ic_menu_gallery.xml │ ├── ic_menu_manage.xml │ ├── ic_menu_send.xml │ ├── ic_menu_share.xml │ └── ic_menu_slideshow.xml │ ├── drawable │ ├── book.png │ ├── book_open.png │ ├── comment.png │ ├── email.png │ ├── film.png │ ├── folder.png │ ├── glyphish_02_redo.png │ ├── glyphish_104_index_cards.png │ ├── glyphish_106_sliders.png │ ├── glyphish_10_medical.png │ ├── glyphish_151_telescope.png │ ├── glyphish_195_barcode.png │ ├── glyphish_22_skull_n_bones.png │ ├── glyphish_59_flag.png │ ├── ic_launcher_background.xml │ ├── ic_launcher_foreground.xml │ ├── layout.png │ ├── list_child_indicator.xml │ ├── map.png │ ├── newspaper.png │ ├── note.png │ ├── page.png │ ├── page_white.png │ ├── page_white_acrobat.png │ ├── page_white_powerpoint.png │ ├── page_white_text.png │ ├── page_white_text_width.png │ ├── page_white_width.png │ ├── picture.png │ ├── report.png │ ├── report_user.png │ ├── script.png │ ├── side_nav_bar.xml │ ├── television.png │ └── zandy72.png │ ├── layout │ ├── activity_drawer_navigation.xml │ ├── app_bar_drawer_navigation.xml │ ├── collection_card.xml │ ├── collections.xml │ ├── content_drawer_navigation.xml │ ├── creator_dialog.xml │ ├── item_card.xml │ ├── items.xml │ ├── json.xml │ ├── list_attach.xml │ ├── list_collection.xml │ ├── list_data.xml │ ├── list_item.xml │ ├── lookup.xml │ ├── main.xml │ ├── nav_header_drawer_navigation.xml │ ├── note.xml │ ├── preferences.xml │ └── requests.xml │ ├── menu │ ├── activity_drawer_navigation_drawer.xml │ ├── drawer_navigation.xml │ ├── note_menu.xml │ └── zotero_menu.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── values-en │ └── strings.xml │ ├── values-es │ └── strings.xml │ ├── values-fr │ └── strings.xml │ ├── values-pt │ └── strings.xml │ ├── values-ru │ └── strings.xml │ ├── values │ ├── dimens.xml │ ├── drawables.xml │ ├── item_templates.xml │ └── strings.xml │ └── xml │ ├── searchable.xml │ └── settings.xml └── test.properties /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: set up JDK 1.8 17 | uses: actions/setup-java@v1 18 | with: 19 | java-version: 1.8 20 | - name: Grant execute permission for gradlew 21 | run: chmod +x gradlew 22 | - name: Build with Gradle 23 | run: ./gradlew assembleDebug 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | local.properties 3 | keystore 4 | keystore.properties 5 | .gradle 6 | build/ 7 | .idea 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zandy: a Zotero client for Android 2 | by Avram Lyon (ajlyon@gmail.com) 3 | 4 | ## Installation 5 | A built and signed version of Zandy is available for purchase on the Google Play (https://play.google.com/store/apps/details?id=com.gimranov.zandy.app). 6 | 7 | You can also check out this project and build an APK yourself: 8 | ``` 9 | ./gradlew installGoogleDebug 10 | ``` 11 | 12 | ## Requirements 13 | Android 4.3 or later. 14 | 15 | ## Support 16 | See the Zandy User Guide (http://www.gimranov.com/avram/w/zandy-user-guide) for basic documentation. Feature requests and bug reports are highly encouraged-- please post to the issue tracker on GitHub or to the Zandy user forum (http://www.gimranov.com/forum). Also feel free to write to zandy@gimranov.com with questions. 17 | 18 | ## License 19 | GNU Affero General Public License, Version 3 or later. 20 | 21 | This is based in part on the code by Martin Paul Eve (University of Sussex) to create an Android client for Mendeley, hosted at (https://code.google.com/p/mendeley-for-android/). That code is GPL-licensed, and the present code is licensed under the GPL-compatible Affero GPL. 22 | Icons from FamFamFam's Silk icon set (http://www.famfamfam.com/lab/icons/silk/) are used for item types and elsewhere. These icons are licensed under the Creative Commons Attribution 2.5 License. 23 | Icons from Glyphish's free icon set (http://glyphish.com/) are used for some buttons. They are licensed under the Creative Commons Attribution 3.0 United States License. To view a copy of this license, visit http://creativecommons.org/licenses/by/3.0/us/ or send a letter to Creative Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA. 24 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:4.0.2' 9 | //noinspection DifferentKotlinGradleVersion 10 | classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.20' 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | jcenter() 17 | } 18 | } 19 | 20 | 21 | repositories { 22 | google() 23 | jcenter() 24 | maven { url 'https://jitpack.io' } 25 | } 26 | 27 | apply plugin: 'com.android.application' 28 | apply plugin: 'kotlin-android' 29 | apply plugin: 'org.jetbrains.kotlin.android.extensions' 30 | apply plugin: 'kotlin-kapt' 31 | 32 | androidExtensions { 33 | experimental = true 34 | } 35 | 36 | dependencies { 37 | implementation('oauth.signpost:signpost-commonshttp4:1.2.1.2') { 38 | exclude group: 'org.apache.httpcomponents' 39 | } 40 | implementation 'commons-io:commons-io:2.6' 41 | implementation 'com.squareup:otto:1.3.8' 42 | implementation 'com.google.zxing:android-integration:3.3.0' 43 | 44 | implementation 'com.github.blocoio:faker:1.0.1' 45 | 46 | implementation 'androidx.annotation:annotation:1.1.0' 47 | 48 | implementation 'androidx.legacy:legacy-support-v4:1.0.0' 49 | implementation 'com.google.android.material:material:1.2.1' 50 | implementation 'androidx.appcompat:appcompat:1.2.0' 51 | implementation 'androidx.recyclerview:recyclerview:1.1.0' 52 | implementation 'androidx.cardview:cardview:1.0.0' 53 | implementation 'androidx.constraintlayout:constraintlayout:2.0.4' 54 | 55 | testImplementation 'junit:junit:4.13.1' 56 | 57 | // Required for instrumented tests 58 | androidTestImplementation 'androidx.annotation:annotation:1.1.0' 59 | 60 | androidTestImplementation('com.schibsted.spain:barista:2.7.1') { 61 | exclude group: 'com.android.support' 62 | exclude group: 'org.jetbrains.kotlin' // Only if you already use Kotlin in your project 63 | } 64 | 65 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' 66 | androidTestImplementation 'androidx.test.espresso:espresso-intents:3.3.0' 67 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' 68 | androidTestUtil 'androidx.test:orchestrator:1.3.0' 69 | 70 | } 71 | 72 | def keystorePropertiesFile = rootProject.file("keystore.properties") 73 | def keystoreProperties = new Properties() 74 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 75 | 76 | def testPropertiesFile = rootProject.file("test.properties") 77 | def testProperties = new Properties() 78 | testProperties.load(new FileInputStream(testPropertiesFile)) 79 | 80 | testProperties.putAll(gradle.getStartParameter().getProjectProperties()) 81 | 82 | android { 83 | compileSdkVersion 29 84 | buildToolsVersion "28.0.3" 85 | 86 | useLibrary 'org.apache.http.legacy' 87 | 88 | buildFeatures { 89 | dataBinding = true 90 | } 91 | 92 | defaultConfig { 93 | versionCode 1461 94 | versionName "1.4.6.1" 95 | 96 | minSdkVersion 18 97 | targetSdkVersion 29 98 | 99 | testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' 100 | } 101 | 102 | testOptions { 103 | execution 'ANDROIDX_TEST_ORCHESTRATOR' 104 | } 105 | 106 | 107 | signingConfigs { 108 | release { 109 | keyAlias keystoreProperties.getProperty('keyAlias', '') 110 | keyPassword keystoreProperties.getProperty('keyPassword', '') 111 | storeFile file(keystoreProperties.getProperty('storeFile', 'dummy')) 112 | storePassword keystoreProperties.getProperty('storePassword', '') 113 | } 114 | } 115 | 116 | buildTypes { 117 | release { 118 | zipAlignEnabled true 119 | signingConfig signingConfigs.release 120 | minifyEnabled true 121 | } 122 | 123 | debug { 124 | buildConfigField("String", "TEST_USER_ID", "\"${testProperties.testUserId}\"") 125 | buildConfigField("String", "TEST_USER_KEY_READONLY", "\"${testProperties.testUserKeyReadonly}\"") 126 | buildConfigField("String", "FLAVOR", "\"\"") 127 | } 128 | } 129 | 130 | lintOptions { 131 | abortOnError false 132 | } 133 | 134 | packagingOptions { 135 | exclude 'META-INF/LICENSE.txt' 136 | } 137 | } 138 | 139 | apply plugin: 'kotlin-android-extensions' -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | android.enableJetifier=true 2 | android.useAndroidX=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avram/zandy/bce70c1b1dd83b6ddc4592cb6e503a4add313f64/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Mar 14 23:37:06 PDT 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https://services.gradle.org/distributions/gradle-6.7.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /keystore.properties: -------------------------------------------------------------------------------- 1 | # Put keys here 2 | -------------------------------------------------------------------------------- /src/androidTest/java/com/gimranov/zandy/app/ApiTest.java: -------------------------------------------------------------------------------- 1 | package com.gimranov.zandy.app; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.preference.PreferenceManager; 6 | import androidx.test.filters.SmallTest; 7 | import androidx.test.ext.junit.runners.AndroidJUnit4; 8 | 9 | import com.gimranov.zandy.app.data.Database; 10 | import com.gimranov.zandy.app.task.APIException; 11 | import com.gimranov.zandy.app.task.APIRequest; 12 | 13 | import org.junit.Before; 14 | import org.junit.Test; 15 | import org.junit.runner.RunWith; 16 | 17 | import static androidx.test.core.app.ApplicationProvider.getApplicationContext; 18 | import static junit.framework.TestCase.assertTrue; 19 | import static junit.framework.TestCase.fail; 20 | 21 | 22 | @RunWith(AndroidJUnit4.class) 23 | @SmallTest 24 | public class ApiTest { 25 | 26 | private Context mContext; 27 | private Database mDb; 28 | private ServerCredentials mCred; 29 | 30 | /** 31 | * Access information for the Zandy test user on Zotero.org 32 | */ 33 | private static final String TEST_UID = BuildConfig.TEST_USER_ID; 34 | private static final String TEST_KEY = BuildConfig.TEST_USER_KEY_READONLY; 35 | private static final String TEST_COLLECTION = "U8GNSSF3"; 36 | 37 | // unlikely to exist 38 | private static final String TEST_MISSING_ITEM = "ZZZZZZZZ"; 39 | 40 | @Before 41 | public void setUp() { 42 | mContext = getApplicationContext(); 43 | mDb = new Database(mContext); 44 | 45 | SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mContext); 46 | SharedPreferences.Editor editor = settings.edit(); 47 | // For Zotero, the key and secret are identical, it seems 48 | editor.putString("user_key", TEST_KEY); 49 | editor.putString("user_secret", TEST_KEY); 50 | editor.putString("user_id", TEST_UID); 51 | editor.commit(); 52 | 53 | mCred = new ServerCredentials(mContext); 54 | } 55 | 56 | @Test 57 | public void testPreConditions() { 58 | // Make sure we do indeed have the key set up 59 | assertTrue(ServerCredentials.check(mContext)); 60 | } 61 | 62 | @Test 63 | public void testItemsRequest() throws APIException { 64 | APIRequest items = APIRequest.fetchItems(false, mCred); 65 | items.issue(mDb, mCred); 66 | } 67 | 68 | @Test 69 | public void testCollectionsRequest() throws APIException { 70 | APIRequest collections = APIRequest.fetchCollections(mCred); 71 | collections.issue(mDb, mCred); 72 | } 73 | 74 | @Test 75 | public void testItemsForCollection() throws APIException { 76 | APIRequest collection = APIRequest.fetchItems(TEST_COLLECTION, false, mCred); 77 | collection.issue(mDb, mCred); 78 | } 79 | 80 | // verify that we fail on this item, which should be missing 81 | public void testMissingItem() throws APIException { 82 | APIRequest missingItem = APIRequest.fetchItem(TEST_MISSING_ITEM, mCred); 83 | try { 84 | missingItem.issue(mDb, mCred); 85 | // We shouldn't get here 86 | fail(); 87 | } catch (APIException e) { 88 | // We expect only one specific exception message 89 | if (!"Item does not exist".equals(e.getCause().getMessage())) 90 | throw e; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/androidTest/java/com/gimranov/zandy/app/MainActivityLoggedInTest.java: -------------------------------------------------------------------------------- 1 | package com.gimranov.zandy.app; 2 | 3 | import android.content.SharedPreferences; 4 | import android.preference.PreferenceManager; 5 | 6 | import androidx.test.espresso.intent.rule.IntentsTestRule; 7 | import androidx.test.ext.junit.runners.AndroidJUnit4; 8 | import androidx.test.filters.LargeTest; 9 | 10 | import com.gimranov.zandy.app.data.Database; 11 | 12 | import org.junit.After; 13 | import org.junit.Before; 14 | import org.junit.Rule; 15 | import org.junit.Test; 16 | import org.junit.runner.RunWith; 17 | 18 | import static androidx.test.core.app.ApplicationProvider.getApplicationContext; 19 | import static androidx.test.espresso.Espresso.onView; 20 | import static androidx.test.espresso.assertion.ViewAssertions.matches; 21 | import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; 22 | import static androidx.test.espresso.matcher.ViewMatchers.withId; 23 | import static org.hamcrest.Matchers.not; 24 | 25 | @RunWith(AndroidJUnit4.class) 26 | @LargeTest 27 | public class MainActivityLoggedInTest { 28 | @Rule 29 | public IntentsTestRule activityTestRule = 30 | new IntentsTestRule(MainActivity.class){ 31 | @Override 32 | protected void beforeActivityLaunched() { 33 | super.beforeActivityLaunched(); 34 | SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); 35 | preferences.edit() 36 | .putString("user_id", BuildConfig.TEST_USER_ID) 37 | .putString("user_key", BuildConfig.TEST_USER_KEY_READONLY) 38 | .commit(); 39 | } 40 | }; 41 | 42 | @Before 43 | public void setUpCredentials() { 44 | SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); 45 | preferences.edit() 46 | .putString("user_id", BuildConfig.TEST_USER_ID) 47 | .putString("user_key", BuildConfig.TEST_USER_KEY_READONLY) 48 | .commit(); 49 | } 50 | 51 | @After 52 | public void clearCredentials() { 53 | SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); 54 | preferences.edit().clear().commit(); 55 | } 56 | 57 | @Before 58 | @After 59 | public void clearDatabase() { 60 | Database database = new Database(getApplicationContext()); 61 | database.resetAllData(); 62 | } 63 | 64 | @Test 65 | public void loginButtonDoesNotShow() { 66 | onView(withId(R.id.loginButton)).check(matches(not(isDisplayed()))); 67 | } 68 | } -------------------------------------------------------------------------------- /src/androidTest/java/com/gimranov/zandy/app/MainActivityLoggedOutTest.java: -------------------------------------------------------------------------------- 1 | package com.gimranov.zandy.app; 2 | 3 | import android.content.Intent; 4 | import android.net.Uri; 5 | import androidx.test.espresso.intent.rule.IntentsTestRule; 6 | import androidx.test.filters.LargeTest; 7 | import androidx.test.ext.junit.runners.AndroidJUnit4; 8 | 9 | import org.hamcrest.BaseMatcher; 10 | import org.hamcrest.Description; 11 | import org.junit.Rule; 12 | import org.junit.Test; 13 | import org.junit.runner.RunWith; 14 | 15 | import static androidx.test.espresso.Espresso.onView; 16 | import static androidx.test.espresso.action.ViewActions.click; 17 | import static androidx.test.espresso.intent.Intents.intended; 18 | import static androidx.test.espresso.intent.matcher.ComponentNameMatchers.hasClassName; 19 | import static androidx.test.espresso.intent.matcher.IntentMatchers.hasAction; 20 | import static androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent; 21 | import static androidx.test.espresso.intent.matcher.IntentMatchers.hasData; 22 | import static androidx.test.espresso.matcher.ViewMatchers.withId; 23 | import static com.schibsted.spain.barista.assertion.BaristaVisibilityAssertions.assertDisplayed; 24 | import static org.hamcrest.Matchers.allOf; 25 | 26 | @RunWith(AndroidJUnit4.class) 27 | @LargeTest 28 | public class MainActivityLoggedOutTest { 29 | @Rule 30 | public IntentsTestRule activityTestRule = 31 | new IntentsTestRule<>(MainActivity.class); 32 | 33 | @Test 34 | public void loginButtonShowsOnLaunch() { 35 | assertDisplayed(R.id.loginButton); 36 | } 37 | 38 | @Test 39 | public void loginButtonLaunchesOauth() throws Exception { 40 | onView(withId(R.id.loginButton)).perform(click()); 41 | Thread.sleep(1000); 42 | intended(allOf(hasAction(Intent.ACTION_VIEW), hasData(new BaseMatcher() { 43 | @Override 44 | public boolean matches(Object item) { 45 | return ((Uri) item).getHost().contains("zotero.org"); 46 | } 47 | 48 | @Override 49 | public void describeTo(Description description) { 50 | description.appendText("should have host zotero.org"); 51 | } 52 | }))); 53 | } 54 | 55 | @Test 56 | public void viewCollectionsLaunchesActivity() throws Exception { 57 | onView(withId(R.id.collectionButton)).perform(click()); 58 | intended(hasComponent(hasClassName(CollectionActivity.class.getName()))); 59 | } 60 | } -------------------------------------------------------------------------------- /src/androidTest/java/com/gimranov/zandy/app/SyncTest.java: -------------------------------------------------------------------------------- 1 | package com.gimranov.zandy.app; 2 | 3 | 4 | import android.content.SharedPreferences; 5 | import android.preference.PreferenceManager; 6 | import androidx.test.espresso.ViewInteraction; 7 | import androidx.test.filters.LargeTest; 8 | import androidx.test.rule.ActivityTestRule; 9 | import androidx.test.ext.junit.runners.AndroidJUnit4; 10 | import android.view.View; 11 | import android.view.ViewGroup; 12 | import android.view.ViewParent; 13 | import com.gimranov.zandy.app.data.Database; 14 | import org.hamcrest.Description; 15 | import org.hamcrest.Matcher; 16 | import org.hamcrest.TypeSafeMatcher; 17 | import org.junit.After; 18 | import org.junit.Before; 19 | import org.junit.Rule; 20 | import org.junit.Test; 21 | import org.junit.runner.RunWith; 22 | 23 | import static androidx.test.core.app.ApplicationProvider.getApplicationContext; 24 | import static androidx.test.espresso.Espresso.onView; 25 | import static androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu; 26 | import static androidx.test.espresso.action.ViewActions.click; 27 | import static androidx.test.espresso.assertion.ViewAssertions.matches; 28 | import static androidx.test.espresso.matcher.RootMatchers.withDecorView; 29 | import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; 30 | import static androidx.test.espresso.matcher.ViewMatchers.withClassName; 31 | import static androidx.test.espresso.matcher.ViewMatchers.withId; 32 | import static androidx.test.espresso.matcher.ViewMatchers.withText; 33 | import static org.hamcrest.Matchers.allOf; 34 | import static org.hamcrest.Matchers.is; 35 | import static org.hamcrest.Matchers.not; 36 | 37 | @LargeTest 38 | @RunWith(AndroidJUnit4.class) 39 | public class SyncTest { 40 | 41 | @Rule 42 | public ActivityTestRule mActivityTestRule = new ActivityTestRule<>(MainActivity.class); 43 | 44 | @Before 45 | public void setUpCredentials() { 46 | SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); 47 | preferences.edit() 48 | .putString("user_id", BuildConfig.TEST_USER_ID) 49 | .putString("user_key", BuildConfig.TEST_USER_KEY_READONLY) 50 | .commit(); 51 | } 52 | 53 | @After 54 | public void clearCredentials() { 55 | SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); 56 | preferences.edit().clear().commit(); 57 | } 58 | 59 | @Before 60 | @After 61 | public void clearDatabase() { 62 | Database database = new Database(getApplicationContext()); 63 | database.resetAllData(); 64 | } 65 | 66 | @Test 67 | public void syncTest() { 68 | openActionBarOverflowOrOptionsMenu(getApplicationContext()); 69 | 70 | ViewInteraction textView = onView( 71 | allOf(withId(android.R.id.title), withText(R.string.menu_sync), 72 | childAtPosition( 73 | childAtPosition( 74 | withClassName(is("com.android.internal.view.menu.ListMenuItemView")), 75 | 0), 76 | 0), 77 | isDisplayed())); 78 | textView.perform(click()); 79 | 80 | onView(withText(R.string.sync_started)) 81 | .inRoot(withDecorView(not(is(mActivityTestRule.getActivity().getWindow().getDecorView())))) 82 | .check(matches(isDisplayed())); 83 | 84 | ViewInteraction button2 = onView( 85 | allOf(withId(R.id.itemButton), withText(R.string.view_items), 86 | childAtPosition( 87 | allOf(withId(R.id.main), 88 | childAtPosition( 89 | withId(android.R.id.content), 90 | 0)), 91 | 1), 92 | isDisplayed())); 93 | button2.perform(click()); 94 | } 95 | 96 | private static Matcher childAtPosition( 97 | final Matcher parentMatcher, final int position) { 98 | 99 | return new TypeSafeMatcher() { 100 | @Override 101 | public void describeTo(Description description) { 102 | description.appendText("Child at position " + position + " in parent "); 103 | parentMatcher.describeTo(description); 104 | } 105 | 106 | @Override 107 | public boolean matchesSafely(View view) { 108 | ViewParent parent = view.getParent(); 109 | return parent instanceof ViewGroup && parentMatcher.matches(parent) 110 | && view.equals(((ViewGroup) parent).getChildAt(position)); 111 | } 112 | }; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 15 | 16 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 47 | 51 | 52 | 53 | 54 | 55 | 58 | 59 | 63 | 67 | 71 | 75 | 79 | 83 | 87 | 91 | 95 | 96 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /src/main/java/com/gimranov/zandy/app/AmazonZxingGlue.java: -------------------------------------------------------------------------------- 1 | package com.gimranov.zandy.app; 2 | 3 | import android.app.AlertDialog; 4 | import android.content.ActivityNotFoundException; 5 | import android.content.Context; 6 | import android.content.DialogInterface; 7 | import android.content.Intent; 8 | import android.net.Uri; 9 | import android.util.Log; 10 | 11 | import com.google.zxing.integration.android.IntentIntegrator; 12 | 13 | public class AmazonZxingGlue { 14 | private static final String TAG = AmazonZxingGlue.class.getSimpleName(); 15 | 16 | public static AlertDialog showDownloadDialog(final Context activity) { 17 | AlertDialog.Builder downloadDialog = new AlertDialog.Builder(activity); 18 | downloadDialog.setTitle(IntentIntegrator.DEFAULT_TITLE); 19 | downloadDialog.setMessage(IntentIntegrator.DEFAULT_MESSAGE); 20 | downloadDialog.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { 21 | @Override 22 | public void onClick(DialogInterface dialogInterface, int i) { 23 | Uri uri = Uri.parse("amzn://apps/android?asin=B004R1FCII"); 24 | Intent intent = new Intent(Intent.ACTION_VIEW, uri); 25 | try { 26 | activity.startActivity(intent); 27 | } catch (ActivityNotFoundException anfe) { 28 | // Hmm, market is not installed 29 | Log.w(TAG, "Amazon is not installed; cannot install"); 30 | } 31 | } 32 | }); 33 | downloadDialog.setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() { 34 | @Override 35 | public void onClick(DialogInterface dialogInterface, int i) {} 36 | }); 37 | return downloadDialog.show(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/gimranov/zandy/app/Application.java: -------------------------------------------------------------------------------- 1 | package com.gimranov.zandy.app; 2 | 3 | import com.squareup.otto.Bus; 4 | 5 | public class Application extends android.app.Application { 6 | private static final String TAG = Application.class.getSimpleName(); 7 | 8 | private static Application instance; 9 | 10 | private Bus bus; 11 | 12 | @Override 13 | public void onCreate() { 14 | super.onCreate(); 15 | 16 | bus = new Bus(); 17 | 18 | instance = this; 19 | } 20 | 21 | public Bus getBus() { 22 | return bus; 23 | } 24 | 25 | public static Application getInstance() { 26 | return instance; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/gimranov/zandy/app/CollectionAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.gimranov.zandy.app 2 | 3 | import androidx.recyclerview.widget.RecyclerView 4 | import android.view.LayoutInflater 5 | import android.view.ViewGroup 6 | import com.gimranov.zandy.app.data.Database 7 | import com.gimranov.zandy.app.data.DatabaseAccess 8 | import com.gimranov.zandy.app.data.ItemCollection 9 | import com.gimranov.zandy.app.databinding.CollectionCardBinding 10 | import kotlinx.android.synthetic.main.collection_card.view.* 11 | 12 | class CollectionAdapter(val database: Database, 13 | itemListingRule: ItemListingRule, 14 | private val onNavigate: (ItemCollection, ItemAction) -> Unit) : RecyclerView.Adapter() { 15 | 16 | val cursor = when (itemListingRule) { 17 | is AllItems -> DatabaseAccess.collections(database) 18 | is Children -> when (itemListingRule.parent) { 19 | null -> DatabaseAccess.collections(database) 20 | else -> DatabaseAccess.collectionsForParent(database, itemListingRule.parent) 21 | } 22 | is SearchResults -> TODO("No collection searches") 23 | } 24 | 25 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemCollectionViewHolder { 26 | val layoutInflater = LayoutInflater.from(parent.context) 27 | val cardBinding = CollectionCardBinding.inflate(layoutInflater, parent, false) 28 | 29 | return ItemCollectionViewHolder(cardBinding, onNavigate) 30 | } 31 | 32 | override fun onBindViewHolder(holder: ItemCollectionViewHolder, position: Int) { 33 | if (cursor?.moveToPosition(position) != true) { 34 | return 35 | } 36 | 37 | holder.bind(ItemCollection.load(cursor)) 38 | } 39 | 40 | override fun onViewRecycled(holder: ItemCollectionViewHolder) { 41 | } 42 | 43 | override fun getItemCount(): Int { 44 | return cursor?.count ?: 0 45 | } 46 | 47 | class ItemCollectionViewHolder(cardBinding: CollectionCardBinding, 48 | private val onNavigate: (ItemCollection, ItemAction) -> Unit) : RecyclerView.ViewHolder(cardBinding.root) { 49 | 50 | private val binding = cardBinding 51 | 52 | fun bind(itemCollection: ItemCollection) { 53 | binding.collection = itemCollection 54 | binding.root.card_collection.setOnClickListener { onNavigate(binding.collection!!, ItemAction.VIEW) } 55 | binding.executePendingBindings() 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/main/java/com/gimranov/zandy/app/DrawerNavigationActivity.kt: -------------------------------------------------------------------------------- 1 | package com.gimranov.zandy.app 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import androidx.core.view.GravityCompat 6 | import androidx.appcompat.app.ActionBarDrawerToggle 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.recyclerview.widget.LinearLayoutManager 9 | import android.view.Menu 10 | import android.view.MenuItem 11 | import com.gimranov.zandy.app.data.Database 12 | import com.gimranov.zandy.app.data.Item 13 | import com.gimranov.zandy.app.data.ItemCollection 14 | import kotlinx.android.synthetic.main.activity_drawer_navigation.* 15 | import kotlinx.android.synthetic.main.app_bar_drawer_navigation.* 16 | import kotlinx.android.synthetic.main.content_drawer_navigation.* 17 | 18 | class DrawerNavigationActivity : AppCompatActivity() { 19 | 20 | private val collectionKey = "com.gimranov.zandy.app.collectionKey" 21 | private val database = Database(this) 22 | 23 | override fun onCreate(savedInstanceState: Bundle?) { 24 | super.onCreate(savedInstanceState) 25 | setContentView(R.layout.activity_drawer_navigation) 26 | setSupportActionBar(toolbar) 27 | } 28 | 29 | override fun onResume() { 30 | super.onResume() 31 | 32 | val itemListingRule = intent?.extras?.getString(collectionKey)?.let { 33 | Children(ItemCollection.load(it, database), true) 34 | } ?: AllItems 35 | 36 | title = when (itemListingRule::class) { 37 | Children::class -> { 38 | (itemListingRule as Children).parent?.title ?: this.getString(R.string.all_items) 39 | } 40 | else -> { 41 | this.getString(R.string.all_items) 42 | } 43 | } 44 | 45 | val itemAdapter = ItemAdapter(database, itemListingRule) { item: Item, itemAction: ItemAction -> 46 | run { 47 | when (itemAction) { 48 | ItemAction.EDIT -> { 49 | val i = Intent(baseContext, ItemDataActivity::class.java) 50 | i.putExtra("com.gimranov.zandy.app.itemKey", item.key) 51 | i.putExtra("com.gimranov.zandy.app.itemDbId", item.dbId) 52 | startActivity(i) 53 | } 54 | ItemAction.ORGANIZE -> { 55 | val i = Intent(baseContext, CollectionMembershipActivity::class.java) 56 | i.putExtra("com.gimranov.zandy.app.itemKey", item.key) 57 | startActivity(i) 58 | } 59 | ItemAction.VIEW -> TODO() 60 | } 61 | } 62 | } 63 | 64 | val collectionAdapter = CollectionAdapter(database, itemListingRule) { collection: ItemCollection, itemAction: ItemAction -> 65 | run { 66 | when (itemAction) { 67 | ItemAction.VIEW -> { 68 | val i = Intent(baseContext, DrawerNavigationActivity::class.java) 69 | i.putExtra(collectionKey, collection.key) 70 | startActivity(i) 71 | } 72 | ItemAction.EDIT -> TODO() 73 | ItemAction.ORGANIZE -> TODO() 74 | } 75 | } 76 | } 77 | 78 | navigation_drawer_sidebar_recycler.adapter = collectionAdapter 79 | navigation_drawer_sidebar_recycler.setHasFixedSize(true) 80 | navigation_drawer_sidebar_recycler.layoutManager = LinearLayoutManager(this) 81 | 82 | navigation_drawer_content_recycler.adapter = itemAdapter 83 | navigation_drawer_content_recycler.setHasFixedSize(true) 84 | navigation_drawer_content_recycler.layoutManager = LinearLayoutManager(this) 85 | 86 | val toggle = ActionBarDrawerToggle( 87 | this, drawer_layout, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close) 88 | drawer_layout.addDrawerListener(toggle) 89 | toggle.syncState() 90 | } 91 | 92 | override fun onBackPressed() { 93 | if (drawer_layout.isDrawerOpen(GravityCompat.START)) { 94 | drawer_layout.closeDrawer(GravityCompat.START) 95 | } else { 96 | super.onBackPressed() 97 | } 98 | } 99 | 100 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 101 | // Inflate the menu; this adds items to the action bar if it is present. 102 | menuInflater.inflate(R.menu.drawer_navigation, menu) 103 | 104 | return true 105 | } 106 | 107 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 108 | // Handle action bar item clicks here. The action bar will 109 | // automatically handle clicks on the Home/Up button, so long 110 | // as you specify a parent activity in AndroidManifest.xml. 111 | return when (item.itemId) { 112 | R.id.action_settings -> { 113 | startActivity(Intent(baseContext, SettingsActivity::class.java)) 114 | true 115 | } 116 | else -> super.onOptionsItemSelected(item) 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/com/gimranov/zandy/app/FakeDataUtil.kt: -------------------------------------------------------------------------------- 1 | package com.gimranov.zandy.app 2 | 3 | import com.gimranov.zandy.app.data.Item 4 | import io.bloco.faker.Faker 5 | import java.util.* 6 | 7 | internal object FakeDataUtil { 8 | private val faker = Faker() 9 | 10 | fun book(): Item { 11 | val item = Item() 12 | item.title = faker.book.title() 13 | item.type = "book" 14 | item.creatorSummary = faker.name.name() + ", " + faker.name.name() 15 | item.year = (1900 + Random().nextInt(117)).toString() 16 | 17 | return item 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/gimranov/zandy/app/ItemAction.kt: -------------------------------------------------------------------------------- 1 | package com.gimranov.zandy.app 2 | 3 | enum class ItemAction { 4 | EDIT, 5 | ORGANIZE, 6 | VIEW 7 | } -------------------------------------------------------------------------------- /src/main/java/com/gimranov/zandy/app/ItemAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.gimranov.zandy.app 2 | 3 | import android.graphics.Typeface 4 | import android.os.Build 5 | import androidx.recyclerview.widget.RecyclerView 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import android.widget.TableRow 10 | import android.widget.TextView 11 | import com.gimranov.zandy.app.data.Database 12 | import com.gimranov.zandy.app.data.DatabaseAccess 13 | import com.gimranov.zandy.app.data.Item 14 | import com.gimranov.zandy.app.databinding.ItemCardBinding 15 | import kotlinx.android.synthetic.main.item_card.view.* 16 | 17 | 18 | class ItemAdapter(val database: Database, 19 | itemListingRule: ItemListingRule, 20 | private val onItemNavigate: (Item, ItemAction) -> Unit) : RecyclerView.Adapter() { 21 | val cursor = when (itemListingRule) { 22 | is AllItems -> DatabaseAccess.items(database, null, null) 23 | is Children -> DatabaseAccess.items(database, itemListingRule.parent, null) 24 | is SearchResults -> DatabaseAccess.items(database, itemListingRule.query, null) 25 | } 26 | 27 | 28 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder { 29 | val layoutInflater = LayoutInflater.from(parent.context) 30 | val cardBinding = ItemCardBinding.inflate(layoutInflater, parent, false) 31 | return ItemViewHolder(cardBinding, onItemNavigate) 32 | } 33 | 34 | override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { 35 | if (cursor?.moveToPosition(position) != true) { 36 | return 37 | } 38 | 39 | holder.bind(Item.load(cursor)) 40 | } 41 | 42 | override fun onViewRecycled(holder: ItemViewHolder) { 43 | holder.unbind() 44 | } 45 | 46 | override fun getItemCount(): Int { 47 | return cursor?.count ?: 0 48 | } 49 | 50 | class ItemViewHolder(cardBinding: ItemCardBinding, 51 | private val onItemNavigate: (Item, ItemAction) -> Unit) : RecyclerView.ViewHolder(cardBinding.root) { 52 | 53 | private val binding = cardBinding 54 | private var expanded = false 55 | 56 | private fun toggle() { 57 | if (expanded) { 58 | hide() 59 | } else { 60 | show() 61 | } 62 | } 63 | 64 | private fun hide() { 65 | expanded = false 66 | binding.cardExpandedContent.visibility = View.GONE 67 | binding.cardButtonBar.visibility = View.GONE 68 | } 69 | 70 | private fun show() { 71 | expanded = true 72 | binding.cardExpandedContent.visibility = View.VISIBLE 73 | binding.cardButtonBar.visibility = View.VISIBLE 74 | } 75 | 76 | fun bind(item: Item) { 77 | binding.item = item 78 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 79 | binding.itemTypeIcon = binding.root.context.getDrawable(Item.resourceForType(item.type)) 80 | } else { 81 | @Suppress("DEPRECATION") 82 | binding.itemTypeIcon = binding.root.context.resources.getDrawable(Item.resourceForType(item.type)) 83 | } 84 | 85 | binding.cardHeader.setOnClickListener { toggle() } 86 | binding.cardButtonBar.card_button_bar_edit 87 | .setOnClickListener { onItemNavigate(item, ItemAction.EDIT) } 88 | binding.cardButtonBar.card_button_bar_organize 89 | .setOnClickListener { onItemNavigate(item, ItemAction.ORGANIZE) } 90 | 91 | val keys = item.content.keys().asSequence().sortedBy { Item.sortValueForLabel(it) }.toList() 92 | 93 | keys.forEach { 94 | val row = TableRow(binding.root.context) 95 | row.layoutParams = TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT) 96 | 97 | val labelField = TextView(binding.root.context) 98 | val contentField = TextView(binding.root.context) 99 | 100 | labelField.typeface = Typeface.DEFAULT_BOLD 101 | labelField.setPadding(0, 0, 10, 0) 102 | 103 | val (label, value) = ItemDisplayUtil.datumDisplayComponents(it, item.content.optString(it)) 104 | 105 | if (value.isEmpty()) { 106 | return 107 | } 108 | 109 | labelField.text = label 110 | contentField.text = value 111 | 112 | row.addView(labelField) 113 | row.addView(contentField) 114 | 115 | binding.cardExpandedContent.addView(row) 116 | } 117 | 118 | binding.executePendingBindings() 119 | } 120 | 121 | fun unbind() { 122 | binding.cardExpandedContent.removeAllViews() 123 | hide() 124 | } 125 | } 126 | } 127 | 128 | -------------------------------------------------------------------------------- /src/main/java/com/gimranov/zandy/app/ItemDisplayUtil.kt: -------------------------------------------------------------------------------- 1 | package com.gimranov.zandy.app 2 | 3 | import android.os.Build 4 | import android.text.Html 5 | import com.gimranov.zandy.app.data.Item 6 | import org.json.JSONArray 7 | import org.json.JSONObject 8 | 9 | internal object ItemDisplayUtil { 10 | fun datumDisplayComponents(label: String, 11 | value: String): Pair { 12 | 13 | /* Since the field names are the API / internal form, we 14 | * attempt to get a localized, human-readable version. */ 15 | val localizedLabel = Item.localizedStringForString(label) 16 | 17 | if ("itemType" == label) { 18 | return Pair(localizedLabel, Item.localizedStringForString(value)) 19 | } 20 | 21 | if ("creators" == label) { 22 | return Pair(localizedLabel, formatCreatorList(JSONArray(value))) 23 | } 24 | 25 | if ("title" == label || "note" == label || "abstractNote" == label) { 26 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 27 | return Pair(localizedLabel, Html.fromHtml(value, Html.FROM_HTML_MODE_LEGACY)) 28 | } else { 29 | @Suppress("DEPRECATION") 30 | return Pair(localizedLabel, Html.fromHtml(value)) 31 | } 32 | } else { 33 | return Pair(localizedLabel, value) 34 | } 35 | } 36 | 37 | fun formatCreatorList(creators: JSONArray): CharSequence { 38 | /* 39 | * Creators should be labeled with role and listed nicely 40 | * This logic isn't as good as it could be. 41 | */ 42 | var creator: JSONObject 43 | val sb = StringBuilder() 44 | for (j in 0 until creators.length()) { 45 | creator = creators.getJSONObject(j) 46 | if (creator.getString("creatorType") == "author") { 47 | if (creator.has("name")) 48 | sb.append(creator.getString("name")) 49 | else 50 | sb.append(creator.getString("firstName") + " " 51 | + creator.getString("lastName")) 52 | } else { 53 | if (creator.has("name")) 54 | sb.append(creator.getString("name")) 55 | else 56 | sb.append(creator.getString("firstName") 57 | + " " 58 | + creator.getString("lastName") 59 | + " (" 60 | + Item.localizedStringForString(creator 61 | .getString("creatorType")) 62 | + ")") 63 | } 64 | if (j < creators.length() - 1) 65 | sb.append(", ") 66 | } 67 | 68 | return sb.toString() 69 | } 70 | } -------------------------------------------------------------------------------- /src/main/java/com/gimranov/zandy/app/ItemListingRule.kt: -------------------------------------------------------------------------------- 1 | package com.gimranov.zandy.app 2 | 3 | import com.gimranov.zandy.app.data.ItemCollection 4 | 5 | sealed class ItemListingRule 6 | data class Children(val parent: ItemCollection?, val includeCollections: Boolean): ItemListingRule() 7 | data class SearchResults(val query: String): ItemListingRule() 8 | object AllItems: ItemListingRule() 9 | -------------------------------------------------------------------------------- /src/main/java/com/gimranov/zandy/app/NoteActivity.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * This file is part of Zandy. 3 | * 4 | * Zandy is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Zandy is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with Zandy. If not, see . 16 | ******************************************************************************/ 17 | package com.gimranov.zandy.app; 18 | 19 | import android.app.Activity; 20 | import android.app.AlertDialog; 21 | import android.app.Dialog; 22 | import android.content.DialogInterface; 23 | import android.os.Build; 24 | import android.os.Bundle; 25 | import android.text.Editable; 26 | import android.text.Html; 27 | import android.text.Spanned; 28 | import android.util.Log; 29 | import android.view.View; 30 | import android.view.View.OnClickListener; 31 | import android.widget.Button; 32 | import android.widget.EditText; 33 | import android.widget.TextView; 34 | import android.widget.TextView.BufferType; 35 | import android.widget.Toast; 36 | 37 | import com.gimranov.zandy.app.data.Attachment; 38 | import com.gimranov.zandy.app.data.Database; 39 | import com.gimranov.zandy.app.data.Item; 40 | import com.gimranov.zandy.app.task.APIRequest; 41 | 42 | /** 43 | * This Activity handles displaying and editing of notes. 44 | * 45 | * @author mlt 46 | */ 47 | public class NoteActivity extends Activity { 48 | 49 | private static final String TAG = NoteActivity.class.getSimpleName(); 50 | 51 | static final int DIALOG_NOTE = 3; 52 | 53 | public Attachment att; 54 | private Database db; 55 | 56 | /** 57 | * Called when the activity is first created. 58 | */ 59 | @Override 60 | public void onCreate(Bundle savedInstanceState) { 61 | super.onCreate(savedInstanceState); 62 | 63 | setContentView(R.layout.note); 64 | 65 | db = new Database(this); 66 | 67 | /* Get the incoming data from the calling activity */ 68 | final String attKey = getIntent().getStringExtra("com.gimranov.zandy.app.attKey"); 69 | final Attachment att = Attachment.load(attKey, db); 70 | 71 | if (att == null) { 72 | Log.e(TAG, "NoteActivity started without attKey; finishing."); 73 | finish(); 74 | return; 75 | } 76 | 77 | Item item = Item.load(att.parentKey, db); 78 | this.att = att; 79 | 80 | setTitle(getResources().getString(R.string.note_for_item, item.getTitle())); 81 | 82 | TextView text = findViewById(R.id.noteText); 83 | TextView title = findViewById(R.id.noteTitle); 84 | title.setText(att.title); 85 | Spanned spanned; 86 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 87 | spanned = Html.fromHtml(att.content.optString("note", ""), Html.FROM_HTML_MODE_COMPACT); 88 | } else { 89 | //noinspection deprecation 90 | spanned = Html.fromHtml(att.content.optString("note", "")); 91 | } 92 | text.setText(spanned); 93 | 94 | Button editButton = findViewById(R.id.editNote); 95 | editButton.setOnClickListener(new OnClickListener() { 96 | @Override 97 | public void onClick(View arg0) { 98 | showDialog(DIALOG_NOTE); 99 | } 100 | }); 101 | 102 | /* Warn that this won't propagate for attachment notes */ 103 | if (!"note".equals(att.getType())) { 104 | Toast.makeText(this, R.string.attachment_note_warning, Toast.LENGTH_LONG).show(); 105 | } 106 | } 107 | 108 | protected Dialog onCreateDialog(int id) { 109 | AlertDialog dialog; 110 | switch (id) { 111 | case DIALOG_NOTE: 112 | final EditText input = new EditText(this); 113 | input.setText(att.content.optString("note", ""), BufferType.EDITABLE); 114 | 115 | AlertDialog.Builder builder = new AlertDialog.Builder(this) 116 | .setTitle(getResources().getString(R.string.note)) 117 | .setView(input) 118 | .setPositiveButton(getResources().getString(R.string.ok), new DialogInterface.OnClickListener() { 119 | public void onClick(DialogInterface dialog, int whichButton) { 120 | Editable value = input.getText(); 121 | String fixed = value.toString().replaceAll("\n\n", "\n
"); 122 | att.setNoteText(fixed); 123 | att.dirty = APIRequest.API_DIRTY; 124 | att.save(db); 125 | 126 | TextView text = findViewById(R.id.noteText); 127 | TextView title = findViewById(R.id.noteTitle); 128 | title.setText(att.title); 129 | text.setText(Html.fromHtml(att.content.optString("note", ""))); 130 | } 131 | }).setNeutralButton(getResources().getString(R.string.cancel), 132 | new DialogInterface.OnClickListener() { 133 | public void onClick(DialogInterface dialog, int whichButton) { 134 | // do nothing 135 | } 136 | }); 137 | dialog = builder.create(); 138 | return dialog; 139 | default: 140 | Log.e(TAG, "Invalid dialog requested"); 141 | return null; 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/main/java/com/gimranov/zandy/app/Persistence.java: -------------------------------------------------------------------------------- 1 | package com.gimranov.zandy.app; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import androidx.annotation.Nullable; 6 | 7 | class Persistence { 8 | 9 | private static final String FILE = "Persistence"; 10 | 11 | static void write(String key, String value) { 12 | SharedPreferences.Editor editor = Application.getInstance().getSharedPreferences(FILE, Context.MODE_PRIVATE).edit(); 13 | editor.putString(key, value); 14 | editor.apply(); 15 | } 16 | 17 | @Nullable 18 | static String read(String key) { 19 | SharedPreferences store = Application.getInstance().getSharedPreferences(FILE, Context.MODE_PRIVATE); 20 | if (!store.contains(key)) return null; 21 | 22 | return store.getString(key, null); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/gimranov/zandy/app/Query.java: -------------------------------------------------------------------------------- 1 | package com.gimranov.zandy.app; 2 | 3 | import java.util.ArrayList; 4 | 5 | import android.database.Cursor; 6 | import android.os.Bundle; 7 | 8 | import com.gimranov.zandy.app.data.Database; 9 | 10 | /** 11 | * This class is intended to provide ways of handling queries to the database. 12 | *

13 | * TODO 14 | * Needed functions: 15 | * - specify sets of fields and terms for those fields 16 | * * related need for mapping of fields-- i.e., publicationTitle = journalTitle 17 | * - provide reasonable efficiency through use of indexes; in SQLite or in Java 18 | * - return data in efficient fashion, preferably by exposing a Cursor or 19 | * some other paged access method. 20 | * - normalize queries and data to let 21 | * - allow saving of queries 22 | *

23 | * Some of this will mean changes to other parts of Zandy's data storage model; 24 | * specifically, the raw JSON we're using now won't get us much further. We 25 | * could in theory maintain an index with tokens drawn from the JSON that 26 | * we populate on original save... Not sure about this. 27 | * 28 | * @author ajlyon 29 | */ 30 | public class Query { 31 | 32 | private ArrayList parameters; 33 | 34 | private String sortBy; 35 | 36 | public Query() { 37 | parameters = new ArrayList<>(); 38 | } 39 | 40 | public void set(String field, String value) { 41 | Bundle b = new Bundle(); 42 | b.putString("field", field); 43 | b.putString("value", value); 44 | parameters.add(b); 45 | } 46 | 47 | public void sortBy(String term) { 48 | sortBy = term; 49 | } 50 | 51 | public Cursor query(Database db) { 52 | StringBuilder sb = new StringBuilder(); 53 | String[] args = new String[parameters.size()]; 54 | int i = 0; 55 | for (Bundle b : parameters) { 56 | if ("tag".equals(b.getString("field"))) { 57 | sb.append("item_content LIKE ?"); 58 | args[i] = "%" + b.getString("value") + "%"; 59 | } else { 60 | sb.append(b.getString("field")).append("=?"); 61 | args[i] = b.getString("value"); 62 | } 63 | i++; 64 | if (i < parameters.size()) sb.append(","); 65 | } 66 | return db.query("items", Database.ITEMCOLS, sb.toString(), args, null, null, this.sortBy, null); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/gimranov/zandy/app/RequestActivity.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * This file is part of Zandy. 3 | * 4 | * Zandy is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Zandy is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with Zandy. If not, see . 16 | ******************************************************************************/ 17 | package com.gimranov.zandy.app; 18 | 19 | import android.app.ListActivity; 20 | import android.content.Context; 21 | import android.database.Cursor; 22 | import android.os.Build; 23 | import android.os.Bundle; 24 | import android.text.Html; 25 | import android.view.View; 26 | import android.widget.AdapterView; 27 | import android.widget.AdapterView.OnItemClickListener; 28 | import android.widget.ListView; 29 | import android.widget.ResourceCursorAdapter; 30 | import android.widget.TextView; 31 | import android.widget.Toast; 32 | 33 | import com.gimranov.zandy.app.data.Database; 34 | import com.gimranov.zandy.app.task.APIRequest; 35 | 36 | /** 37 | * This activity exists only for debugging, at least at this point 38 | *

39 | * Potentially, this could let users cancel pending requests and do things 40 | * like resolve sync conflicts. 41 | * 42 | * @author ajlyon 43 | */ 44 | public class RequestActivity extends ListActivity { 45 | 46 | @SuppressWarnings("unused") 47 | private static final String TAG = RequestActivity.class.getSimpleName(); 48 | private Database db; 49 | 50 | /** 51 | * Called when the activity is first created. 52 | */ 53 | @Override 54 | public void onCreate(Bundle savedInstanceState) { 55 | super.onCreate(savedInstanceState); 56 | 57 | db = new Database(this); 58 | 59 | setContentView(R.layout.requests); 60 | 61 | this.setTitle(getResources().getString(R.string.sync_pending_requests)); 62 | 63 | setListAdapter(new ResourceCursorAdapter(this, android.R.layout.simple_list_item_2, create()) { 64 | @Override 65 | public void bindView(View view, Context c, Cursor cur) { 66 | APIRequest req = new APIRequest(cur); 67 | TextView tvTitle = view.findViewById(android.R.id.text1); 68 | TextView tvInfo = view.findViewById(android.R.id.text2); 69 | 70 | tvTitle.setText(req.query); 71 | // Set to an html-formatted representation of the request 72 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 73 | tvInfo.setText(Html.fromHtml(req.toHtmlString(), Html.FROM_HTML_MODE_COMPACT)); 74 | } else { 75 | //noinspection deprecation 76 | tvInfo.setText(Html.fromHtml(req.toHtmlString())); 77 | } 78 | } 79 | 80 | }); 81 | 82 | ListView lv = getListView(); 83 | lv.setOnItemClickListener(new OnItemClickListener() { 84 | public void onItemClick(AdapterView parent, View view, int position, long id) { 85 | ResourceCursorAdapter adapter = (ResourceCursorAdapter) parent.getAdapter(); 86 | Cursor cur = adapter.getCursor(); 87 | // Place the cursor at the selected item 88 | if (cur.moveToPosition(position)) { 89 | // and replace the cursor with one for the selected collection 90 | APIRequest req = new APIRequest(cur); 91 | // toast for now-- later do something 92 | Toast.makeText(getApplicationContext(), 93 | req.query, 94 | Toast.LENGTH_SHORT).show(); 95 | } else { 96 | // failed to move cursor; should do something 97 | } 98 | } 99 | }); 100 | 101 | } 102 | 103 | public Cursor create() { 104 | String[] cols = Database.REQUESTCOLS; 105 | String[] args = {}; 106 | 107 | return db.query("apirequests", cols, "", args, null, null, 108 | null, null); 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/com/gimranov/zandy/app/ServerCredentials.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * This file is part of Zandy. 3 | * 4 | * Zandy is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Zandy is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with Zandy. If not, see . 16 | ******************************************************************************/ 17 | package com.gimranov.zandy.app; 18 | 19 | import java.io.File; 20 | 21 | import android.content.Context; 22 | import android.content.SharedPreferences; 23 | import android.preference.PreferenceManager; 24 | import android.util.Log; 25 | 26 | import com.gimranov.zandy.app.storage.StorageManager; 27 | import com.gimranov.zandy.app.task.APIRequest; 28 | 29 | public class ServerCredentials { 30 | /** 31 | * Application key -- available from Zotero 32 | */ 33 | static final String CONSUMERKEY = "93a5aac13612aed2a236"; 34 | static final String CONSUMERSECRET = "196d86bd1298cb78511c"; 35 | 36 | /** 37 | * This is the zotero:// protocol we intercept 38 | * It probably shouldn't be changed. 39 | */ 40 | static final String CALLBACKURL = "zotero://"; 41 | 42 | /** 43 | * This is the Zotero API server. Those who set up independent 44 | * Zotero installations will need to change this. 45 | */ 46 | public static final String APIBASE = "https://api.zotero.org"; 47 | 48 | /** 49 | * These are the API GET-only methods 50 | */ 51 | public static final String ITEMFIELDS = "/itemFields"; 52 | public static final String ITEMTYPES = "/itemTypes"; 53 | public static final String ITEMTYPECREATORTYPES = "/itemTypeCreatorTypes"; 54 | public static final String CREATORFIELDS = "/creatorFields"; 55 | public static final String ITEMNEW = "/items/new"; 56 | 57 | /* These are the manipulation methods */ 58 | // /users/1/items GET, POST, PUT, DELETE 59 | public static final String ITEMS = "/users/USERID/items"; 60 | public static final String COLLECTIONS = "/users/USERID/collections"; 61 | 62 | public static final String TAGS = "/tags"; 63 | public static final String GROUPS = "/groups"; 64 | 65 | /** 66 | * And these are the OAuth endpoints we talk to. 67 | *

68 | * We embed the requested permissions in the endpoint URLs; see 69 | * http://www.zotero.org/support/dev/server_api/oauth#requesting_specific_permissions 70 | * for more details. 71 | */ 72 | static final String OAUTHREQUEST = "https://www.zotero.org/oauth/request?" + 73 | "library_access=1&" + 74 | "notes_access=1&" + 75 | "write_access=1&" + 76 | "all_groups=write"; 77 | static final String OAUTHACCESS = "https://www.zotero.org/oauth/access?" + 78 | "library_access=1&" + 79 | "notes_access=1&" + 80 | "write_access=1&" + 81 | "all_groups=write"; 82 | static final String OAUTHAUTHORIZE = "https://www.zotero.org/oauth/authorize?" + 83 | "library_access=1&" + 84 | "notes_access=1&" + 85 | "write_access=1&" + 86 | "all_groups=write"; 87 | 88 | private String userID; 89 | private String userKey; 90 | 91 | public ServerCredentials(Context c) { 92 | SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(c); 93 | userID = settings.getString("user_id", null); 94 | userKey = settings.getString("user_key", null); 95 | } 96 | 97 | public String prep(String in) { 98 | if (userID == null) { 99 | Log.d(ServerCredentials.class.getCanonicalName(), "UserID was null"); 100 | return in; 101 | } 102 | return in.replace("USERID", userID); 103 | } 104 | 105 | /** 106 | * Replaces USERID with appropriate ID if needed, and sets key if missing 107 | * 108 | * @param req 109 | * @return 110 | */ 111 | public APIRequest prep(APIRequest req) { 112 | req.query = prep(req.query); 113 | if (req.key == null) 114 | req.key = userKey; 115 | return req; 116 | } 117 | 118 | public String getKey() { 119 | return userKey; 120 | } 121 | 122 | public static boolean check(Context c) { 123 | SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(c); 124 | return settings.getString("user_id", null) != null 125 | && settings.getString("user_key", null) != null 126 | && !"".equals(settings.getString("user_id", null)) 127 | && !"".equals(settings.getString("user_key", null)); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/main/java/com/gimranov/zandy/app/SettingsActivity.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * This file is part of Zandy. 3 | * 4 | * Zandy is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Zandy is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with Zandy. If not, see . 16 | ******************************************************************************/ 17 | package com.gimranov.zandy.app; 18 | 19 | import android.app.AlertDialog; 20 | import android.app.Dialog; 21 | import android.content.DialogInterface; 22 | import android.content.Intent; 23 | import android.os.Bundle; 24 | import android.preference.PreferenceActivity; 25 | import android.util.Log; 26 | import android.view.View; 27 | import android.view.View.OnClickListener; 28 | import android.widget.Button; 29 | 30 | import com.gimranov.zandy.app.data.Database; 31 | 32 | public class SettingsActivity extends PreferenceActivity implements OnClickListener { 33 | 34 | private static final String TAG = SettingsActivity.class.getSimpleName(); 35 | 36 | static final int DIALOG_CONFIRM_DELETE = 5; 37 | 38 | @Override 39 | public void onCreate(Bundle savedInstanceState) { 40 | super.onCreate(savedInstanceState); 41 | 42 | addPreferencesFromResource(R.xml.settings); 43 | setContentView(R.layout.preferences); 44 | 45 | Button requestButton = findViewById(R.id.requestQueue); 46 | requestButton.setOnClickListener(this); 47 | 48 | Button resetButton = findViewById(R.id.resetDatabase); 49 | resetButton.setOnClickListener(this); 50 | } 51 | 52 | public void onClick(View v) { 53 | if (v.getId() == R.id.requestQueue) { 54 | Intent i = new Intent(getApplicationContext(), RequestActivity.class); 55 | startActivity(i); 56 | } else if (v.getId() == R.id.resetDatabase) { 57 | showDialog(DIALOG_CONFIRM_DELETE); 58 | } 59 | } 60 | 61 | protected Dialog onCreateDialog(int id) { 62 | AlertDialog dialog; 63 | 64 | switch (id) { 65 | case DIALOG_CONFIRM_DELETE: 66 | dialog = new AlertDialog.Builder(this) 67 | .setTitle(getResources().getString(R.string.settings_reset_database_warning)) 68 | .setPositiveButton(getResources().getString(R.string.menu_delete), new DialogInterface.OnClickListener() { 69 | public void onClick(DialogInterface dialog, int whichButton) { 70 | Database db = new Database(getBaseContext()); 71 | db.resetAllData(); 72 | finish(); 73 | } 74 | }).setNegativeButton(getResources().getString(R.string.cancel), new DialogInterface.OnClickListener() { 75 | public void onClick(DialogInterface dialog, int whichButton) { 76 | // do nothing 77 | } 78 | }).create(); 79 | return dialog; 80 | default: 81 | Log.e(TAG, "Invalid dialog requested"); 82 | return null; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/com/gimranov/zandy/app/SyncEvent.java: -------------------------------------------------------------------------------- 1 | package com.gimranov.zandy.app; 2 | 3 | class SyncEvent { 4 | private static final String TAG = SyncEvent.class.getCanonicalName(); 5 | 6 | static final int COMPLETE_CODE = 1; 7 | 8 | static final SyncEvent COMPLETE = new SyncEvent(COMPLETE_CODE); 9 | 10 | private int status; 11 | 12 | private SyncEvent(int status) { 13 | this.status = status; 14 | } 15 | 16 | public int getStatus() { 17 | return status; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/gimranov/zandy/app/Util.kt: -------------------------------------------------------------------------------- 1 | package com.gimranov.zandy.app 2 | 3 | internal object Util { 4 | private val DOI_PREFIX = "https://doi.org/" 5 | 6 | fun doiToUri(doi: String): String { 7 | return if (isDoi(doi)) { 8 | DOI_PREFIX + doi.replace("^doi:".toRegex(), "") 9 | } else doi 10 | } 11 | 12 | private fun isDoi(doi: String): Boolean { 13 | return doi.startsWith("doi:") || doi.startsWith("10.") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/gimranov/zandy/app/data/CollectionAdapter.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * This file is part of Zandy. 3 | * 4 | * Zandy is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Zandy is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with Zandy. If not, see . 16 | ******************************************************************************/ 17 | package com.gimranov.zandy.app.data; 18 | 19 | import android.content.Context; 20 | import android.database.Cursor; 21 | import android.view.LayoutInflater; 22 | import android.view.View; 23 | import android.view.ViewGroup; 24 | import android.widget.ResourceCursorAdapter; 25 | import android.widget.TextView; 26 | 27 | import com.gimranov.zandy.app.R; 28 | import com.gimranov.zandy.app.task.APIRequest; 29 | 30 | /** 31 | * Exposes collection to be displayed by a ListView 32 | * @author ajlyon 33 | * 34 | */ 35 | public class CollectionAdapter extends ResourceCursorAdapter { 36 | public static final String TAG = CollectionAdapter.class.getSimpleName(); 37 | 38 | public Context context; 39 | 40 | public CollectionAdapter(Context context, Cursor cursor) { 41 | super(context, R.layout.list_collection, cursor, false); 42 | this.context = context; 43 | } 44 | 45 | public View newView(Context context, Cursor cur, ViewGroup parent) { 46 | LayoutInflater li = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 47 | return li.inflate(R.layout.list_collection, parent, false); 48 | } 49 | 50 | /** 51 | * Call this when the data has been updated-- it refreshes the cursor and notifies of the change 52 | */ 53 | public void notifyDataSetChanged() { 54 | super.notifyDataSetChanged(); 55 | } 56 | 57 | @Override 58 | public void bindView(View view, Context context, Cursor cursor) { 59 | TextView tvTitle = (TextView)view.findViewById(R.id.collection_title); 60 | TextView tvInfo = (TextView)view.findViewById(R.id.collection_info); 61 | 62 | Database db = new Database(context); 63 | 64 | ItemCollection collection = ItemCollection.load(cursor); 65 | tvTitle.setText(collection.getTitle()); 66 | StringBuilder sb = new StringBuilder(); 67 | sb.append(collection.getSize()).append(" items"); 68 | sb.append("; ").append(collection.getSubcollections(db).size()).append(" subcollections"); 69 | if(!collection.dirty.equals(APIRequest.API_CLEAN)) 70 | sb.append("; ").append(collection.dirty); 71 | tvInfo.setText(sb.toString()); 72 | db.close(); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/com/gimranov/zandy/app/data/Creator.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * This file is part of Zandy. 3 | * 4 | * Zandy is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Zandy is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with Zandy. If not, see . 16 | ******************************************************************************/ 17 | package com.gimranov.zandy.app.data; 18 | 19 | import org.json.JSONException; 20 | import org.json.JSONObject; 21 | 22 | /** 23 | * Need to work out DB saving and loading soon 24 | * 25 | * @author ajlyon 26 | */ 27 | public class Creator { 28 | private String lastName; 29 | private String firstName; 30 | private String name; 31 | private String creatorType; 32 | private boolean singleField; 33 | 34 | private int dbId; 35 | 36 | public static final String TAG = "com.gimranov.zandy.app.data.Creator"; 37 | 38 | /** 39 | * A Creator, given type, a single string, and a boolean mode. 40 | * 41 | * @param mCreatorType A valid creator type 42 | * @param mName Name. If not in single-field-mode, last word will be lastName 43 | * @param mSingleField If true, name won't be parsed into first and last 44 | */ 45 | public Creator(String mCreatorType, String mName, boolean mSingleField) { 46 | creatorType = mCreatorType; 47 | singleField = mSingleField; 48 | name = mName; 49 | if (singleField) return; 50 | 51 | String[] pieces = name.split(" "); 52 | if (pieces.length > 1) { 53 | StringBuilder sb = new StringBuilder(); 54 | for (int i = 0; i < pieces.length - 1; i++) { 55 | sb.append(pieces[i]); 56 | } 57 | firstName = sb.toString(); 58 | lastName = pieces[pieces.length - 1]; 59 | } 60 | } 61 | 62 | /** 63 | * A creator given two name parts. They'll be joined for the name field. 64 | * 65 | * @param type 66 | * @param first 67 | * @param last 68 | */ 69 | public Creator(String type, String first, String last) { 70 | creatorType = type; 71 | firstName = first; 72 | lastName = last; 73 | singleField = false; 74 | name = first + " " + last; 75 | } 76 | 77 | public String getCreatorType() { 78 | return creatorType; 79 | } 80 | 81 | public void setCreatorType(String creatorType) { 82 | this.creatorType = creatorType; 83 | } 84 | 85 | public int getDbId() { 86 | return dbId; 87 | } 88 | 89 | public void setDbId(int dbId) { 90 | this.dbId = dbId; 91 | } 92 | 93 | public String getLastName() { 94 | return lastName; 95 | } 96 | 97 | public String getFirstName() { 98 | return firstName; 99 | } 100 | 101 | public String getName() { 102 | return name; 103 | } 104 | 105 | public boolean isSingleField() { 106 | return singleField; 107 | } 108 | 109 | public JSONObject toJSON() throws JSONException { 110 | if (singleField) 111 | return new JSONObject().accumulate("name", name) 112 | .accumulate("creatorType", creatorType); 113 | return new JSONObject().accumulate("firstName", firstName) 114 | .accumulate("lastName", lastName) 115 | .accumulate("creatorType", creatorType); 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/com/gimranov/zandy/app/data/DatabaseAccess.kt: -------------------------------------------------------------------------------- 1 | package com.gimranov.zandy.app.data 2 | 3 | import android.database.Cursor 4 | import com.gimranov.zandy.app.Query 5 | 6 | object DatabaseAccess { 7 | val TAG = this.javaClass.simpleName 8 | 9 | private val sortOptions = arrayOf("item_year, item_title COLLATE NOCASE", 10 | "item_creator COLLATE NOCASE, item_year", 11 | "item_title COLLATE NOCASE, item_year", 12 | "timestamp ASC, item_title COLLATE NOCASE") 13 | 14 | fun collections(db: Database): Cursor? { 15 | val args = arrayOf("false") 16 | return db.query("collections", Database.COLLCOLS, "collection_parent=?", args, null, null, "collection_name", null) 17 | } 18 | 19 | fun collectionsForParent(db: Database, parent: ItemCollection): Cursor? { 20 | val args = arrayOf(parent.key) 21 | return db.query("collections", Database.COLLCOLS, "collection_parent=?", args, null, null, "collection_name", null) 22 | } 23 | 24 | fun items(db: Database, parent: ItemCollection?, sortRule: String?): Cursor? { 25 | val sortClause = sortRule ?: sortOptions[0] 26 | 27 | when (parent) { 28 | null -> Query().query(db) 29 | else -> { 30 | val args = arrayOf(parent.dbId) 31 | return db.rawQuery("SELECT item_title, item_type, item_content, etag, dirty, items._id, item_key, item_year, item_creator, timestamp, item_children FROM items, itemtocollections WHERE items._id = item_id AND collection_id=? ORDER BY $sortClause", 32 | args) 33 | } 34 | } 35 | return Query().query(db) 36 | } 37 | 38 | fun items(db: Database, query: String, sortRule: String?): Cursor? { 39 | val sortClause = sortRule ?: sortOptions[0] 40 | 41 | val args = arrayOf("%$query%", "%$query%") 42 | return db.rawQuery("SELECT item_title, item_type, item_content, etag, dirty, _id, item_key, item_year, item_creator, timestamp, item_children FROM items WHERE item_title LIKE ? OR item_creator LIKE ? ORDER BY $sortClause", 43 | args) 44 | } 45 | } -------------------------------------------------------------------------------- /src/main/java/com/gimranov/zandy/app/data/ItemAdapter.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * This file is part of Zandy. 3 | * 4 | * Zandy is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Zandy is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with Zandy. If not, see . 16 | ******************************************************************************/ 17 | package com.gimranov.zandy.app.data; 18 | 19 | import android.content.Context; 20 | import android.database.Cursor; 21 | import android.util.Log; 22 | import android.view.LayoutInflater; 23 | import android.view.View; 24 | import android.view.ViewGroup; 25 | import android.widget.ImageView; 26 | import android.widget.ResourceCursorAdapter; 27 | import android.widget.TextView; 28 | 29 | import com.gimranov.zandy.app.R; 30 | 31 | /** 32 | * Exposes items to be displayed by a ListView 33 | * 34 | * @author ajlyon 35 | */ 36 | public class ItemAdapter extends ResourceCursorAdapter { 37 | public static final String TAG = ItemAdapter.class.getSimpleName(); 38 | 39 | public ItemAdapter(Context context, Cursor cursor) { 40 | super(context, R.layout.list_item, cursor, false); 41 | } 42 | 43 | public View newView(Context context, Cursor cur, ViewGroup parent) { 44 | LayoutInflater li = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 45 | return li.inflate(R.layout.list_item, parent, false); 46 | } 47 | 48 | /** 49 | * Call this when the data has been updated 50 | */ 51 | public void notifyDataSetChanged() { 52 | super.notifyDataSetChanged(); 53 | } 54 | 55 | @Override 56 | public void bindView(View view, Context context, Cursor cursor) { 57 | TextView tvTitle = view.findViewById(R.id.item_title); 58 | ImageView tvType = view.findViewById(R.id.item_type); 59 | TextView tvSummary = view.findViewById(R.id.item_summary); 60 | 61 | if (cursor == null) { 62 | Log.e(TAG, "cursor is null in bindView"); 63 | } 64 | Item item = Item.load(cursor); 65 | 66 | if (item == null) { 67 | Log.e(TAG, "item is null in bindView"); 68 | } 69 | if (tvTitle == null) { 70 | Log.e(TAG, "tvTitle is null in bindView"); 71 | } 72 | 73 | Log.d(TAG, "setting image for item (" + item.getKey() + ") of type: " + item.getType()); 74 | tvType.setImageResource(Item.resourceForType(item.getType())); 75 | 76 | tvSummary.setText(item.getCreatorSummary() + " (" + item.getYear() + ")"); 77 | if (tvSummary.getText().equals(" ()")) tvSummary.setVisibility(View.GONE); 78 | 79 | tvTitle.setText(item.getTitle()); 80 | 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/gimranov/zandy/app/storage/StorageManager.java: -------------------------------------------------------------------------------- 1 | package com.gimranov.zandy.app.storage; 2 | 3 | import android.content.Context; 4 | import android.os.Environment; 5 | 6 | import java.io.File; 7 | 8 | public class StorageManager { 9 | public static File getDocumentsDirectory(Context context) { 10 | File documents = new File(context.getExternalFilesDir(null), "documents"); 11 | //noinspection ResultOfMethodCallIgnored 12 | documents.mkdirs(); 13 | return documents; 14 | } 15 | 16 | public static File getCacheDirectory(Context context) { 17 | File cache = new File(context.getExternalFilesDir(null), "cache"); 18 | //noinspection ResultOfMethodCallIgnored 19 | cache.mkdirs(); 20 | return cache; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/gimranov/zandy/app/task/APIEvent.java: -------------------------------------------------------------------------------- 1 | package com.gimranov.zandy.app.task; 2 | 3 | public interface APIEvent { 4 | void onComplete(APIRequest request); 5 | 6 | void onUpdate(APIRequest request); 7 | 8 | void onError(APIRequest request, Exception exception); 9 | 10 | void onError(APIRequest request, int error); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/gimranov/zandy/app/task/APIException.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * This file is part of Zandy. 3 | * 4 | * Zandy is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Zandy is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with Zandy. If not, see . 16 | ******************************************************************************/ 17 | package com.gimranov.zandy.app.task; 18 | 19 | public class APIException extends Exception { 20 | 21 | /** 22 | * Don't know what this is for. 23 | */ 24 | private static final long serialVersionUID = 1L; 25 | 26 | /** 27 | * Exception types 28 | */ 29 | static final int INVALID_METHOD = 10; 30 | static final int INVALID_UUID = 11; 31 | static final int INVALID_URI = 12; 32 | static final int HTTP_ERROR = 13; 33 | 34 | public APIRequest request; 35 | public int type; 36 | 37 | APIException(int type, String message, APIRequest request) { 38 | super(message); 39 | this.request = request; 40 | } 41 | 42 | APIException(int type, String message, APIRequest request, Throwable cause) { 43 | super(message, cause); 44 | this.request = request; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/gimranov/zandy/app/task/ZoteroAPITask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Zandy 3 | * Based in part on Mendroid, Copyright 2011 Martin Paul Eve 4 | * 5 | * This file is part of Zandy. 6 | * 7 | * Zandy is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Affero General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * Zandy is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with Zandy. If not, see . 19 | * 20 | */ 21 | 22 | package com.gimranov.zandy.app.task; 23 | 24 | import java.util.ArrayList; 25 | 26 | import android.content.Context; 27 | import android.os.AsyncTask; 28 | import android.os.Handler; 29 | import android.os.Message; 30 | import android.util.Log; 31 | 32 | import com.gimranov.zandy.app.ServerCredentials; 33 | import com.gimranov.zandy.app.XMLResponseParser; 34 | import com.gimranov.zandy.app.data.Attachment; 35 | import com.gimranov.zandy.app.data.Database; 36 | import com.gimranov.zandy.app.data.Item; 37 | 38 | /** 39 | * Executes one or more API requests asynchronously. 40 | *

41 | * Steps in migration: 42 | * 1. Move the logic on what kind of request is handled how into the APIRequest itself 43 | * 2. Throw exceptions when we have errors 44 | * 3. Call the handlers when provided 45 | * 4. Move aggressive syncing logic out of ZoteroAPITask itself; it should be elsewhere. 46 | * 47 | * @author ajlyon 48 | */ 49 | public class ZoteroAPITask extends AsyncTask { 50 | private static final String TAG = ZoteroAPITask.class.getSimpleName(); 51 | 52 | public ArrayList deletions; 53 | public ArrayList queue; 54 | 55 | public int syncMode = -1; 56 | 57 | public static final int AUTO_SYNC_STALE_COLLECTIONS = 1; 58 | 59 | public boolean autoMode = false; 60 | 61 | private Database db; 62 | private ServerCredentials cred; 63 | 64 | private Handler handler; 65 | 66 | public ZoteroAPITask(Context c) { 67 | queue = new ArrayList(); 68 | cred = new ServerCredentials(c); 69 | /* TODO reenable in a working way 70 | if (settings.getBoolean("sync_aggressively", false)) 71 | syncMode = AUTO_SYNC_STALE_COLLECTIONS; 72 | */ 73 | deletions = APIRequest.delete(c); 74 | db = new Database(c); 75 | } 76 | 77 | public void setHandler(Handler h) { 78 | handler = h; 79 | } 80 | 81 | private Handler getHandler() { 82 | if (handler == null) { 83 | handler = new Handler() { 84 | }; 85 | } 86 | return handler; 87 | } 88 | 89 | @Override 90 | protected Message doInBackground(APIRequest... params) { 91 | return doFetch(params); 92 | } 93 | 94 | @SuppressWarnings("unused") 95 | public Message doFetch(APIRequest... reqs) { 96 | int count = reqs.length; 97 | 98 | for (int i = 0; i < count; i++) { 99 | if (reqs[i] == null) { 100 | Log.d(TAG, "Skipping null request"); 101 | continue; 102 | } 103 | 104 | // Just in case we missed something, we fix the user ID right here too, 105 | // and we set the key as well. 106 | reqs[i] = cred.prep(reqs[i]); 107 | 108 | try { 109 | Log.i(TAG, "Executing API call: " + reqs[i].query); 110 | reqs[i].issue(db, cred); 111 | Log.i(TAG, "Successfully retrieved API call: " + reqs[i].query); 112 | reqs[i].succeeded(db); 113 | 114 | } catch (APIException e) { 115 | Log.e(TAG, "Failed to execute API call: " + e.request.query, e); 116 | e.request.status = APIRequest.REQ_FAILING + e.request.getHttpStatus(); 117 | e.request.save(db); 118 | Message msg = Message.obtain(); 119 | msg.arg1 = APIRequest.ERROR_UNKNOWN + e.request.getHttpStatus(); 120 | return msg; 121 | } 122 | 123 | // The XML parser's queue is simply from following continuations in the paged 124 | // feed. We shouldn't split out its requests... 125 | if (XMLResponseParser.queue != null && !XMLResponseParser.queue.isEmpty()) { 126 | Log.i(TAG, "Finished call, but adding " + 127 | XMLResponseParser.queue.size() + 128 | " items to queue."); 129 | queue.addAll(XMLResponseParser.queue); 130 | XMLResponseParser.queue.clear(); 131 | } else { 132 | Log.i(TAG, "Finished call, and parser's request queue is empty"); 133 | } 134 | } 135 | 136 | // 137 | if (queue.size() > 0) { 138 | // If the last batch saw unchanged items, don't follow the Atom 139 | // continuations; just run the child requests 140 | // XXX This is disabled for now 141 | if (false && !XMLResponseParser.followNext) { 142 | ArrayList toRemove = new ArrayList(); 143 | for (APIRequest r : queue) { 144 | if (r.type != APIRequest.ITEMS_CHILDREN) { 145 | Log.d(TAG, "Removing request from queue since last page had old items: " + r.query); 146 | toRemove.add(r); 147 | } 148 | } 149 | queue.removeAll(toRemove); 150 | } 151 | Log.i(TAG, "Starting queued requests: " + queue.size() + " requests"); 152 | APIRequest[] templ = {}; 153 | APIRequest[] requests = queue.toArray(templ); 154 | queue.clear(); 155 | Log.i(TAG, "Queue size now: " + queue.size()); 156 | // XXX I suspect that this calling of doFetch from doFetch might be the cause of our 157 | // out-of-memory situations. We may be able to accomplish the same thing by expecting 158 | // the code listening to our handler to fetch again if QUEUED_MORE is received. In that 159 | // case, we could just save our queue here and really return. 160 | 161 | // XXX Test: Here, we try to use doInBackground instead 162 | doInBackground(requests); 163 | 164 | // Return a message with the number of requests added to the queue 165 | Message msg = Message.obtain(); 166 | msg.arg1 = APIRequest.QUEUED_MORE; 167 | msg.arg2 = requests.length; 168 | return msg; 169 | } 170 | 171 | 172 | // Here's where we tie in to periodic housekeeping syncs 173 | // If we're already in auto mode (that is, here), just move on 174 | if (autoMode) { 175 | Message msg = Message.obtain(); 176 | msg.arg1 = APIRequest.UPDATED_DATA; 177 | return msg; 178 | } 179 | 180 | Log.d(TAG, "Sending local changes"); 181 | Item.queue(db); 182 | Attachment.queue(db); 183 | 184 | APIRequest[] templ = {}; 185 | 186 | ArrayList list = new ArrayList(); 187 | for (Item i : Item.queue) { 188 | list.add(cred.prep(APIRequest.update(i))); 189 | } 190 | 191 | for (Attachment a : Attachment.queue) { 192 | list.add(cred.prep(APIRequest.update(a, db))); 193 | } 194 | 195 | // This queue has deletions, collection memberships, and failing requests 196 | // We may want to filter it in the future 197 | list.addAll(APIRequest.queue(db)); 198 | 199 | // We're in auto mode... 200 | autoMode = true; 201 | doInBackground(list.toArray(templ)); 202 | 203 | // Return a message noting that we've queued more requests 204 | Message msg = Message.obtain(); 205 | msg.arg1 = APIRequest.QUEUED_MORE; 206 | msg.arg2 = list.size(); 207 | return msg; 208 | } 209 | 210 | 211 | @Override 212 | protected void onPostExecute(Message result) { 213 | getHandler().sendMessage(result); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/main/java/com/gimranov/zandy/app/view/CardViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.gimranov.zandy.app.view 2 | 3 | import com.gimranov.zandy.app.data.Item 4 | import com.gimranov.zandy.app.data.ItemCollection 5 | 6 | sealed class CardViewModel { 7 | 8 | } 9 | data class ItemViewModel(val item: Item) : CardViewModel() 10 | data class CollectionViewModel(val collection: ItemCollection) : CardViewModel() -------------------------------------------------------------------------------- /src/main/java/com/gimranov/zandy/app/webdav/WebDavTrust.java: -------------------------------------------------------------------------------- 1 | package com.gimranov.zandy.app.webdav; 2 | 3 | import android.annotation.SuppressLint; 4 | 5 | import javax.net.ssl.HttpsURLConnection; 6 | import javax.net.ssl.SSLContext; 7 | import javax.net.ssl.TrustManager; 8 | import javax.net.ssl.X509TrustManager; 9 | 10 | import java.security.SecureRandom; 11 | import java.security.cert.X509Certificate; 12 | 13 | public class WebDavTrust { 14 | private static final String TAG = WebDavTrust.class.getSimpleName(); 15 | 16 | // Kudos and blame to http://stackoverflow.com/a/1201102/950790 17 | public static void installAllTrustingCertificate() { 18 | // Create a trust manager that does not validate certificate chains 19 | TrustManager[] trustAllCerts = new TrustManager[]{ 20 | new X509TrustManager() { 21 | public X509Certificate[] getAcceptedIssuers() { 22 | return null; 23 | } 24 | 25 | @SuppressLint("TrustAllX509TrustManager") 26 | public void checkClientTrusted(X509Certificate[] certs, String authType) { 27 | } 28 | 29 | @SuppressLint("TrustAllX509TrustManager") 30 | public void checkServerTrusted(X509Certificate[] certs, String authType) { 31 | } 32 | } 33 | }; 34 | 35 | // Install the all-trusting trust manager 36 | try { 37 | SSLContext sc = SSLContext.getInstance("SSL"); 38 | sc.init(null, trustAllCerts, new SecureRandom()); 39 | HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); 40 | } catch (Exception ignored) { 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/res/drawable-v21/ic_menu_camera.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/res/drawable-v21/ic_menu_gallery.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/res/drawable-v21/ic_menu_manage.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /src/main/res/drawable-v21/ic_menu_send.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/res/drawable-v21/ic_menu_share.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/res/drawable-v21/ic_menu_slideshow.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/res/drawable/book.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avram/zandy/bce70c1b1dd83b6ddc4592cb6e503a4add313f64/src/main/res/drawable/book.png -------------------------------------------------------------------------------- /src/main/res/drawable/book_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avram/zandy/bce70c1b1dd83b6ddc4592cb6e503a4add313f64/src/main/res/drawable/book_open.png -------------------------------------------------------------------------------- /src/main/res/drawable/comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avram/zandy/bce70c1b1dd83b6ddc4592cb6e503a4add313f64/src/main/res/drawable/comment.png -------------------------------------------------------------------------------- /src/main/res/drawable/email.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avram/zandy/bce70c1b1dd83b6ddc4592cb6e503a4add313f64/src/main/res/drawable/email.png -------------------------------------------------------------------------------- /src/main/res/drawable/film.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avram/zandy/bce70c1b1dd83b6ddc4592cb6e503a4add313f64/src/main/res/drawable/film.png -------------------------------------------------------------------------------- /src/main/res/drawable/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avram/zandy/bce70c1b1dd83b6ddc4592cb6e503a4add313f64/src/main/res/drawable/folder.png -------------------------------------------------------------------------------- /src/main/res/drawable/glyphish_02_redo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avram/zandy/bce70c1b1dd83b6ddc4592cb6e503a4add313f64/src/main/res/drawable/glyphish_02_redo.png -------------------------------------------------------------------------------- /src/main/res/drawable/glyphish_104_index_cards.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avram/zandy/bce70c1b1dd83b6ddc4592cb6e503a4add313f64/src/main/res/drawable/glyphish_104_index_cards.png -------------------------------------------------------------------------------- /src/main/res/drawable/glyphish_106_sliders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avram/zandy/bce70c1b1dd83b6ddc4592cb6e503a4add313f64/src/main/res/drawable/glyphish_106_sliders.png -------------------------------------------------------------------------------- /src/main/res/drawable/glyphish_10_medical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avram/zandy/bce70c1b1dd83b6ddc4592cb6e503a4add313f64/src/main/res/drawable/glyphish_10_medical.png -------------------------------------------------------------------------------- /src/main/res/drawable/glyphish_151_telescope.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avram/zandy/bce70c1b1dd83b6ddc4592cb6e503a4add313f64/src/main/res/drawable/glyphish_151_telescope.png -------------------------------------------------------------------------------- /src/main/res/drawable/glyphish_195_barcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avram/zandy/bce70c1b1dd83b6ddc4592cb6e503a4add313f64/src/main/res/drawable/glyphish_195_barcode.png -------------------------------------------------------------------------------- /src/main/res/drawable/glyphish_22_skull_n_bones.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avram/zandy/bce70c1b1dd83b6ddc4592cb6e503a4add313f64/src/main/res/drawable/glyphish_22_skull_n_bones.png -------------------------------------------------------------------------------- /src/main/res/drawable/glyphish_59_flag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avram/zandy/bce70c1b1dd83b6ddc4592cb6e503a4add313f64/src/main/res/drawable/glyphish_59_flag.png -------------------------------------------------------------------------------- /src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /src/main/res/drawable/layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avram/zandy/bce70c1b1dd83b6ddc4592cb6e503a4add313f64/src/main/res/drawable/layout.png -------------------------------------------------------------------------------- /src/main/res/drawable/list_child_indicator.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/res/drawable/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avram/zandy/bce70c1b1dd83b6ddc4592cb6e503a4add313f64/src/main/res/drawable/map.png -------------------------------------------------------------------------------- /src/main/res/drawable/newspaper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avram/zandy/bce70c1b1dd83b6ddc4592cb6e503a4add313f64/src/main/res/drawable/newspaper.png -------------------------------------------------------------------------------- /src/main/res/drawable/note.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avram/zandy/bce70c1b1dd83b6ddc4592cb6e503a4add313f64/src/main/res/drawable/note.png -------------------------------------------------------------------------------- /src/main/res/drawable/page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avram/zandy/bce70c1b1dd83b6ddc4592cb6e503a4add313f64/src/main/res/drawable/page.png -------------------------------------------------------------------------------- /src/main/res/drawable/page_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avram/zandy/bce70c1b1dd83b6ddc4592cb6e503a4add313f64/src/main/res/drawable/page_white.png -------------------------------------------------------------------------------- /src/main/res/drawable/page_white_acrobat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avram/zandy/bce70c1b1dd83b6ddc4592cb6e503a4add313f64/src/main/res/drawable/page_white_acrobat.png -------------------------------------------------------------------------------- /src/main/res/drawable/page_white_powerpoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avram/zandy/bce70c1b1dd83b6ddc4592cb6e503a4add313f64/src/main/res/drawable/page_white_powerpoint.png -------------------------------------------------------------------------------- /src/main/res/drawable/page_white_text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avram/zandy/bce70c1b1dd83b6ddc4592cb6e503a4add313f64/src/main/res/drawable/page_white_text.png -------------------------------------------------------------------------------- /src/main/res/drawable/page_white_text_width.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avram/zandy/bce70c1b1dd83b6ddc4592cb6e503a4add313f64/src/main/res/drawable/page_white_text_width.png -------------------------------------------------------------------------------- /src/main/res/drawable/page_white_width.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avram/zandy/bce70c1b1dd83b6ddc4592cb6e503a4add313f64/src/main/res/drawable/page_white_width.png -------------------------------------------------------------------------------- /src/main/res/drawable/picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avram/zandy/bce70c1b1dd83b6ddc4592cb6e503a4add313f64/src/main/res/drawable/picture.png -------------------------------------------------------------------------------- /src/main/res/drawable/report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avram/zandy/bce70c1b1dd83b6ddc4592cb6e503a4add313f64/src/main/res/drawable/report.png -------------------------------------------------------------------------------- /src/main/res/drawable/report_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avram/zandy/bce70c1b1dd83b6ddc4592cb6e503a4add313f64/src/main/res/drawable/report_user.png -------------------------------------------------------------------------------- /src/main/res/drawable/script.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avram/zandy/bce70c1b1dd83b6ddc4592cb6e503a4add313f64/src/main/res/drawable/script.png -------------------------------------------------------------------------------- /src/main/res/drawable/side_nav_bar.xml: -------------------------------------------------------------------------------- 1 | 3 | 9 | -------------------------------------------------------------------------------- /src/main/res/drawable/television.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avram/zandy/bce70c1b1dd83b6ddc4592cb6e503a4add313f64/src/main/res/drawable/television.png -------------------------------------------------------------------------------- /src/main/res/drawable/zandy72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avram/zandy/bce70c1b1dd83b6ddc4592cb6e503a4add313f64/src/main/res/drawable/zandy72.png -------------------------------------------------------------------------------- /src/main/res/layout/activity_drawer_navigation.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 22 | 23 | 31 | 32 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/main/res/layout/app_bar_drawer_navigation.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/main/res/layout/collection_card.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 16 | 17 | 27 | 28 | 29 | 35 | 36 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/main/res/layout/collections.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 22 | 23 | 26 | 27 | 28 | 31 | 32 | -------------------------------------------------------------------------------- /src/main/res/layout/content_drawer_navigation.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 17 | 18 | -------------------------------------------------------------------------------- /src/main/res/layout/creator_dialog.xml: -------------------------------------------------------------------------------- 1 | 17 | 23 | 24 | 28 | 29 | 34 | 35 | 36 | 40 | 41 | 45 | 46 | 50 | 51 | 52 | 56 | 57 | 63 | 64 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/main/res/layout/item_card.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 13 | 14 | 15 | 19 | 20 | 27 | 28 | 32 | 33 | 39 | 40 | 52 | 53 | 54 | 58 | 59 | 67 | 68 | 79 | 80 | 81 | 82 | 83 | 92 | 93 | 99 | 100 | 34 | 35 | -------------------------------------------------------------------------------- /src/main/res/layout/main.xml: -------------------------------------------------------------------------------- 1 | 17 | 22 | 23 |