1
0
mirror of https://github.com/sismics/docs.git synced 2025-12-14 10:16:21 +00:00

114 Commits
v1.1 ... v1.3

Author SHA1 Message Date
jendib
08e4f6ddae Closes #14: Soft delete on DocumentTag + audit log ordering 2015-08-28 01:02:33 +02:00
jendib
86cae53789 Closes #20: Clean error message if document or file does not exist 2015-08-26 22:11:39 +02:00
jendib
5cbdb5d87d Merge branch 'master' of https://github.com/sismics/docs.git 2015-08-24 22:18:55 +02:00
jendib
f8d889bb1f Closes #22: incorrect composite ID for DocumentTag 2015-08-24 22:18:47 +02:00
Benjamin Gamard
4625f9e42a Tesseract package for japanese language 2015-08-21 00:16:33 +02:00
jendib
6add34bb33 #20: Display logs on documents 2015-05-23 19:16:38 +02:00
jendib
ea4e3fd8f2 #20: Audit log displayed on main screen 2015-05-17 22:20:34 +02:00
jendib
b2a38cea62 Closes #21: Save IP and UA on login 2015-05-15 17:30:21 +02:00
jendib
0228d43442 Design fix 2015-05-11 11:34:49 +02:00
jendib
060e5e8e24 Android: ACLs in right drawer 2015-05-10 21:44:39 +02:00
jendib
566c563786 Android: metadata in right drawer 2015-05-10 14:44:45 +02:00
jendib
8597eac9f9 Fix ACL resource 2015-05-10 13:55:49 +02:00
jendib
b7f920f864 Native query for GET /document/id 2015-05-10 13:45:39 +02:00
jendib
1e3282eff3 Fix non-deletable ACL 2015-05-10 13:19:16 +02:00
jendib
451d913442 Simplify adding READ+WRITE ACL 2015-05-10 13:12:23 +02:00
jendib
e3962c4e3f Merge branch 'master' of https://github.com/sismics/docs.git 2015-05-09 23:35:20 +02:00
jendib
25136bc146 Refactor, TODO 2015-05-09 23:35:10 +02:00
jendib
c727ac3a56 Refactor, TODO 2015-05-09 23:34:43 +02:00
jendib
52387d93ac Closes #13: Don't show tags from other users 2015-05-09 21:52:01 +02:00
jendib
072dd7b280 Android: handle read-only documents, use ACLs for sharing 2015-05-09 21:51:06 +02:00
jendib
42320dc9b9 #13: Fix performance issue 2015-05-09 18:00:03 +02:00
jendib
ff994ce63b #13: Disable shared status in GET /document/list (too slow) 2015-05-09 16:48:01 +02:00
jendib
82ba0b5761 #13: Display the document's creator 2015-05-09 16:21:59 +02:00
jendib
fc1bb22d8d #13: ACL system 2015-05-09 14:44:19 +02:00
jendib
6ff639baac Android: advanced search (done) 2015-05-07 01:56:03 +02:00
jendib
4c24e7921a Android: advanced search UI 2015-05-05 23:00:23 +02:00
jendib
f7f5f93a9e Android: advanced search UI 2015-05-04 23:05:03 +02:00
jendib
f1eb3795d9 Android: login activity design 2015-05-04 20:51:32 +02:00
jendib
1f092f7d93 Android: signing 2015-05-02 19:37:56 +02:00
jendib
9b4b13a721 Convert JS click event to <a/> links 2015-05-02 17:15:37 +02:00
jendib
a1af1f369f Fixes #10: Page size for pagination 2015-05-01 19:44:20 +02:00
jendib
c283607063 Don't crash if a file is deleted before OCR is completed 2015-04-29 01:28:42 +02:00
jendib
b8061a6a1e Android: upgrade to AppCompat 22.1 2015-04-22 23:18:14 +02:00
jendib
1c4161981b Upload drag & dropped files sequentially 2015-03-29 16:05:42 +02:00
jendib
5e3093d0d3 Attach orphan files to a new document 2015-03-28 18:02:21 +01:00
jendib
0d4643cc93 File modal refactoring + orphan files selection 2015-03-28 00:09:28 +01:00
jendib
80a2e0d055 Drag & drop files to documents 2015-03-27 23:03:55 +01:00
Benjamin Gamard
06e97824df JCE prerequisite 2015-03-27 00:57:40 +01:00
jendib
3461804399 Drag & drop to upload orphan files 2015-03-27 00:50:00 +01:00
jendib
bfc70baefb Fix Junit 2015-03-24 23:59:06 +01:00
jendib
aa4b73b730 Android: don't fail build on lint errors 2015-03-24 23:53:28 +01:00
jendib
07247854ac #4 : Upgrade PDFBox 2015-03-24 22:20:54 +01:00
jendib
9b5780bbb0 Android: API 22 2015-03-24 01:07:32 +01:00
Jean-Marc Tremeaux
bad42d96f3 chmod +x 2015-03-13 15:58:32 +01:00
Jean-Marc Tremeaux
6ff1570b3c Deploy script 2015-03-11 22:51:24 +01:00
Jean-Marc Tremeaux
24345bc176 Deploy script 2015-03-11 22:37:35 +01:00
Walter
75cde7e9d6 Docker: Using sismics/data for data image 2015-03-11 14:31:10 +01:00
Walter
192c2030d3 Dockerization + Fix for Tesseract 3.03 2015-03-11 00:35:42 +01:00
jendib
18cedaef2c Orphan files are linked to a specific user 2015-03-06 22:40:33 +01:00
jendib
d0c259ead2 List orphan files 2015-03-06 21:23:50 +01:00
jendib
2347483676 Order of files attached to document 2015-03-06 21:13:09 +01:00
jendib
6c976087de Missing file + TODO 2015-03-03 00:26:40 +01:00
jendib
c36014b46f Ability to upload files without document (no OCR, no Lucene)
+ New resource to attach a document to a file and OCR/Lucene it
2015-03-03 00:23:30 +01:00
jendib
5cf0532db7 Page size selector 2015-03-02 00:07:42 +01:00
jendib
6edae27d26 Fixes #3: Race condition on settings screen 2015-01-25 19:20:49 +01:00
jendib
9ae8303b18 Android: sort tags by count desc 2015-01-15 01:57:29 +01:00
jendib
a0356845b1 Android: event listeners on main thread, files UI polish 2015-01-15 01:49:11 +01:00
jendib
6fa0b8494e Android: file delete 2015-01-14 23:31:53 +01:00
jendib
c9210c39c4 Android: file upload 2015-01-11 01:53:40 +01:00
jendib
790453047d Android: file upload background service 2015-01-08 00:34:57 +01:00
jendib
5befef2992 Android: Document deleting 2014-12-13 16:27:27 +01:00
jendib
dd59172a19 Android: Document form validation 2014-12-11 22:29:04 +01:00
jendib
17e5c65d04 Android: Document adding/editing 2014-12-11 22:13:06 +01:00
jendib
a762ce4715 Android: Update view after an editing 2014-12-05 00:12:42 +01:00
jendib
89d66eca4a Android: Document edit activity filling 2014-12-04 22:32:53 +01:00
jendib
323b95ad7a Android: language selection 2014-12-04 03:41:35 +01:00
jendib
b42b195245 Android: tags autocompletion 2014-12-04 02:28:44 +01:00
jendib
a7987386e1 Android: tags autocompletion (in progress) 2014-12-03 00:28:26 +01:00
jendib
a181eac9a5 Android: settings activity 2014-12-01 22:20:23 +01:00
jendib
5662c080d6 Merge branch 'master' of https://github.com/sismics/docs.git 2014-12-01 01:21:48 +01:00
jendib
745766a2c3 Fix error handling on DELETE /share/id 2014-12-01 01:21:38 +01:00
Benjamin Gamard
f44d30d890 Update README.md 2014-11-30 23:13:08 +01:00
Benjamin Gamard
ea0c7982ac Update README.md 2014-11-30 23:12:28 +01:00
jendib
2abf0c6eab Android: drawer background 2014-11-30 22:49:35 +01:00
jendib
13e8b828ac Android: filtering intents with an URL to Docs is not possible
The Android platform forces to specify a full hostname which is variable in our case
2014-11-30 22:02:07 +01:00
jendib
551c10e7a3 Android: file upload (in progress), title marquee 2014-11-30 16:11:35 +01:00
jendib
e17abfe411 Android: share editing 2014-11-29 23:50:56 +01:00
jendib
3330acfc75 Android: tags caching 2014-11-28 01:40:54 +01:00
jendib
2837b21a86 Android: init document edit activity 2014-11-26 23:30:25 +01:00
jendib
5666d9b8b5 Android: floating action button 2014-11-26 02:08:20 +01:00
jendib
824e37b8ea Android: swipe refresh on documents list 2014-11-26 01:01:36 +01:00
jendib
1d08508e51 Android: empty lists and loading error feedback 2014-11-25 23:53:52 +01:00
jendib
a84748f075 Android: better flags, tags loading feedback 2014-11-25 00:09:12 +01:00
jendib
a6c123ad03 Android: display shared status 2014-11-24 20:07:06 +01:00
jendib
407564f28c Android: show all documents from drawer 2014-11-23 22:10:24 +01:00
jendib
6a9a166670 Android: tags in drawer 2014-11-23 19:55:08 +01:00
jendib
1773998ca0 Android: Searching 2014-11-23 00:49:56 +01:00
jendib
c610364ef7 Android: Infinite scrolling 2014-11-22 22:43:31 +01:00
jendib
bc719b3165 Android: File(s) download 2014-11-22 22:18:21 +01:00
jendib
92b2219bed Android: Display tags, language and shared status on document 2014-11-22 14:06:08 +01:00
jendib
8c5c54125f Android: document details 2014-11-22 01:09:12 +01:00
jendib
2ce5749226 Android: documents listing: tags and date 2014-11-21 03:18:20 +01:00
jendib
76d195a344 Android: documents listing from API 2014-11-20 02:11:18 +01:00
jendib
04752cab0c Android: material design, API 21 2014-11-19 01:18:42 +01:00
jendib
ffa7d796b5 Upgrade Android build tools 2014-05-27 16:53:15 +02:00
jendib
5119d32714 Init login 2014-05-26 22:22:13 +02:00
jendib
c0a963e845 Button to clear the search field, bug fix on share modal 2014-05-12 21:39:20 +02:00
jendib
20c2b0460b TODO 2014-05-11 15:05:06 +02:00
jendib
2ee979c012 TODO 2014-05-11 15:02:02 +02:00
jendib
bc3683990f Merge branch 'master' of https://github.com/sismics/docs 2014-05-05 23:14:02 +02:00
jendib
c06e2971bc AS/Gradle upgrade 2014-05-05 23:13:23 +02:00
Benjamin Gamard
b3aeac8bf4 Update README.md 2014-02-23 14:30:54 +01:00
jendib
34e3ac5478 Download all files from a document as ZIP 2014-02-23 14:09:41 +01:00
jendib
ae566018d6 Gruntification 2014-02-23 02:28:44 +01:00
jendib
12c3c4750f Print a file 2014-02-02 01:19:17 +01:00
jendib
42f23ed0d7 Git ignore 2014-01-28 14:41:04 +01:00
jendib
d76b8e32c8 File upload progress 2014-01-19 18:46:07 +01:00
jendib
6aaecb473f New logo 2014-01-13 20:05:17 +01:00
jendib
03976160bf Migration of /share.html 2014-01-11 21:39:26 +01:00
Benjamin Gamard
438a38985a Merge pull request #1 from sismics/bs3
Merge BS3 branch
2014-01-11 11:40:03 -08:00
jendib
9802beaf7b Migration of /tags and /settings views 2014-01-11 20:39:00 +01:00
jendib
6963fd9770 Migration of /document views 2014-01-11 16:38:58 +01:00
jendib
85aa16afba Angular UI Bootstrap migration 2014-01-11 00:56:36 +01:00
jendib
0e99f06310 Upgrade libs 2014-01-10 23:54:12 +01:00
344 changed files with 38095 additions and 67741 deletions

4
.gitignore vendored
View File

@@ -4,6 +4,8 @@
/*/bin
/*/gen
/*/target
/*/build
/*/*.iml
/out
/.idea
/.idea
/.project

10
Dockerfile Normal file
View File

@@ -0,0 +1,10 @@
FROM sismics/debian-java7-jetty9
MAINTAINER benjamin.gam@gmail.com
RUN apt-get -y -q install tesseract-ocr tesseract-ocr-fra tesseract-ocr-jpn
ENV TESSDATA_PREFIX /usr/share/tesseract-ocr
ENV LC_NUMERIC C
ADD docs-web/target/docs-web-*.war /opt/jetty/webapps/docs.war
ADD docs.xml /opt/jetty/webapps/docs.xml

View File

@@ -1,7 +1,13 @@
Sismics Docs
============
![](http://www.sismics.com/docs/img/docs.jpg)
_Web interface_
![Web interface](http://sismics.com/docs/screenshot1.png)
_Android application_
![Android documents list](http://sismics.com/docs/android1.png) ![Android navigation](http://sismics.com/docs/android2.png) ![Android document details](http://sismics.com/docs/android3.png)
What is Docs?
---------------
@@ -20,20 +26,21 @@ Features
- Full text search in image and PDF
- SHA-256 encryption
- Tag system
- Multi-users
- Document sharing
- Multi-users ACL system
- Document sharing by URL
- RESTful Web API
- Modern Android client
License
-------
Download
--------
Docs is released under the terms of the GPL license. See `COPYING` for more
information or see <http://opensource.org/licenses/GPL-2.0>.
The latest release is downloadable here: <https://github.com/sismics/docs/releases> in WAR format.
You will need a Java webapp server to run it, like [Jetty](http://eclipse.org/jetty/) or [Tomcat](http://tomcat.apache.org/)
How to build Docs from the sources
----------------------------------
Prerequisites: JDK 7, Maven 3, Tesseract 3.02
Prerequisites: JDK 7 with JCE, Maven 3, Tesseract 3.02
Docs is organized in several Maven modules:
@@ -65,3 +72,9 @@ From the `docs-web` directory:
mvn -Pprod -DskipTests clean install
You will get your deployable WAR in the `target` directory.
License
-------
Docs is released under the terms of the GPL license. See `COPYING` for more
information or see <http://opensource.org/licenses/GPL-2.0>.

2
build.sh Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/sh
docker build -t sismics/docs .

View File

@@ -1,13 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" type="JAVA_MODULE" version="4">
<module external.linked.project.id=":app" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="docs-android" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="android-gradle" name="Android-Gradle">
<configuration>
<option name="GRADLE_PROJECT_PATH" value=":app" />
</configuration>
</facet>
<facet type="android" name="Android">
<configuration>
<option name="SELECTED_BUILD_VARIANT" value="debug" />
<option name="SELECTED_TEST_ARTIFACT" value="_android_test_" />
<option name="ASSEMBLE_TASK_NAME" value="assembleDebug" />
<option name="COMPILE_JAVA_TASK_NAME" value="compileDebugJava" />
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleDebugTest" />
<option name="COMPILE_JAVA_TASK_NAME" value="compileDebugSources" />
<option name="SOURCE_GEN_TASK_NAME" value="generateDebugSources" />
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleDebugAndroidTest" />
<option name="COMPILE_JAVA_TEST_TASK_NAME" value="compileDebugAndroidTestSources" />
<option name="TEST_SOURCE_GEN_TASK_NAME" value="generateDebugAndroidTestSources" />
<option name="ALLOW_USER_CONFIGURATION" value="false" />
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
@@ -15,62 +23,99 @@
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
</configuration>
</facet>
<facet type="android-gradle" name="Android-Gradle">
<configuration>
<option name="GRADLE_PROJECT_PATH" value=":app" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="false">
<output url="file://$MODULE_DIR$/build/classes/debug" />
<output url="file://$MODULE_DIR$/build/intermediates/classes/debug" />
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/androidTest/debug" />
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/build/source/r/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/source/aidl/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/source/buildConfig/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/source/rs/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/res/rs/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/source/r/test/debug" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/build/source/aidl/test/debug" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/build/source/buildConfig/test/debug" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/build/source/rs/test/debug" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/build/res/rs/test/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/androidTest/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/androidTest/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/androidTest/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/androidTest/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/assets" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/assets" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/instrumentTest/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/instrumentTest/assets" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/instrumentTest/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/instrumentTest/jni" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/instrumentTest/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/instrumentTest/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/instrumentTest/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/jni" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/build/apk" />
<excludeFolder url="file://$MODULE_DIR$/build/assets" />
<excludeFolder url="file://$MODULE_DIR$/build/bundles" />
<excludeFolder url="file://$MODULE_DIR$/build/classes" />
<excludeFolder url="file://$MODULE_DIR$/build/dependency-cache" />
<excludeFolder url="file://$MODULE_DIR$/build/dex" />
<excludeFolder url="file://$MODULE_DIR$/build/incremental" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/assets" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/bundles" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/classes" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/coverage-instrumented-classes" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dependency-cache" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex-cache" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/appcompat-v7/22.1.1/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/recyclerview-v7/22.0.0/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/support-v4/22.1.1/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.shamanland/fab/0.0.6/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/it.sephiroth.android.library.easing/android-easing/1.0.3/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/it.sephiroth.android.library.imagezoom/imagezoom/1.0.5/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/jacoco" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/javaResources" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/libs" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/lint" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/manifests" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/ndk" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/pre-dexed" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/proguard" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/res" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" />
<excludeFolder url="file://$MODULE_DIR$/build/libs" />
<excludeFolder url="file://$MODULE_DIR$/build/manifests" />
<excludeFolder url="file://$MODULE_DIR$/build/ndk" />
<excludeFolder url="file://$MODULE_DIR$/build/outputs" />
<excludeFolder url="file://$MODULE_DIR$/build/pre-dexed" />
<excludeFolder url="file://$MODULE_DIR$/build/res" />
<excludeFolder url="file://$MODULE_DIR$/build/rs" />
<excludeFolder url="file://$MODULE_DIR$/build/source" />
<excludeFolder url="file://$MODULE_DIR$/build/symbols" />
<excludeFolder url="file://$MODULE_DIR$/build/tmp" />
</content>
<orderEntry type="jdk" jdkName="Android API 19 Platform" jdkType="Android SDK" />
<orderEntry type="jdk" jdkName="Android API 22 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" exported="" name="support-v4-18.0.0" level="project" />
<orderEntry type="library" exported="" name="appcompat-v7-22.1.1" level="project" />
<orderEntry type="library" exported="" name="fab-0.0.6" level="project" />
<orderEntry type="library" exported="" name="android-easing-1.0.3" level="project" />
<orderEntry type="library" exported="" name="imagezoom-1.0.5" level="project" />
<orderEntry type="library" exported="" name="eventbus-2.4.0" level="project" />
<orderEntry type="library" exported="" name="android-query.0.26.8" level="project" />
<orderEntry type="library" exported="" name="tokenautocomplete-1.2.1" level="project" />
<orderEntry type="library" exported="" name="support-v4-22.1.1" level="project" />
<orderEntry type="library" exported="" name="support-annotations-22.1.1" level="project" />
<orderEntry type="library" exported="" name="recyclerview-v7-22.0.0" level="project" />
<orderEntry type="library" exported="" name="android-async-http-1.4.6" level="project" />
</component>
</module>
</module>

View File

@@ -3,22 +3,22 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:0.7.+'
classpath 'com.android.tools.build:gradle:1.2.3'
}
}
apply plugin: 'android'
apply plugin: 'com.android.application'
repositories {
mavenCentral()
}
android {
compileSdkVersion 19
buildToolsVersion "19.0.0"
compileSdkVersion 22
buildToolsVersion "22.0.1"
defaultConfig {
minSdkVersion 14
targetSdkVersion 19
targetSdkVersion 22
versionCode 1
versionName "1.0"
}
@@ -27,14 +27,33 @@ android {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
signingConfigs {
release {
storeFile file(System.getenv("TRACKINO_STORE_PATH"))
storePassword System.getenv("TRACKINO_STORE_PASS")
keyAlias System.getenv("TRACKINO_STORE_ALIAS")
keyPassword System.getenv("TRACKINO_STORE_KEYPASS")
}
}
buildTypes {
release {
runProguard false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
signingConfig signingConfigs.release
}
}
lintOptions {
abortOnError false
}
}
dependencies {
compile 'com.android.support:support-v4:18.0.0'
compile fileTree(dir: 'libs', include: '*.jar')
compile 'com.android.support:appcompat-v7:22.1.1'
compile 'com.android.support:recyclerview-v7:22.0.0'
compile 'com.loopj.android:android-async-http:1.4.6'
compile 'it.sephiroth.android.library.imagezoom:imagezoom:1.0.5'
compile 'de.greenrobot:eventbus:2.4.0'
compile 'com.shamanland:fab:0.0.6'
}

Binary file not shown.

Binary file not shown.

View File

@@ -2,28 +2,66 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.sismics.docs" >
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application
android:name=".MainApplication"
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="com.sismics.docs.DocListActivity"
android:label="@string/app_name" >
android:name=".activity.LoginActivity"
android:label="@string/app_name"
android:theme="@style/AppThemeDark">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="com.sismics.docs.DocDetailActivity"
android:label="@string/title_doc_detail"
android:parentActivityName="com.sismics.docs.DocListActivity" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.sismics.docs.DocListActivity" />
android:name=".activity.MainActivity"
android:label="@string/app_name"
android:launchMode="singleTop"
android:windowSoftInputMode="adjustNothing">
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.GET_CONTENT" />
<category android:name="android.intent.category.OPENABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
<meta-data android:name="android.app.searchable" android:resource="@xml/searchable" />
</activity>
<activity
android:name=".activity.DocumentViewActivity"
android:label="">
</activity>
<activity
android:name=".activity.DocumentEditActivity"
android:label="@string/new_document">
</activity>
<activity
android:name=".activity.SettingsActivity"
android:label="@string/settings">
</activity>
<provider android:name=".provider.RecentSuggestionsProvider"
android:exported="false"
android:authorities="com.sismics.docs.provider.RecentSuggestionsProvider" />
<service
android:name=".service.FileUploadService"
android:enabled="true"
android:exported="false" >
<intent-filter>
<action android:name="com.sismics.docs.file.upload"/>
</intent-filter>
</service>
</application>
</manifest>

View File

@@ -1,67 +0,0 @@
package com.sismics.docs;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.NavUtils;
import android.view.MenuItem;
/**
* An activity representing a single Doc detail screen. This
* activity is only used on handset devices. On tablet-size devices,
* item details are presented side-by-side with a list of items
* in a {@link DocListActivity}.
* <p>
* This activity is mostly just a 'shell' activity containing nothing
* more than a {@link DocDetailFragment}.
*/
public class DocDetailActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_doc_detail);
// Show the Up button in the action bar.
getActionBar().setDisplayHomeAsUpEnabled(true);
// savedInstanceState is non-null when there is fragment state
// saved from previous configurations of this activity
// (e.g. when rotating the screen from portrait to landscape).
// In this case, the fragment will automatically be re-added
// to its container so we don't need to manually add it.
// For more information, see the Fragments API guide at:
//
// http://developer.android.com/guide/components/fragments.html
//
if (savedInstanceState == null) {
// Create the detail fragment and add it to the activity
// using a fragment transaction.
Bundle arguments = new Bundle();
arguments.putString(DocDetailFragment.ARG_ITEM_ID,
getIntent().getStringExtra(DocDetailFragment.ARG_ITEM_ID));
DocDetailFragment fragment = new DocDetailFragment();
fragment.setArguments(arguments);
getSupportFragmentManager().beginTransaction()
.add(R.id.doc_detail_container, fragment)
.commit();
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
// This ID represents the Home or Up button. In the case of this
// activity, the Up button is shown. Use NavUtils to allow users
// to navigate up one level in the application structure. For
// more details, see the Navigation pattern on Android Design:
//
// http://developer.android.com/design/patterns/navigation.html#up-vs-back
//
NavUtils.navigateUpTo(this, new Intent(this, DocListActivity.class));
return true;
}
return super.onOptionsItemSelected(item);
}
}

View File

@@ -1,61 +0,0 @@
package com.sismics.docs;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.sismics.docs.dummy.DummyContent;
/**
* A fragment representing a single Doc detail screen.
* This fragment is either contained in a {@link DocListActivity}
* in two-pane mode (on tablets) or a {@link DocDetailActivity}
* on handsets.
*/
public class DocDetailFragment extends Fragment {
/**
* The fragment argument representing the item ID that this fragment
* represents.
*/
public static final String ARG_ITEM_ID = "item_id";
/**
* The dummy content this fragment is presenting.
*/
private DummyContent.DummyItem mItem;
/**
* Mandatory empty constructor for the fragment manager to instantiate the
* fragment (e.g. upon screen orientation changes).
*/
public DocDetailFragment() {
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments().containsKey(ARG_ITEM_ID)) {
// Load the dummy content specified by the fragment
// arguments. In a real-world scenario, use a Loader
// to load content from a content provider.
mItem = DummyContent.ITEM_MAP.get(getArguments().getString(ARG_ITEM_ID));
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_doc_detail, container, false);
// Show the dummy content as text in a TextView.
if (mItem != null) {
((TextView) rootView.findViewById(R.id.doc_detail)).setText(mItem.content);
}
return rootView;
}
}

View File

@@ -1,81 +0,0 @@
package com.sismics.docs;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
/**
* An activity representing a list of Docs. This activity
* has different presentations for handset and tablet-size devices. On
* handsets, the activity presents a list of items, which when touched,
* lead to a {@link DocDetailActivity} representing
* item details. On tablets, the activity presents the list of items and
* item details side-by-side using two vertical panes.
* <p>
* The activity makes heavy use of fragments. The list of items is a
* {@link DocListFragment} and the item details
* (if present) is a {@link DocDetailFragment}.
* <p>
* This activity also implements the required
* {@link DocListFragment.Callbacks} interface
* to listen for item selections.
*/
public class DocListActivity extends FragmentActivity
implements DocListFragment.Callbacks {
/**
* Whether or not the activity is in two-pane mode, i.e. running on a tablet
* device.
*/
private boolean mTwoPane;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_doc_list);
if (findViewById(R.id.doc_detail_container) != null) {
// The detail container view will be present only in the
// large-screen layouts (res/values-large and
// res/values-sw600dp). If this view is present, then the
// activity should be in two-pane mode.
mTwoPane = true;
// In two-pane mode, list items should be given the
// 'activated' state when touched.
((DocListFragment) getSupportFragmentManager()
.findFragmentById(R.id.doc_list))
.setActivateOnItemClick(true);
}
// TODO: If exposing deep links into your app, handle intents here.
}
/**
* Callback method from {@link DocListFragment.Callbacks}
* indicating that the item with the given ID was selected.
*/
@Override
public void onItemSelected(String id) {
if (mTwoPane) {
// In two-pane mode, show the detail view in this activity by
// adding or replacing the detail fragment using a
// fragment transaction.
Bundle arguments = new Bundle();
arguments.putString(DocDetailFragment.ARG_ITEM_ID, id);
DocDetailFragment fragment = new DocDetailFragment();
fragment.setArguments(arguments);
getSupportFragmentManager().beginTransaction()
.replace(R.id.doc_detail_container, fragment)
.commit();
} else {
// In single-pane mode, simply start the detail activity
// for the selected item ID.
Intent detailIntent = new Intent(this, DocDetailActivity.class);
detailIntent.putExtra(DocDetailFragment.ARG_ITEM_ID, id);
startActivity(detailIntent);
}
}
}

View File

@@ -1,151 +0,0 @@
package com.sismics.docs;
import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.ListFragment;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import com.sismics.docs.dummy.DummyContent;
/**
* A list fragment representing a list of Docs. This fragment
* also supports tablet devices by allowing list items to be given an
* 'activated' state upon selection. This helps indicate which item is
* currently being viewed in a {@link DocDetailFragment}.
* <p>
* Activities containing this fragment MUST implement the {@link Callbacks}
* interface.
*/
public class DocListFragment extends ListFragment {
/**
* The serialization (saved instance state) Bundle key representing the
* activated item position. Only used on tablets.
*/
private static final String STATE_ACTIVATED_POSITION = "activated_position";
/**
* The fragment's current callback object, which is notified of list item
* clicks.
*/
private Callbacks mCallbacks = sDummyCallbacks;
/**
* The current activated item position. Only used on tablets.
*/
private int mActivatedPosition = ListView.INVALID_POSITION;
/**
* A callback interface that all activities containing this fragment must
* implement. This mechanism allows activities to be notified of item
* selections.
*/
public interface Callbacks {
/**
* Callback for when an item has been selected.
*/
public void onItemSelected(String id);
}
/**
* A dummy implementation of the {@link Callbacks} interface that does
* nothing. Used only when this fragment is not attached to an activity.
*/
private static Callbacks sDummyCallbacks = new Callbacks() {
@Override
public void onItemSelected(String id) {
}
};
/**
* Mandatory empty constructor for the fragment manager to instantiate the
* fragment (e.g. upon screen orientation changes).
*/
public DocListFragment() {
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// TODO: replace with a real list adapter.
setListAdapter(new ArrayAdapter<DummyContent.DummyItem>(
getActivity(),
android.R.layout.simple_list_item_activated_1,
android.R.id.text1,
DummyContent.ITEMS));
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// Restore the previously serialized activated item position.
if (savedInstanceState != null
&& savedInstanceState.containsKey(STATE_ACTIVATED_POSITION)) {
setActivatedPosition(savedInstanceState.getInt(STATE_ACTIVATED_POSITION));
}
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
// Activities containing this fragment must implement its callbacks.
if (!(activity instanceof Callbacks)) {
throw new IllegalStateException("Activity must implement fragment's callbacks.");
}
mCallbacks = (Callbacks) activity;
}
@Override
public void onDetach() {
super.onDetach();
// Reset the active callbacks interface to the dummy implementation.
mCallbacks = sDummyCallbacks;
}
@Override
public void onListItemClick(ListView listView, View view, int position, long id) {
super.onListItemClick(listView, view, position, id);
// Notify the active callbacks interface (the activity, if the
// fragment is attached to one) that an item has been selected.
mCallbacks.onItemSelected(DummyContent.ITEMS.get(position).id);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (mActivatedPosition != ListView.INVALID_POSITION) {
// Serialize and persist the activated item position.
outState.putInt(STATE_ACTIVATED_POSITION, mActivatedPosition);
}
}
/**
* Turns on activate-on-click mode. When this mode is on, list items will be
* given the 'activated' state when touched.
*/
public void setActivateOnItemClick(boolean activateOnItemClick) {
// When setting CHOICE_MODE_SINGLE, ListView will automatically
// give items the 'activated' state when touched.
getListView().setChoiceMode(activateOnItemClick
? ListView.CHOICE_MODE_SINGLE
: ListView.CHOICE_MODE_NONE);
}
private void setActivatedPosition(int position) {
if (position == ListView.INVALID_POSITION) {
getListView().setItemChecked(mActivatedPosition, false);
} else {
getListView().setItemChecked(position, true);
}
mActivatedPosition = position;
}
}

View File

@@ -0,0 +1,33 @@
package com.sismics.docs;
import android.app.Application;
import com.androidquery.callback.BitmapAjaxCallback;
import com.sismics.docs.model.application.ApplicationContext;
import com.sismics.docs.util.PreferenceUtil;
import org.json.JSONObject;
/**
* Main application.
*
* @author bgamard
*/
public class MainApplication extends Application {
@Override
public void onCreate() {
// Fetching GET /user from cache
JSONObject json = PreferenceUtil.getCachedJson(getApplicationContext(), PreferenceUtil.PREF_CACHED_USER_INFO_JSON);
ApplicationContext.getInstance().setUserInfo(getApplicationContext(), json);
// TODO google docs app: right drawer with all actions, with acls, with deep metadatas
// TODO Provide documents to intent action get content
super.onCreate();
}
@Override
public void onLowMemory() {
BitmapAjaxCallback.clearCache();
}
}

View File

@@ -0,0 +1,240 @@
package com.sismics.docs.activity;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.Toast;
import com.sismics.docs.R;
import com.sismics.docs.adapter.LanguageAdapter;
import com.sismics.docs.adapter.TagAutoCompleteAdapter;
import com.sismics.docs.event.DocumentAddEvent;
import com.sismics.docs.event.DocumentEditEvent;
import com.sismics.docs.listener.JsonHttpResponseHandler;
import com.sismics.docs.resource.DocumentResource;
import com.sismics.docs.ui.form.Validator;
import com.sismics.docs.ui.form.validator.Required;
import com.sismics.docs.ui.view.DatePickerView;
import com.sismics.docs.ui.view.TagsCompleteTextView;
import com.sismics.docs.util.PreferenceUtil;
import org.apache.http.Header;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import de.greenrobot.event.EventBus;
/**
* Document edition activity.
*
* @author bgamard.
*/
public class DocumentEditActivity extends AppCompatActivity {
/**
* Document edited.
*/
private JSONObject document;
/**
* Form validator.
*/
private Validator validator;
// View cache
private EditText titleEditText;
private EditText descriptionEditText;
private TagsCompleteTextView tagsEditText;
private Spinner languageSpinner;
private DatePickerView datePickerView;
@Override
protected void onCreate(Bundle args) {
super.onCreate(args);
// Handle activity context
if (getIntent() == null) {
finish();
return;
}
// Parse input document
String documentJson = getIntent().getStringExtra("document");
if (documentJson != null) {
try {
document = new JSONObject(documentJson);
} catch (JSONException e) {
finish();
return;
}
}
// Setup the activity
setContentView(R.layout.document_edit_activity);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setHomeButtonEnabled(true);
}
languageSpinner = (Spinner) findViewById(R.id.languageSpinner);
tagsEditText = (TagsCompleteTextView) findViewById(R.id.tagsEditText);
datePickerView = (DatePickerView) findViewById(R.id.dateEditText);
titleEditText = (EditText) findViewById(R.id.titleEditText);
descriptionEditText = (EditText) findViewById(R.id.descriptionEditText);
// Language spinner
LanguageAdapter languageAdapter = new LanguageAdapter(this, false);
languageSpinner.setAdapter(languageAdapter);
// Tags auto-complete
JSONObject tags = PreferenceUtil.getCachedJson(this, PreferenceUtil.PREF_CACHED_TAGS_JSON);
if (tags == null) {
finish();
return;
}
JSONArray tagArray = tags.optJSONArray("stats");
List<JSONObject> tagList = new ArrayList<>();
for (int i = 0; i < tagArray.length(); i++) {
tagList.add(tagArray.optJSONObject(i));
}
tagsEditText.allowDuplicates(false);
tagsEditText.setAdapter(new TagAutoCompleteAdapter(this, 0, tagList));
// Validation
validator = new Validator(this, true);
validator.addValidable(titleEditText, new Required());
// Fill the activity
if (document == null) {
datePickerView.setDate(new Date());
} else {
setTitle(R.string.edit_document);
titleEditText.setText(document.optString("title"));
descriptionEditText.setText(document.optString("description"));
datePickerView.setDate(new Date(document.optLong("create_date")));
languageSpinner.setSelection(languageAdapter.getItemPosition(document.optString("language")));
JSONArray documentTags = document.optJSONArray("tags");
for (int i = 0; i < documentTags.length(); i++) {
tagsEditText.addObject(documentTags.optJSONObject(i));
}
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.document_edit_activity, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.save:
validator.validate();
if (!validator.isValidated()) {
return true;
}
// Metadata
final String title = titleEditText.getText().toString();
final String description = descriptionEditText.getText().toString();
LanguageAdapter.Language language = (LanguageAdapter.Language) languageSpinner.getSelectedItem();
final String langId = language.getId();
final long createDate = datePickerView.getDate().getTime();
Set<String> tagIdList = new HashSet<>();
for (Object object : tagsEditText.getObjects()) {
JSONObject tag = (JSONObject) object;
tagIdList.add(tag.optString("id"));
}
// Cancellable progress dialog
final ProgressDialog progressDialog = ProgressDialog.show(this,
getString(R.string.please_wait),
getString(R.string.document_editing_message), true, true,
new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
DocumentResource.cancel(DocumentEditActivity.this);
}
});
// Server callback
JsonHttpResponseHandler callback = new JsonHttpResponseHandler() {
@Override
public void onSuccess(int statusCode, Header[] headers, JSONObject response) {
// Build a fake document JSON to update the UI
final JSONObject outputDoc = new JSONObject();
try {
if (document == null) {
outputDoc.putOpt("id", response.optString("id"));
outputDoc.putOpt("shared", false);
} else {
outputDoc.putOpt("id", document.optString("id"));
outputDoc.putOpt("shared", document.optBoolean("shared"));
}
outputDoc.putOpt("title", title);
outputDoc.putOpt("description", description);
outputDoc.putOpt("language", langId);
outputDoc.putOpt("create_date", createDate);
JSONArray tags = new JSONArray();
for (Object object : tagsEditText.getObjects()) {
tags.put(object);
}
outputDoc.putOpt("tags", tags);
} catch (JSONException e) {
Log.e(DocumentEditActivity.class.getSimpleName(), "Error building JSON for document", e);
}
// Fire the right event
if (document == null) {
EventBus.getDefault().post(new DocumentAddEvent(outputDoc));
} else {
EventBus.getDefault().post(new DocumentEditEvent(outputDoc));
}
setResult(RESULT_OK);
finish();
}
@Override
public void onAllFailure(int statusCode, Header[] headers, byte[] responseBytes, Throwable throwable) {
Toast.makeText(DocumentEditActivity.this, R.string.error_editing_document, Toast.LENGTH_LONG).show();
}
@Override
public void onFinish() {
progressDialog.dismiss();
}
};
// Actual server call
if (document == null) {
DocumentResource.add(this, title, description, tagIdList, langId, createDate, callback);
} else {
DocumentResource.edit(this, document.optString("id"), title, description, tagIdList, langId, createDate, callback);
}
return true;
case android.R.id.home:
finish();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
}

View File

@@ -0,0 +1,614 @@
package com.sismics.docs.activity;
import android.app.AlertDialog;
import android.app.DownloadManager;
import android.app.ProgressDialog;
import android.content.ClipData;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.support.v4.app.DialogFragment;
import android.support.v4.view.GravityCompat;
import android.support.v4.view.ViewPager;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import com.sismics.docs.R;
import com.sismics.docs.adapter.AclListAdapter;
import com.sismics.docs.adapter.FilePagerAdapter;
import com.sismics.docs.event.DocumentDeleteEvent;
import com.sismics.docs.event.DocumentEditEvent;
import com.sismics.docs.event.DocumentFullscreenEvent;
import com.sismics.docs.event.FileAddEvent;
import com.sismics.docs.event.FileDeleteEvent;
import com.sismics.docs.fragment.DocShareFragment;
import com.sismics.docs.listener.JsonHttpResponseHandler;
import com.sismics.docs.model.application.ApplicationContext;
import com.sismics.docs.resource.DocumentResource;
import com.sismics.docs.resource.FileResource;
import com.sismics.docs.service.FileUploadService;
import com.sismics.docs.util.PreferenceUtil;
import com.sismics.docs.util.TagUtil;
import org.apache.http.Header;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import de.greenrobot.event.EventBus;
/**
* Document activity.
*
* @author bgamard
*/
public class DocumentViewActivity extends AppCompatActivity {
/**
* Request code of adding file.
*/
public static final int REQUEST_CODE_ADD_FILE = 1;
/**
* Request code of editing document.
*/
public static final int REQUEST_CODE_EDIT_DOCUMENT = 2;
/**
* File view pager.
*/
private ViewPager fileViewPager;
/**
* File pager adapter.
*/
private FilePagerAdapter filePagerAdapter;
/**
* Document displayed.
*/
private JSONObject document;
/**
* Menu.
*/
private Menu menu;
@Override
protected void onCreate(final Bundle args) {
super.onCreate(args);
// Check if logged in
if (!ApplicationContext.getInstance().isLoggedIn()) {
startActivity(new Intent(this, LoginActivity.class));
finish();
return;
}
// Handle activity context
if (getIntent() == null) {
finish();
return;
}
// Parse input document
String documentJson = getIntent().getStringExtra("document");
if (documentJson == null) {
finish();
return;
}
try {
document = new JSONObject(documentJson);
} catch (JSONException e) {
finish();
return;
}
// Setup the activity
setContentView(R.layout.document_view_activity);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setHomeButtonEnabled(true);
}
// Fill the view
refreshDocument(document);
EventBus.getDefault().register(this);
}
/**
* Refresh the displayed document.
*
* @param document Document in JSON format
*/
private void refreshDocument(JSONObject document) {
this.document = document;
String title = document.optString("title");
String date = DateFormat.getDateFormat(this).format(new Date(document.optLong("create_date")));
String description = document.optString("description");
boolean shared = document.optBoolean("shared");
String language = document.optString("language");
JSONArray tags = document.optJSONArray("tags");
// Setup the title
setTitle(title);
Toolbar toolbar = (Toolbar) findViewById(R.id.action_bar);
TextView titleTextView = (TextView) toolbar.getChildAt(1);
if (titleTextView != null) {
titleTextView.setEllipsize(TextUtils.TruncateAt.MARQUEE);
titleTextView.setMarqueeRepeatLimit(-1);
titleTextView.setFocusable(true);
titleTextView.setFocusableInTouchMode(true);
}
// Fill the layout
TextView createdDateTextView = (TextView) findViewById(R.id.createdDateTextView);
createdDateTextView.setText(date);
TextView descriptionTextView = (TextView) findViewById(R.id.descriptionTextView);
if (description == null || description.isEmpty()) {
descriptionTextView.setVisibility(View.GONE);
} else {
descriptionTextView.setVisibility(View.VISIBLE);
descriptionTextView.setText(description);
}
TextView tagTextView = (TextView) findViewById(R.id.tagTextView);
if (tags.length() == 0) {
tagTextView.setVisibility(View.GONE);
} else {
tagTextView.setVisibility(View.VISIBLE);
tagTextView.setText(TagUtil.buildSpannable(tags));
}
ImageView languageImageView = (ImageView) findViewById(R.id.languageImageView);
languageImageView.setImageResource(getResources().getIdentifier(language, "drawable", getPackageName()));
ImageView sharedImageView = (ImageView) findViewById(R.id.sharedImageView);
sharedImageView.setVisibility(shared ? View.VISIBLE : View.GONE);
// Action edit document
Button button = (Button) findViewById(R.id.actionEditDocument);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(DocumentViewActivity.this, DocumentEditActivity.class);
intent.putExtra("document", DocumentViewActivity.this.document.toString());
startActivityForResult(intent, REQUEST_CODE_EDIT_DOCUMENT);
}
});
// Action upload file
button = (Button) findViewById(R.id.actionUploadFile);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT)
.setType("*/*")
.putExtra("android.intent.extra.ALLOW_MULTIPLE", true)
.addCategory(Intent.CATEGORY_OPENABLE);
startActivityForResult(Intent.createChooser(intent, getText(R.string.upload_from)), REQUEST_CODE_ADD_FILE);
}
});
// Action download document
button = (Button) findViewById(R.id.actionDownload);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
downloadZip();
}
});
// Action delete document
button = (Button) findViewById(R.id.actionDelete);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
deleteDocument();
}
});
// Action share
button = (Button) findViewById(R.id.actionSharing);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
DialogFragment dialog = DocShareFragment.newInstance(DocumentViewActivity.this.document.optString("id"));
dialog.show(getSupportFragmentManager(), "DocShareFragment");
}
});
// Grab the attached files
updateFiles();
// Grab the full document (used for ACLs and writable status)
updateDocument();
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.document_view_activity, menu);
this.menu = menu;
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.info:
DrawerLayout drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
if (drawerLayout.isDrawerVisible(GravityCompat.END)) {
drawerLayout.closeDrawer(GravityCompat.END);
} else {
drawerLayout.openDrawer(GravityCompat.END);
}
return true;
case R.id.download_file:
downloadCurrentFile();
return true;
case R.id.delete_file:
deleteCurrentFile();
return true;
case android.R.id.home:
finish();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
/**
* Download the current displayed file.
*/
private void downloadCurrentFile() {
if (fileViewPager == null || filePagerAdapter == null) return;
JSONObject file = filePagerAdapter.getObjectAt(fileViewPager.getCurrentItem());
if (file == null) return;
// Build the destination filename
String mimeType = file.optString("mimetype");
int position = fileViewPager.getCurrentItem();
if (mimeType == null || !mimeType.contains("/")) return;
String ext = mimeType.split("/")[1];
String fileName = getTitle() + "-" + position + "." + ext;
// Download the file
String fileUrl = PreferenceUtil.getServerUrl(this) + "/api/file/" + file.optString("id") + "/data";
downloadFile(fileUrl, fileName, getTitle().toString(), getString(R.string.downloading_file, position + 1));
}
private void deleteCurrentFile() {
if (fileViewPager == null || filePagerAdapter == null) return;
final JSONObject file = filePagerAdapter.getObjectAt(fileViewPager.getCurrentItem());
if (file == null) return;
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.delete_file_title)
.setMessage(R.string.delete_file_message)
.setCancelable(true)
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// Dismiss the confirmation dialog
dialog.dismiss();
// Show a progress dialog while deleting
final ProgressDialog progressDialog = ProgressDialog.show(DocumentViewActivity.this,
getString(R.string.please_wait),
getString(R.string.file_deleting_message), true, true,
new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
FileResource.cancel(DocumentViewActivity.this);
}
});
// Actual delete server call
final String fileId = file.optString("id");
FileResource.delete(DocumentViewActivity.this, fileId, new JsonHttpResponseHandler() {
@Override
public void onSuccess(int statusCode, Header[] headers, JSONObject response) {
EventBus.getDefault().post(new FileDeleteEvent(fileId));
}
@Override
public void onAllFailure(int statusCode, Header[] headers, byte[] responseBytes, Throwable throwable) {
Toast.makeText(DocumentViewActivity.this, R.string.file_delete_failure, Toast.LENGTH_LONG).show();
}
@Override
public void onFinish() {
progressDialog.dismiss();
}
});
}
})
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
}).create().show();
}
/**
* Download the document (all files zipped).
*/
private void downloadZip() {
if (document == null) return;
String url = PreferenceUtil.getServerUrl(this) + "/api/file/zip?id=" + document.optString("id");
String fileName = getTitle() + ".zip";
downloadFile(url, fileName, getTitle().toString(), getString(R.string.downloading_document));
}
/**
* Download a file using Android download manager.
*
* @param url URL to download
* @param fileName Destination file name
* @param title Notification title
* @param description Notification description
*/
private void downloadFile(String url, String fileName, String title, String description) {
String authToken = PreferenceUtil.getAuthToken(this);
DownloadManager downloadManager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
request.addRequestHeader("Cookie", "auth_token=" + authToken);
request.setTitle(title);
request.setDescription(description);
downloadManager.enqueue(request);
}
/**
* Delete the current document.
*/
private void deleteDocument() {
if (document == null) return;
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.delete_document_title)
.setMessage(R.string.delete_document_message)
.setCancelable(true)
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// Dismiss the confirmation dialog
dialog.dismiss();
// Show a progress dialog while deleting
final ProgressDialog progressDialog = ProgressDialog.show(DocumentViewActivity.this,
getString(R.string.please_wait),
getString(R.string.document_deleting_message), true, true,
new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
DocumentResource.cancel(DocumentViewActivity.this);
}
});
// Actual delete server call
final String documentId = document.optString("id");
DocumentResource.delete(DocumentViewActivity.this, documentId, new JsonHttpResponseHandler() {
@Override
public void onSuccess(int statusCode, Header[] headers, JSONObject response) {
EventBus.getDefault().post(new DocumentDeleteEvent(documentId));
}
@Override
public void onAllFailure(int statusCode, Header[] headers, byte[] responseBytes, Throwable throwable) {
Toast.makeText(DocumentViewActivity.this, R.string.document_delete_failure, Toast.LENGTH_LONG).show();
}
@Override
public void onFinish() {
progressDialog.dismiss();
}
});
}
})
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
}).create().show();
}
/**
* A document fullscreen event has been fired.
*
* @param event Document fullscreen event
*/
public void onEventMainThread(DocumentFullscreenEvent event) {
findViewById(R.id.detailLayout).setVisibility(event.isFullscreen() ? View.GONE : View.VISIBLE);
}
/**
* A document edit event has been fired.
*
* @param event Document edit event
*/
public void onEventMainThread(DocumentEditEvent event) {
if (document == null) return;
if (event.getDocument().optString("id").equals(document.optString("id"))) {
// The current document has been modified, refresh it
refreshDocument(event.getDocument());
}
}
/**
* A document delete event has been fired.
*
* @param event Document delete event
*/
public void onEventMainThread(DocumentDeleteEvent event) {
if (document == null) return;
if (event.getDocumentId().equals(document.optString("id"))) {
// The current document has been deleted, close this activity
finish();
}
}
/**
* A file delete event has been fired.
*
* @param event File delete event
*/
public void onEventMainThread(FileDeleteEvent event) {
if (filePagerAdapter == null) return;
filePagerAdapter.remove(event.getFileId());
final TextView filesEmptyView = (TextView) findViewById(R.id.filesEmptyView);
if (filePagerAdapter.getCount() == 0) filesEmptyView.setVisibility(View.VISIBLE);
}
/**
* A file add event has been fired.
*
* @param event File add event
*/
public void onEventMainThread(FileAddEvent event) {
if (document == null) return;
if (document.optString("id").equals(event.getDocumentId())) {
updateFiles();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (document == null) return;
if (requestCode == REQUEST_CODE_ADD_FILE && resultCode == RESULT_OK) {
List<Uri> uriList = new ArrayList<>();
// Single file upload
if (data.getData() != null) {
uriList.add(data.getData());
}
// Handle multiple file upload
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
ClipData clipData = data.getClipData();
if (clipData != null) {
for (int i = 0; i < clipData.getItemCount(); ++i) {
Uri uri = clipData.getItemAt(i).getUri();
if (uri != null) {
uriList.add(uri);
}
}
}
}
// Upload all files
for (Uri uri : uriList) {
Intent intent = new Intent(this, FileUploadService.class)
.putExtra(FileUploadService.PARAM_URI, uri)
.putExtra(FileUploadService.PARAM_DOCUMENT_ID, document.optString("id"));
startService(intent);
}
}
}
/**
* Update the document model.
*/
private void updateDocument() {
if (document == null) return;
// Silently get the document to know if it is writable by the current user
// If this call fails or is slow and the document is read-only,
// write actions will be allowed and will fail
DocumentResource.get(this, document.optString("id"), new JsonHttpResponseHandler() {
@Override
public void onSuccess(int statusCode, Header[] headers, JSONObject response) {
document = response;
boolean writable = document.optBoolean("writable");
if (menu != null) {
menu.findItem(R.id.delete_file).setVisible(writable);
}
findViewById(R.id.actionEditDocument).setVisibility(writable ? View.VISIBLE : View.INVISIBLE);
findViewById(R.id.actionUploadFile).setVisibility(writable ? View.VISIBLE : View.INVISIBLE);
findViewById(R.id.actionSharing).setVisibility(writable ? View.VISIBLE : View.INVISIBLE);
findViewById(R.id.actionDelete).setVisibility(writable ? View.VISIBLE : View.INVISIBLE);
// ACLs
ListView aclListView = (ListView) findViewById(R.id.aclListView);
aclListView.setAdapter(new AclListAdapter(document.optJSONArray("acls")));
}
});
}
/**
* Refresh files list.
*/
private void updateFiles() {
if (document == null) return;
final View progressBar = findViewById(R.id.progressBar);
final TextView filesEmptyView = (TextView) findViewById(R.id.filesEmptyView);
fileViewPager = (ViewPager) findViewById(R.id.fileViewPager);
fileViewPager.setOffscreenPageLimit(1);
fileViewPager.setAdapter(null);
progressBar.setVisibility(View.VISIBLE);
filesEmptyView.setVisibility(View.GONE);
FileResource.list(this, document.optString("id"), new JsonHttpResponseHandler() {
@Override
public void onSuccess(int statusCode, Header[] headers, JSONObject response) {
JSONArray files = response.optJSONArray("files");
filePagerAdapter = new FilePagerAdapter(DocumentViewActivity.this, files);
fileViewPager.setAdapter(filePagerAdapter);
progressBar.setVisibility(View.GONE);
if (files.length() == 0) filesEmptyView.setVisibility(View.VISIBLE);
}
@Override
public void onAllFailure(int statusCode, Header[] headers, byte[] responseBytes, Throwable throwable) {
filesEmptyView.setText(R.string.error_loading_files);
progressBar.setVisibility(View.GONE);
filesEmptyView.setVisibility(View.VISIBLE);
}
});
}
@Override
protected void onDestroy() {
EventBus.getDefault().unregister(this);
super.onDestroy();
}
}

View File

@@ -0,0 +1,184 @@
package com.sismics.docs.activity;
import android.content.Intent;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v7.app.AppCompatActivity;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import com.androidquery.AQuery;
import com.sismics.docs.R;
import com.sismics.docs.listener.CallbackListener;
import com.sismics.docs.listener.JsonHttpResponseHandler;
import com.sismics.docs.model.application.ApplicationContext;
import com.sismics.docs.resource.UserResource;
import com.sismics.docs.ui.form.Validator;
import com.sismics.docs.ui.form.validator.Required;
import com.sismics.docs.util.DialogUtil;
import com.sismics.docs.util.PreferenceUtil;
import org.apache.http.Header;
import org.json.JSONObject;
/**
* Login activity.
*
* @author bgamard
*/
public class LoginActivity extends AppCompatActivity {
/**
* User interface.
*/
private View loginForm;
private View progressBar;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.login_activity);
AQuery aq = new AQuery(this);
aq.id(R.id.loginExplain)
.text(Html.fromHtml(getString(R.string.login_explain)))
.getTextView()
.setMovementMethod(LinkMovementMethod.getInstance());
final EditText txtServer = aq.id(R.id.txtServer).getEditText();
final EditText txtUsername = aq.id(R.id.txtUsername).getEditText();
final EditText txtPassword = aq.id(R.id.txtPassword).getEditText();
final Button btnConnect = aq.id(R.id.btnConnect).getButton();
loginForm = aq.id(R.id.loginForm).getView();
progressBar = aq.id(R.id.progressBar).getView();
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
loginForm.setVisibility(View.GONE);
progressBar.setVisibility(View.VISIBLE);
// Form validation
final Validator validator = new Validator(this, false);
validator.addValidable(txtServer, new Required());
validator.addValidable(txtUsername, new Required());
validator.addValidable(txtPassword, new Required());
validator.setOnValidationChanged(new CallbackListener() {
@Override
public void onComplete() {
btnConnect.setEnabled(validator.isValidated());
}
});
// Preset saved server URL
String serverUrl = PreferenceUtil.getStringPreference(this, PreferenceUtil.PREF_SERVER_URL);
if (serverUrl != null) {
txtServer.setText(serverUrl);
}
tryConnect();
// Login button
btnConnect.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
loginForm.setVisibility(View.GONE);
progressBar.setVisibility(View.VISIBLE);
PreferenceUtil.setServerUrl(LoginActivity.this, txtServer.getText().toString());
try {
UserResource.login(getApplicationContext(), txtUsername.getText().toString(), txtPassword.getText().toString(), new JsonHttpResponseHandler() {
@Override
public void onSuccess(int statusCode, Header[] headers, JSONObject json) {
// Empty previous user caches
PreferenceUtil.resetUserCache(getApplicationContext());
// Getting user info and redirecting to main activity
ApplicationContext.getInstance().fetchUserInfo(LoginActivity.this, new CallbackListener() {
@Override
public void onComplete() {
Intent intent = new Intent(LoginActivity.this, MainActivity.class);
startActivity(intent);
finish();
}
});
}
@Override
public void onAllFailure(int statusCode, Header[] headers, byte[] responseBytes, Throwable throwable) {
loginForm.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.GONE);
if (responseBytes != null && new String(responseBytes).contains("\"ForbiddenError\"")) {
DialogUtil.showOkDialog(LoginActivity.this, R.string.login_fail_title, R.string.login_fail);
} else {
DialogUtil.showOkDialog(LoginActivity.this, R.string.network_error_title, R.string.network_error);
}
}
});
} catch (IllegalArgumentException e) {
// Given URL is not valid
loginForm.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.GONE);
PreferenceUtil.setServerUrl(LoginActivity.this, null);
DialogUtil.showOkDialog(LoginActivity.this, R.string.invalid_url_title, R.string.invalid_url);
}
}
});
}
/**
* Try to get a "session".
*/
private void tryConnect() {
String serverUrl = PreferenceUtil.getStringPreference(this, PreferenceUtil.PREF_SERVER_URL);
if (serverUrl == null) {
// Server URL is empty
loginForm.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.GONE);
return;
}
if (ApplicationContext.getInstance().isLoggedIn()) {
// If we are already connected (from cache data)
// redirecting to main activity
Intent intent = new Intent(LoginActivity.this, MainActivity.class);
startActivity(intent);
finish();
} else {
// Trying to get user data
UserResource.info(getApplicationContext(), new JsonHttpResponseHandler() {
@Override
public void onSuccess(int statusCode, Header[] headers, final JSONObject json) {
if (json.optBoolean("anonymous", true)) {
loginForm.setVisibility(View.VISIBLE);
return;
}
// Save user data in application context
ApplicationContext.getInstance().setUserInfo(getApplicationContext(), json);
// Redirecting to main activity
Intent intent = new Intent(LoginActivity.this, MainActivity.class);
startActivity(intent);
finish();
}
@Override
public void onAllFailure(int statusCode, Header[] headers, byte[] responseBytes, Throwable throwable) {
DialogUtil.showOkDialog(LoginActivity.this, R.string.network_error_title, R.string.network_error);
loginForm.setVisibility(View.VISIBLE);
}
@Override
public void onFinish() {
progressBar.setVisibility(View.GONE);
}
});
}
}
}

View File

@@ -0,0 +1,283 @@
package com.sismics.docs.activity;
import android.app.SearchManager;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import android.provider.SearchRecentSuggestions;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v7.app.AppCompatActivity;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.SearchView;
import android.widget.TextView;
import com.androidquery.util.AQUtility;
import com.sismics.docs.R;
import com.sismics.docs.adapter.TagListAdapter;
import com.sismics.docs.event.AdvancedSearchEvent;
import com.sismics.docs.event.SearchEvent;
import com.sismics.docs.fragment.SearchFragment;
import com.sismics.docs.listener.JsonHttpResponseHandler;
import com.sismics.docs.model.application.ApplicationContext;
import com.sismics.docs.provider.RecentSuggestionsProvider;
import com.sismics.docs.resource.TagResource;
import com.sismics.docs.resource.UserResource;
import com.sismics.docs.util.PreferenceUtil;
import org.apache.http.Header;
import org.json.JSONObject;
import de.greenrobot.event.EventBus;
/**
* Main activity.
*
* @author bgamard
*/
public class MainActivity extends AppCompatActivity {
private ActionBarDrawerToggle drawerToggle;
private MenuItem searchItem;
private DrawerLayout drawerLayout;
@Override
protected void onCreate(final Bundle args) {
super.onCreate(args);
// Check if logged in
if (!ApplicationContext.getInstance().isLoggedIn()) {
startActivity(new Intent(this, LoginActivity.class));
finish();
return;
}
// Setup the activity
setContentView(R.layout.main_activity);
// Enable ActionBar app icon to behave as action to toggle nav drawer
drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setHomeButtonEnabled(true);
}
// ActionBarDrawerToggle ties together the the proper interactions
// between the sliding drawer and the action bar app icon
drawerToggle = new ActionBarDrawerToggle(this, drawerLayout,
R.string.drawer_open, R.string.drawer_close);
drawerLayout.setDrawerListener(drawerToggle);
// Fill the drawer user info
JSONObject userInfo = ApplicationContext.getInstance().getUserInfo();
TextView usernameTextView = (TextView) findViewById(R.id.usernameTextView);
usernameTextView.setText(userInfo.optString("username"));
TextView emailTextView = (TextView) findViewById(R.id.emailTextView);
emailTextView.setText(userInfo.optString("email"));
// Get tag list to fill the drawer
final ListView tagListView = (ListView) findViewById(R.id.tagListView);
final View tagProgressView = findViewById(R.id.tagProgressView);
final TextView tagEmptyView = (TextView) findViewById(R.id.tagEmptyView);
tagListView.setEmptyView(tagProgressView);
JSONObject cacheTags = PreferenceUtil.getCachedJson(this, PreferenceUtil.PREF_CACHED_TAGS_JSON);
if (cacheTags != null) {
tagListView.setAdapter(new TagListAdapter(cacheTags.optJSONArray("stats")));
}
TagResource.stats(this, new JsonHttpResponseHandler() {
@Override
public void onSuccess(int statusCode, Header[] headers, JSONObject response) {
PreferenceUtil.setCachedJson(MainActivity.this, PreferenceUtil.PREF_CACHED_TAGS_JSON, response);
tagListView.setAdapter(new TagListAdapter(response.optJSONArray("stats")));
tagProgressView.setVisibility(View.GONE);
tagListView.setEmptyView(tagEmptyView);
}
@Override
public void onAllFailure(int statusCode, Header[] headers, byte[] responseBytes, Throwable throwable) {
tagEmptyView.setText(R.string.error_loading_tags);
tagProgressView.setVisibility(View.GONE);
tagListView.setEmptyView(tagEmptyView);
}
});
// Click on a tag
tagListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
TagListAdapter adapter = (TagListAdapter) tagListView.getAdapter();
if (adapter == null) return;
JSONObject tag = adapter.getItem(position);
if (tag == null) return;
searchQuery("tag:" + tag.optString("name"));
}
});
// Click on All documents
View allDocumentsLayout = findViewById(R.id.allDocumentsLayout);
allDocumentsLayout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
searchQuery(null);
}
});
// Click on Shared documents
View sharedDocumentsLayout = findViewById(R.id.sharedDocumentsLayout);
sharedDocumentsLayout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
searchQuery("shared:yes");
}
});
handleIntent(getIntent());
EventBus.getDefault().register(this);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.logout:
UserResource.logout(getApplicationContext(), new JsonHttpResponseHandler() {
@Override
public void onFinish() {
// Force logout in all cases, so the user is not stuck in case of network error
ApplicationContext.getInstance().setUserInfo(getApplicationContext(), null);
startActivity(new Intent(MainActivity.this, LoginActivity.class));
finish();
}
});
return true;
case R.id.advanced_search:
SearchFragment dialog = SearchFragment.newInstance();
dialog.show(getSupportFragmentManager(), "SearchFragment");
return true;
case R.id.settings:
startActivity(new Intent(MainActivity.this, SettingsActivity.class));
return true;
case android.R.id.home:
// The action bar home/up action should open or close the drawer.
// ActionBarDrawerToggle will take care of this.
if (drawerToggle.onOptionsItemSelected(item)) {
return true;
}
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
// Sync the toggle state after onRestoreInstanceState has occurred.
drawerToggle.syncState();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// Pass any configuration change to the drawer toggle
drawerToggle.onConfigurationChanged(newConfig);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.main_activity, menu);
// Get the SearchView and set the searchable configuration
SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
searchItem = menu.findItem(R.id.action_search);
SearchView searchView = (SearchView) searchItem.getActionView();
searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
searchView.setOnCloseListener(new SearchView.OnCloseListener() {
@Override
public boolean onClose() {
EventBus.getDefault().post(new SearchEvent(null));
return false;
}
});
return super.onCreateOptionsMenu(menu);
}
@Override
protected void onNewIntent(Intent intent) {
setIntent(intent);
handleIntent(intent);
}
/**
* Handle the incoming intent.
*
* @param intent Intent
*/
private void handleIntent(Intent intent) {
// Intent is consumed
setIntent(null);
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
// Perform a search query
String query = intent.getStringExtra(SearchManager.QUERY);
// Collapse the SearchView
if (searchItem != null) {
searchItem.collapseActionView();
}
// Save the query
SearchRecentSuggestions suggestions = new SearchRecentSuggestions(this, RecentSuggestionsProvider.AUTHORITY, RecentSuggestionsProvider.MODE);
suggestions.saveRecentQuery(query, null);
EventBus.getDefault().post(new SearchEvent(query));
}
}
/**
* Perform a search query.
*
* @param query Query
*/
private void searchQuery(String query) {
SearchView searchView = (SearchView) searchItem.getActionView();
searchView.setQuery(query, true);
searchView.setIconified(query == null);
searchView.clearFocus();
drawerLayout.closeDrawers();
}
/**
* An advanced search event has been fired.
*
* @param event Advanced search event
*/
public void onEventMainThread(AdvancedSearchEvent event) {
searchQuery(event.getQuery());
}
@Override
protected void onDestroy() {
EventBus.getDefault().unregister(this);
if(isTaskRoot()) {
int cacheSizeMb = PreferenceUtil.getIntegerPreference(this, PreferenceUtil.PREF_CACHE_SIZE, 10);
AQUtility.cleanCacheAsync(this, cacheSizeMb * 1000000, cacheSizeMb * 1000000);
}
super.onDestroy();
}
}

View File

@@ -0,0 +1,41 @@
package com.sismics.docs.activity;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.MenuItem;
import com.sismics.docs.fragment.SettingsFragment;
/**
* Settings activity.
*
* @author bgamard.
*/
public class SettingsActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setHomeButtonEnabled(true);
}
// Display the fragment as the main content.
getFragmentManager().beginTransaction()
.replace(android.R.id.content, new SettingsFragment())
.commit();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}

View File

@@ -0,0 +1,77 @@
package com.sismics.docs.adapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
import com.sismics.docs.R;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
/**
* ACL list adapter.
*
* @author bgamard.
*/
public class AclListAdapter extends BaseAdapter {
/**
* Shares.
*/
private List<JSONObject> acls;
/**
* ACL list adapter.
*
* @param acls ACLs
*/
public AclListAdapter(JSONArray acls) {
this.acls = new ArrayList<>();
// Extract only share ACLs
for (int i = 0; i < acls.length(); i++) {
JSONObject acl = acls.optJSONObject(i);
this.acls.add(acl);
}
}
@Override
public int getCount() {
return acls.size();
}
@Override
public JSONObject getItem(int position) {
return acls.get(position);
}
@Override
public long getItemId(int position) {
return getItem(position).optString("id").hashCode();
}
@Override
public View getView(int position, View view, final ViewGroup parent) {
if (view == null) {
LayoutInflater vi = (LayoutInflater) parent.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = vi.inflate(R.layout.acl_list_item, parent, false);
}
// Fill the view
final JSONObject acl = getItem(position);
TextView typeTextView = (TextView) view.findViewById(R.id.typeTextView);
typeTextView.setText(acl.optString("type"));
TextView nameTextView = (TextView) view.findViewById(R.id.nameTextView);
nameTextView.setText(acl.optString("name"));
TextView permTextView = (TextView) view.findViewById(R.id.permTextView);
permTextView.setText(acl.optString("perm"));
return view;
}
}

View File

@@ -0,0 +1,158 @@
package com.sismics.docs.adapter;
import android.support.v7.widget.RecyclerView;
import android.text.format.DateFormat;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.sismics.docs.R;
import com.sismics.docs.util.TagUtil;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* Adapter of documents.
*
* @author bgamard
*/
public class DocListAdapter extends RecyclerView.Adapter<DocListAdapter.ViewHolder> {
/**
* Displayed documents.
*/
private List<JSONObject> documents;
/**
* ViewHolder.
*/
public static class ViewHolder extends RecyclerView.ViewHolder {
public TextView titleTextView;
public TextView subtitleTextView;
public TextView dateTextView;
public ImageView sharedImageView;
public ViewHolder(View v) {
super(v);
titleTextView = (TextView) v.findViewById(R.id.titleTextView);
subtitleTextView = (TextView) v.findViewById(R.id.subtitleTextView);
dateTextView = (TextView) v.findViewById(R.id.dateTextView);
sharedImageView = (ImageView) v.findViewById(R.id.sharedImageView);
}
}
/**
* Default constructor.
*/
public DocListAdapter() {
// Nothing
}
@Override
public DocListAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).
inflate(R.layout.doc_list_item, parent, false);
return new ViewHolder(v);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
JSONObject document = documents.get(position);
holder.titleTextView.setText(document.optString("title"));
JSONArray tags = document.optJSONArray("tags");
holder.subtitleTextView.setText(TagUtil.buildSpannable(tags));
String date = DateFormat.getDateFormat(holder.dateTextView.getContext()).format(new Date(document.optLong("create_date")));
holder.dateTextView.setText(date);
holder.sharedImageView.setVisibility(document.optBoolean("shared") ? View.VISIBLE : View.GONE);
}
@Override
public int getItemCount() {
if (documents == null) {
return 0;
}
return documents.size();
}
/**
* Return an item at a given position.
*
* @param position Item position
* @return Item
*/
public JSONObject getItemAt(int position) {
if (documents == null) {
return null;
}
return documents.get(position);
}
/**
* Clear the documents.
*/
public void clearDocuments() {
documents = new ArrayList<>();
notifyDataSetChanged();
}
/**
* Add documents to display.
*
* @param documents Documents
*/
public void addDocuments(JSONArray documents) {
if (this.documents == null) {
this.documents = new ArrayList<>();
}
for (int i = 0; i < documents.length(); i++) {
this.documents.add(documents.optJSONObject(i));
}
notifyDataSetChanged();
}
/**
* Update a document.
*
* @param document Document
*/
public void updateDocument(JSONObject document) {
for (int i = 0; i < documents.size(); i++) {
JSONObject currentDoc = documents.get(i);
if (currentDoc.optString("id").equals(document.optString("id"))) {
// This document has been modified
documents.set(i, document);
notifyDataSetChanged();
}
}
}
/**
* Delete a document.
*
* @param documentId Document ID
*/
public void deleteDocument(String documentId) {
for (int i = 0; i < documents.size(); i++) {
JSONObject currentDoc = documents.get(i);
if (currentDoc.optString("id").equals(documentId)) {
// This document has been deleted
documents.remove(i);
notifyDataSetChanged();
}
}
}
}

View File

@@ -0,0 +1,140 @@
package com.sismics.docs.adapter;
import android.content.Context;
import android.support.v4.view.PagerAdapter;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import com.androidquery.AQuery;
import com.androidquery.callback.BitmapAjaxCallback;
import com.sismics.docs.R;
import com.sismics.docs.util.PreferenceUtil;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
import it.sephiroth.android.library.imagezoom.ImageViewTouch;
import it.sephiroth.android.library.imagezoom.ImageViewTouchBase;
/**
* @author bgamard.
*/
public class FilePagerAdapter extends PagerAdapter {
/**
* Files list.
*/
private List<JSONObject> files;
/**
* AQuery.
*/
private AQuery aq;
/**
* Context.
*/
private Context context;
/**
* Auth token used to download files.
*/
private String authToken;
/**
* File pager adapter.
*
* @param context Context
* @param filesArray Files
*/
public FilePagerAdapter(Context context, JSONArray filesArray) {
this.files = new ArrayList<>();
for (int i = 0; i < filesArray.length(); i++) {
files.add(filesArray.optJSONObject(i));
}
this.context = context;
this.authToken = PreferenceUtil.getAuthToken(context);
aq = new AQuery(context);
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
View view = LayoutInflater.from(container.getContext()).inflate(R.layout.file_viewpager_item, container, false);
ImageViewTouch fileImageView = (ImageViewTouch) view.findViewById(R.id.fileImageView);
ProgressBar progressBar = (ProgressBar) view.findViewById(R.id.fileProgressBar);
JSONObject file = files.get(position);
String fileUrl = PreferenceUtil.getServerUrl(context) + "/api/file/" + file.optString("id") + "/data?size=web";
aq.id(fileImageView)
.image(new BitmapAjaxCallback()
.url(fileUrl)
.progress(progressBar)
.animation(AQuery.FADE_IN_NETWORK)
.cookie("auth_token", authToken));
fileImageView.setDisplayType(ImageViewTouchBase.DisplayType.FIT_TO_SCREEN);
container.addView(view, 0);
return view;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View) object);
}
@Override
public int getCount() {
if (files == null) {
return 0;
}
return files.size();
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
/**
* Return the object at a given position.
*
* @param position Position
* @return Object
*/
public JSONObject getObjectAt(int position) {
if (files == null) {
return null;
}
return files.get(position);
}
/**
* Remove a file.
*
* @param fileId File ID
*/
public void remove(String fileId) {
if (files == null || fileId == null) return;
for (JSONObject file : files) {
if (fileId.equals(file.optString("id"))) {
files.remove(file);
notifyDataSetChanged();
break;
}
}
}
@Override
public int getItemPosition(Object object) {
return POSITION_NONE;
}
}

View File

@@ -0,0 +1,114 @@
package com.sismics.docs.adapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
import com.sismics.docs.R;
import java.util.ArrayList;
import java.util.List;
/**
* Languages adapter.
*
* @author bgamard.
*/
public class LanguageAdapter extends BaseAdapter {
/**
* Context.
*/
private Context context;
private List<Language> languageList;
public LanguageAdapter(Context context, boolean noValue) {
this.context = context;
this.languageList = new ArrayList<>();
if (noValue) {
languageList.add(new Language("", R.string.all_languages, 0));
}
languageList.add(new Language("fra", R.string.language_french, R.drawable.fra));
languageList.add(new Language("eng", R.string.language_english, R.drawable.eng));
languageList.add(new Language("jpn", R.string.language_japanese, R.drawable.jpn));
}
@Override
public int getCount() {
return languageList.size();
}
@Override
public Language getItem(int position) {
if (position >= languageList.size()) {
return null;
}
return languageList.get(position);
}
@Override
public long getItemId(int position) {
return getItem(position).id.hashCode();
}
@Override
public View getView(int position, View view, ViewGroup parent) {
if (view == null) {
LayoutInflater vi = (LayoutInflater) parent.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = vi.inflate(R.layout.language_list_item, parent, false);
}
// Fill the view
Language language = getItem(position);
TextView languageTextView = (TextView) view.findViewById(R.id.languageTextView);
languageTextView.setText(context.getText(language.name));
languageTextView.setCompoundDrawablesWithIntrinsicBounds(language.drawable, 0, 0, 0);
return view;
}
/**
* Return the position of a language.
* 0 if it doesn't exists.
*
* @param languageId Language ID
* @return Position
*/
public int getItemPosition(String languageId) {
for (Language language : languageList) {
if (language.id.equals(languageId)) {
return languageList.indexOf(language);
}
}
return 0;
}
/**
* A language.
*/
public static class Language {
private String id;
private int name;
private int drawable;
/**
* A language.
*
* @param id Language ID
* @param name Language name
* @param drawable Language drawable
*/
public Language(String id, int name, int drawable) {
this.id = id;
this.name = name;
this.drawable = drawable;
}
public String getId() {
return id;
}
}
}

View File

@@ -0,0 +1,99 @@
package com.sismics.docs.adapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageButton;
import android.widget.TextView;
import com.sismics.docs.R;
import com.sismics.docs.event.ShareDeleteEvent;
import com.sismics.docs.event.ShareSendEvent;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
import de.greenrobot.event.EventBus;
/**
* Share list adapter.
*
* @author bgamard.
*/
public class ShareListAdapter extends BaseAdapter {
/**
* Shares.
*/
private List<JSONObject> acls;
/**
* Share list adapter.
*
* @param acls ACLs
*/
public ShareListAdapter(JSONArray acls) {
this.acls = new ArrayList<>();
// Extract only share ACLs
for (int i = 0; i < acls.length(); i++) {
JSONObject acl = acls.optJSONObject(i);
if (acl.optString("type").equals("SHARE")) {
this.acls.add(acl);
}
}
}
@Override
public int getCount() {
return acls.size();
}
@Override
public JSONObject getItem(int position) {
return acls.get(position);
}
@Override
public long getItemId(int position) {
return getItem(position).optString("id").hashCode();
}
@Override
public View getView(int position, View view, final ViewGroup parent) {
if (view == null) {
LayoutInflater vi = (LayoutInflater) parent.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = vi.inflate(R.layout.share_list_item, parent, false);
}
// Fill the view
final JSONObject acl = getItem(position);
String name = acl.optString("name");
TextView shareTextView = (TextView) view.findViewById(R.id.shareTextView);
shareTextView.setText(name.isEmpty() ? parent.getContext().getString(R.string.share_default_name) : name);
// Delete a share
ImageButton shareDeleteButton = (ImageButton) view.findViewById(R.id.shareDeleteButton);
shareDeleteButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
EventBus.getDefault().post(new ShareDeleteEvent(acl.optString("id")));
}
});
// Send the link
ImageButton shareSendButton = (ImageButton) view.findViewById(R.id.shareSendButton);
shareSendButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
EventBus.getDefault().post(new ShareSendEvent(acl));
}
});
return view;
}
}

View File

@@ -0,0 +1,53 @@
package com.sismics.docs.adapter;
import android.content.Context;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.sismics.docs.R;
import com.tokenautocomplete.FilteredArrayAdapter;
import org.json.JSONObject;
import java.util.List;
/**
* Tag auto-complete adapter.
*
* @author bgamard.
*/
public class TagAutoCompleteAdapter extends FilteredArrayAdapter<JSONObject> {
public TagAutoCompleteAdapter(Context context, int resource, List<JSONObject> objects) {
super(context, resource, objects);
}
@Override
public View getView(int position, View view, ViewGroup parent) {
if (view == null) {
LayoutInflater vi = (LayoutInflater) parent.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = vi.inflate(R.layout.tag_autocomplete_item, parent, false);
}
// Fill the view
JSONObject tag = getItem(position);
TextView textView = (TextView) view;
textView.setText(tag.optString("name"));
Drawable drawable = textView.getCompoundDrawables()[0].mutate();
drawable.setColorFilter(Color.parseColor(tag.optString("color")), PorterDuff.Mode.MULTIPLY);
textView.setCompoundDrawables(drawable, null, null, null);
textView.invalidate();
return view;
}
@Override
protected boolean keepObject(JSONObject tag, String s) {
return tag.optString("name").toLowerCase().startsWith(s.toLowerCase());
}
}

View File

@@ -0,0 +1,93 @@
package com.sismics.docs.adapter;
import android.content.Context;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import com.sismics.docs.R;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* Tag list adapter.
*
* @author bgamard.
*/
public class TagListAdapter extends BaseAdapter {
/**
* Tags.
*/
private List<JSONObject> tags;
/**
* Tag list adapter.
*
* @param tagsArray Tags
*/
public TagListAdapter(JSONArray tagsArray) {
this.tags = new ArrayList<>();
for (int i = 0; i < tagsArray.length(); i++) {
tags.add(tagsArray.optJSONObject(i));
}
// Sort tags by count desc
Collections.sort(tags, new Comparator<JSONObject>() {
@Override
public int compare(JSONObject lhs, JSONObject rhs) {
return lhs.optInt("count") < rhs.optInt("count") ? 1 : -1;
}
});
}
@Override
public int getCount() {
return tags.size();
}
@Override
public JSONObject getItem(int position) {
return tags.get(position);
}
@Override
public long getItemId(int position) {
return getItem(position).optString("id").hashCode();
}
@Override
public View getView(int position, View view, ViewGroup parent) {
if (view == null) {
LayoutInflater vi = (LayoutInflater) parent.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = vi.inflate(R.layout.tag_list_item, parent, false);
}
// Fill the view
JSONObject tag = getItem(position);
TextView tagTextView = (TextView) view.findViewById(R.id.tagTextView);
tagTextView.setText(tag.optString("name"));
TextView tagCountTextView = (TextView) view.findViewById(R.id.tagCountTextView);
tagCountTextView.setText(tag.optString("count"));
// Label color filtering
ImageView labelImageView = (ImageView) view.findViewById(R.id.labelImageView);
Drawable labelDrawable = labelImageView.getDrawable().mutate();
labelDrawable.setColorFilter(Color.parseColor(tag.optString("color")), PorterDuff.Mode.MULTIPLY);
labelImageView.setImageDrawable(labelDrawable);
labelImageView.invalidate();
return view;
}
}

View File

@@ -1,55 +0,0 @@
package com.sismics.docs.dummy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Helper class for providing sample content for user interfaces created by
* Android template wizards.
* <p>
* TODO: Replace all uses of this class before publishing your app.
*/
public class DummyContent {
/**
* An array of sample (dummy) items.
*/
public static List<DummyItem> ITEMS = new ArrayList<DummyItem>();
/**
* A map of sample (dummy) items, by ID.
*/
public static Map<String, DummyItem> ITEM_MAP = new HashMap<String, DummyItem>();
static {
// Add 3 sample items.
addItem(new DummyItem("1", "Item 1"));
addItem(new DummyItem("2", "Item 2"));
addItem(new DummyItem("3", "Item 3"));
}
private static void addItem(DummyItem item) {
ITEMS.add(item);
ITEM_MAP.put(item.id, item);
}
/**
* A dummy item representing a piece of content.
*/
public static class DummyItem {
public String id;
public String content;
public DummyItem(String id, String content) {
this.id = id;
this.content = content;
}
@Override
public String toString() {
return content;
}
}
}

View File

@@ -0,0 +1,31 @@
package com.sismics.docs.event;
/**
* Advanced search event.
*
* @author bgamard.
*/
public class AdvancedSearchEvent {
/**
* Search query.
*/
private String query;
/**
* Create an advanced search event.
*
* @param query Query
*/
public AdvancedSearchEvent(String query) {
this.query = query;
}
/**
* Getter of query.
*
* @return query
*/
public String getQuery() {
return query;
}
}

View File

@@ -0,0 +1,33 @@
package com.sismics.docs.event;
import org.json.JSONObject;
/**
* Document add event.
*
* @author bgamard.
*/
public class DocumentAddEvent {
/**
* Document.
*/
private JSONObject document;
/**
* Create a document add event.
*
* @param document Document
*/
public DocumentAddEvent(JSONObject document) {
this.document = document;
}
/**
* Getter of document.
*
* @return document
*/
public JSONObject getDocument() {
return document;
}
}

View File

@@ -0,0 +1,31 @@
package com.sismics.docs.event;
/**
* Document delete event.
*
* @author bgamard.
*/
public class DocumentDeleteEvent {
/**
* Document ID.
*/
private String documentId;
/**
* Create a document delete event.
*
* @param documentId Document ID
*/
public DocumentDeleteEvent(String documentId) {
this.documentId = documentId;
}
/**
* Getter of documentId.
*
* @return documentId
*/
public String getDocumentId() {
return documentId;
}
}

View File

@@ -0,0 +1,33 @@
package com.sismics.docs.event;
import org.json.JSONObject;
/**
* Document edit event.
*
* @author bgamard.
*/
public class DocumentEditEvent {
/**
* Document.
*/
private JSONObject document;
/**
* Create a document edit event.
*
* @param document Document
*/
public DocumentEditEvent(JSONObject document) {
this.document = document;
}
/**
* Getter of document.
*
* @return document
*/
public JSONObject getDocument() {
return document;
}
}

View File

@@ -0,0 +1,17 @@
package com.sismics.docs.event;
/**
* @author bgamard.
*/
public class DocumentFullscreenEvent {
private boolean fullscreen;
public DocumentFullscreenEvent(boolean fullscreen) {
this.fullscreen = fullscreen;
}
public boolean isFullscreen() {
return fullscreen;
}
}

View File

@@ -0,0 +1,45 @@
package com.sismics.docs.event;
/**
* File add event.
*
* @author bgamard.
*/
public class FileAddEvent {
/**
* Document ID.
*/
private String documentId;
/**
* File ID.
*/
private String fileId;
/**
* Create a file add event.
*
* @param fileId File ID
*/
public FileAddEvent(String documentId, String fileId) {
this.documentId = documentId;
this.fileId = fileId;
}
/**
* Getter of fileId.
*
* @return fileId
*/
public String getFileId() {
return fileId;
}
/**
* Getter of documentId.
*
* @return documentId
*/
public String getDocumentId() {
return documentId;
}
}

View File

@@ -0,0 +1,31 @@
package com.sismics.docs.event;
/**
* File delete event.
*
* @author bgamard.
*/
public class FileDeleteEvent {
/**
* File ID.
*/
private String fileId;
/**
* Create a document delete event.
*
* @param fileId File ID
*/
public FileDeleteEvent(String fileId) {
this.fileId = fileId;
}
/**
* Getter of fileId.
*
* @return fileId
*/
public String getFileId() {
return fileId;
}
}

View File

@@ -0,0 +1,31 @@
package com.sismics.docs.event;
/**
* Search event.
*
* @author bgamard.
*/
public class SearchEvent {
/**
* Search query.
*/
private String query;
/**
* Create a search event.
*
* @param query Query
*/
public SearchEvent(String query) {
this.query = query;
}
/**
* Getter of query.
*
* @return query
*/
public String getQuery() {
return query;
}
}

View File

@@ -0,0 +1,26 @@
package com.sismics.docs.event;
/**
* Share delete event.
*
* @author bgamard.
*/
public class ShareDeleteEvent {
/**
* Share ID
*/
private String id;
/**
* Create a share delete event.
*
* @param id Share ID
*/
public ShareDeleteEvent(String id) {
this.id = id;
}
public String getId() {
return id;
}
}

View File

@@ -0,0 +1,28 @@
package com.sismics.docs.event;
import org.json.JSONObject;
/**
* Share send event.
*
* @author bgamard.
*/
public class ShareSendEvent {
/**
* ACL data.
*/
private JSONObject acl;
/**
* Create a share send event.
*
* @param acl ACL data
*/
public ShareSendEvent(JSONObject acl) {
this.acl = acl;
}
public JSONObject getAcl() {
return acl;
}
}

View File

@@ -0,0 +1,246 @@
package com.sismics.docs.fragment;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
import com.sismics.docs.R;
import com.sismics.docs.activity.DocumentEditActivity;
import com.sismics.docs.activity.DocumentViewActivity;
import com.sismics.docs.adapter.DocListAdapter;
import com.sismics.docs.event.DocumentAddEvent;
import com.sismics.docs.event.DocumentDeleteEvent;
import com.sismics.docs.event.DocumentEditEvent;
import com.sismics.docs.event.SearchEvent;
import com.sismics.docs.listener.JsonHttpResponseHandler;
import com.sismics.docs.listener.RecyclerItemClickListener;
import com.sismics.docs.resource.DocumentResource;
import com.sismics.docs.ui.view.DividerItemDecoration;
import com.sismics.docs.ui.view.EmptyRecyclerView;
import org.apache.http.Header;
import org.json.JSONObject;
import de.greenrobot.event.EventBus;
/**
* @author bgamard.
*/
public class DocListFragment extends Fragment {
/**
* Documents adapter.
*/
private DocListAdapter adapter;
/**
* Search query.
*/
private String query;
/**
* Request code of adding document.
*/
private static final int REQUEST_CODE_ADD_DOCUMENT = 1;
// View cache
private EmptyRecyclerView recyclerView;
private SwipeRefreshLayout swipeRefreshLayout;
// Infinite scrolling things
private boolean loading = true;
private int previousTotal = 0;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.doc_list_fragment, container, false);
// Configure the RecyclerView
recyclerView = (EmptyRecyclerView) view.findViewById(R.id.docList);
adapter = new DocListAdapter();
recyclerView.setAdapter(adapter);
recyclerView.setHasFixedSize(true);
recyclerView.setLongClickable(true);
recyclerView.addItemDecoration(new DividerItemDecoration(getResources().getDrawable(R.drawable.abc_list_divider_mtrl_alpha)));
// Configure the LayoutManager
final LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
recyclerView.setLayoutManager(layoutManager);
// Configure the swipe refresh layout
swipeRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.swipeRefreshLayout);
swipeRefreshLayout.setColorSchemeResources(android.R.color.holo_blue_bright,
android.R.color.holo_green_light,
android.R.color.holo_orange_light,
android.R.color.holo_red_light);
swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
loadDocuments(view, true);
}
});
// Document opening
recyclerView.addOnItemTouchListener(new RecyclerItemClickListener(getActivity(), new RecyclerItemClickListener.OnItemClickListener() {
@Override
public void onItemClick(View view, int position) {
JSONObject document = adapter.getItemAt(position);
if (document != null) {
openDocument(document);
}
}
}));
// Infinite scrolling
recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int visibleItemCount = recyclerView.getChildCount();
int totalItemCount = layoutManager.getItemCount();
int firstVisibleItem = layoutManager.findFirstVisibleItemPosition();
if (loading) {
if (totalItemCount > previousTotal) {
loading = false;
previousTotal = totalItemCount;
}
}
if (!loading && totalItemCount - visibleItemCount <= firstVisibleItem + 3) {
loadDocuments(getView(), false);
loading = true;
}
}
});
// Add document button
ImageButton addDocumentButton = (ImageButton) view.findViewById(R.id.addDocumentButton);
addDocumentButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(getActivity(), DocumentEditActivity.class);
startActivityForResult(intent, REQUEST_CODE_ADD_DOCUMENT);
}
});
// Grab the documents
loadDocuments(view, true);
EventBus.getDefault().register(this);
return view;
}
@Override
public void onDestroyView() {
EventBus.getDefault().unregister(this);
super.onDestroyView();
}
/**
* A search event has been fired.
*
* @param event Search event
*/
public void onEventMainThread(SearchEvent event) {
query = event.getQuery();
loadDocuments(getView(), true);
}
/**
* A document edit event has been fired.
*
* @param event Document edit event
*/
public void onEventMainThread(DocumentEditEvent event) {
adapter.updateDocument(event.getDocument());
}
/**
* A document delete event has been fired.
*
* @param event Document delete event
*/
public void onEventMainThread(DocumentDeleteEvent event) {
adapter.deleteDocument(event.getDocumentId());
}
/**
* A document add event has been fired.
*
* @param event Document add event
*/
public void onEventMainThread(DocumentAddEvent event) {
// Refresh the list, maybe the new document fit in it
loadDocuments(getView(), true);
// Open the newly created document
openDocument(event.getDocument());
}
/**
* Open a document.
*
* @param document Document to open
*/
private void openDocument(JSONObject document) {
Intent intent = new Intent(getActivity(), DocumentViewActivity.class);
intent.putExtra("document", document.toString());
startActivity(intent);
}
/**
* Refresh the document list.
*
* @param view View
* @param reset If true, reload the documents
*/
private void loadDocuments(final View view, final boolean reset) {
if (view == null) return;
final View progressBar = view.findViewById(R.id.progressBar);
final TextView documentsEmptyView = (TextView) view.findViewById(R.id.documentsEmptyView);
if (reset) {
loading = true;
previousTotal = 0;
adapter.clearDocuments();
} else {
swipeRefreshLayout.setRefreshing(true);
}
recyclerView.setEmptyView(progressBar);
DocumentResource.list(getActivity(), reset ? 0 : adapter.getItemCount(), query, new JsonHttpResponseHandler() {
@Override
public void onSuccess(int statusCode, Header[] headers, JSONObject response) {
adapter.addDocuments(response.optJSONArray("documents"));
documentsEmptyView.setText(R.string.no_documents);
recyclerView.setEmptyView(documentsEmptyView);
}
@Override
public void onAllFailure(int statusCode, Header[] headers, byte[] responseBytes, Throwable throwable) {
documentsEmptyView.setText(R.string.error_loading_documents);
recyclerView.setEmptyView(documentsEmptyView);
if (!reset) {
// We are loading a new page, so the empty view won't be visible, pop a toast
Toast.makeText(getActivity(), R.string.error_loading_documents, Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFinish() {
swipeRefreshLayout.setRefreshing(false);
}
});
}
}

View File

@@ -0,0 +1,186 @@
package com.sismics.docs.fragment;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.DialogFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.sismics.docs.R;
import com.sismics.docs.adapter.ShareListAdapter;
import com.sismics.docs.event.ShareDeleteEvent;
import com.sismics.docs.event.ShareSendEvent;
import com.sismics.docs.listener.JsonHttpResponseHandler;
import com.sismics.docs.resource.DocumentResource;
import com.sismics.docs.resource.ShareResource;
import com.sismics.docs.util.PreferenceUtil;
import org.apache.http.Header;
import org.json.JSONArray;
import org.json.JSONObject;
import de.greenrobot.event.EventBus;
/**
* Document sharing dialog fragment.
*
* @author bgamard.
*/
public class DocShareFragment extends DialogFragment {
/**
* Document data.
*/
private JSONObject document;
/**
* Document sharing dialog fragment
* @param id Document ID
*/
public static DocShareFragment newInstance(String id) {
DocShareFragment fragment = new DocShareFragment();
Bundle args = new Bundle();
args.putString("id", id);
fragment.setArguments(args);
return fragment;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
// Setup the view
LayoutInflater inflater = getActivity().getLayoutInflater();
View view = inflater.inflate(R.layout.document_share_dialog, null);
final Button shareAddButton = (Button) view.findViewById(R.id.shareAddButton);
final EditText shareNameEditText = (EditText) view.findViewById(R.id.shareNameEditText);
// Add a share
shareAddButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
shareNameEditText.setEnabled(false);
shareAddButton.setEnabled(false);
ShareResource.add(getActivity(), getArguments().getString("id"), shareNameEditText.getText().toString(),
new JsonHttpResponseHandler() {
@Override
public void onSuccess(int statusCode, Header[] headers, JSONObject response) {
shareNameEditText.setText("");
loadShares(getDialog().getWindow().getDecorView());
}
@Override
public void onAllFailure(int statusCode, Header[] headers, byte[] responseBytes, Throwable throwable) {
Toast.makeText(getActivity(), R.string.error_adding_share, Toast.LENGTH_SHORT).show();
}
@Override
public void onFinish() {
shareNameEditText.setEnabled(true);
shareAddButton.setEnabled(true);
}
});
}
});
// Get the shares
loadShares(view);
// Build the dialog
builder.setView(view)
.setNegativeButton(R.string.close, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
getDialog().cancel();
}
});
return builder.create();
}
/**
* Load the shares.
*
* @param view View
*/
private void loadShares(View view) {
if (isDetached()) return;
final ListView shareListView = (ListView) view.findViewById(R.id.shareListView);
final TextView shareEmptyView = (TextView) view.findViewById(R.id.shareEmptyView);
final ProgressBar shareProgressBar = (ProgressBar) view.findViewById(R.id.shareProgressBar);
shareListView.setEmptyView(shareProgressBar);
DocumentResource.get(getActivity(), getArguments().getString("id"), new JsonHttpResponseHandler() {
@Override
public void onSuccess(int statusCode, Header[] headers, JSONObject response) {
document = response;
JSONArray acls = response.optJSONArray("acls");
shareProgressBar.setVisibility(View.GONE);
shareListView.setEmptyView(shareEmptyView);
shareListView.setAdapter(new ShareListAdapter(acls));
}
@Override
public void onAllFailure(int statusCode, Header[] headers, byte[] responseBytes, Throwable throwable) {
getDialog().cancel();
Toast.makeText(getActivity(), R.string.error_loading_shares, Toast.LENGTH_SHORT).show();
}
});
}
public void onEventMainThread(ShareDeleteEvent event) {
ShareResource.delete(getActivity(), event.getId(), new JsonHttpResponseHandler() {
@Override
public void onSuccess(int statusCode, Header[] headers, JSONObject response) {
loadShares(getDialog().getWindow().getDecorView());
}
@Override
public void onAllFailure(int statusCode, Header[] headers, byte[] responseBytes, Throwable throwable) {
Toast.makeText(getActivity(), R.string.error_deleting_share, Toast.LENGTH_SHORT).show();
}
});
}
public void onEventMainThread(ShareSendEvent event) {
if (document == null) return;
// Build the share link
String serverUrl = PreferenceUtil.getServerUrl(getActivity());
String link = serverUrl + "/share.html#/share/" + document.optString("id") + "/" + event.getAcl().optString("id");
// Build the intent
Context context = getActivity();
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_SUBJECT, document.optString("title"));
intent.putExtra(Intent.EXTRA_TEXT, link);
intent.setType("text/plain");
// Open the target chooser
context.startActivity(Intent.createChooser(intent, context.getText(R.string.send_share_to)));
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EventBus.getDefault().register(this);
}
@Override
public void onDestroy() {
EventBus.getDefault().unregister(this);
super.onDestroy();
}
}

View File

@@ -0,0 +1,128 @@
package com.sismics.docs.fragment;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.DialogFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Spinner;
import com.sismics.docs.R;
import com.sismics.docs.adapter.LanguageAdapter;
import com.sismics.docs.adapter.TagAutoCompleteAdapter;
import com.sismics.docs.event.AdvancedSearchEvent;
import com.sismics.docs.ui.view.DatePickerView;
import com.sismics.docs.ui.view.TagsCompleteTextView;
import com.sismics.docs.util.PreferenceUtil;
import com.sismics.docs.util.SearchQueryBuilder;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
import de.greenrobot.event.EventBus;
/**
* Advanced search fragment.
*
* @author bgamard.
*/
public class SearchFragment extends DialogFragment {
/**
* Document sharing dialog fragment
*/
public static SearchFragment newInstance() {
SearchFragment fragment = new SearchFragment();
Bundle args = new Bundle();
fragment.setArguments(args);
return fragment;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
// Setup the view
LayoutInflater inflater = getActivity().getLayoutInflater();
View view = inflater.inflate(R.layout.search_dialog, null);
final EditText searchEditText = (EditText) view.findViewById(R.id.searchEditText);
final EditText fulltextEditText = (EditText) view.findViewById(R.id.fulltextEditText);
final CheckBox sharedCheckbox = (CheckBox) view.findViewById(R.id.sharedCheckbox);
final Spinner languageSpinner = (Spinner) view.findViewById(R.id.languageSpinner);
final DatePickerView beforeDatePicker = (DatePickerView) view.findViewById(R.id.beforeDatePicker);
final DatePickerView afterDatePicker = (DatePickerView) view.findViewById(R.id.afterDatePicker);
final TagsCompleteTextView tagsEditText = (TagsCompleteTextView) view.findViewById(R.id.tagsEditText);
// Language spinner
LanguageAdapter languageAdapter = new LanguageAdapter(getActivity(), true);
languageSpinner.setAdapter(languageAdapter);
// Tags auto-complete
JSONObject tags = PreferenceUtil.getCachedJson(getActivity(), PreferenceUtil.PREF_CACHED_TAGS_JSON);
if (tags == null) {
Dialog dialog = builder.create();
dialog.cancel();
return dialog;
}
JSONArray tagArray = tags.optJSONArray("stats");
List<JSONObject> tagList = new ArrayList<>();
for (int i = 0; i < tagArray.length(); i++) {
tagList.add(tagArray.optJSONObject(i));
}
tagsEditText.allowDuplicates(false);
tagsEditText.setAdapter(new TagAutoCompleteAdapter(getActivity(), 0, tagList));
// Build the dialog
builder.setView(view)
.setPositiveButton(R.string.search, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
// Build the simple criterias
SearchQueryBuilder queryBuilder = new SearchQueryBuilder()
.simpleSearch(searchEditText.getText().toString())
.shared(sharedCheckbox.isChecked())
.language(((LanguageAdapter.Language) languageSpinner.getSelectedItem()).getId())
.before(beforeDatePicker.getDate())
.after(afterDatePicker.getDate());
// Fulltext criteria
String fulltextCriteria = fulltextEditText.getText().toString();
if (!fulltextCriteria.trim().isEmpty()) {
String[] criterias = fulltextCriteria.split(" ");
for (String criteria : criterias) {
queryBuilder.fulltextSearch(criteria);
}
}
// Tags criteria
for (Object object : tagsEditText.getObjects()) {
JSONObject tag = (JSONObject) object;
queryBuilder.tag(tag.optString("name"));
}
// Send the advanced search event
EventBus.getDefault().post(new AdvancedSearchEvent(queryBuilder.build()));
getDialog().cancel();
}
})
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
getDialog().cancel();
}
});
Dialog dialog = builder.create();
dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
return dialog;
}
}

View File

@@ -0,0 +1,88 @@
package com.sismics.docs.fragment;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;
import android.provider.SearchRecentSuggestions;
import android.widget.Toast;
import com.androidquery.util.AQUtility;
import com.sismics.docs.R;
import com.sismics.docs.provider.RecentSuggestionsProvider;
import com.sismics.docs.util.ApplicationUtil;
import com.sismics.docs.util.PreferenceUtil;
/**
* Settings fragment.
*
* @author bgamard.
*/
public class SettingsFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.preferences);
// Initialize summaries
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
onSharedPreferenceChanged(sharedPreferences, PreferenceUtil.PREF_CACHE_SIZE);
// Handle clearing the recent search history
Preference clearHistoryPref = findPreference("pref_clearHistory");
clearHistoryPref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
SearchRecentSuggestions suggestions = new SearchRecentSuggestions(getActivity(),
RecentSuggestionsProvider.AUTHORITY, RecentSuggestionsProvider.MODE);
suggestions.clearHistory();
Toast.makeText(getActivity(), R.string.pref_clear_history_success, Toast.LENGTH_LONG).show();
return true;
}
});
// Handle clearing the cache
Preference clearCachePref = findPreference("pref_clearCache");
clearCachePref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
AQUtility.cleanCacheAsync(getActivity());
Toast.makeText(getActivity(), R.string.pref_clear_cache_success, Toast.LENGTH_LONG).show();
return true;
}
});
// Initialize static text preferences
Preference versionPref = findPreference("pref_version");
versionPref.setSummary(getString(R.string.version) + " " + ApplicationUtil.getVersionName(getActivity())
+ " | " + getString(R.string.build) + " " + ApplicationUtil.getVersionCode(getActivity()));
}
@Override
public void onResume() {
super.onResume();
getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
}
@Override
public void onPause() {
getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
super.onPause();
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
Preference pref = findPreference(key);
if (pref instanceof ListPreference) {
ListPreference listPref = (ListPreference) pref;
pref.setSummary(listPref.getEntry());
}
}
}

View File

@@ -0,0 +1,10 @@
package com.sismics.docs.listener;
/**
* Simple listener.
*
* @author bgamard
*/
public interface CallbackListener {
public void onComplete();
}

View File

@@ -0,0 +1,241 @@
/*
Android Asynchronous Http Client
Copyright (c) 2011 James Smith <james@loopj.com>
http://loopj.com
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package com.sismics.docs.listener;
import android.util.Log;
import com.loopj.android.http.TextHttpResponseHandler;
import org.apache.http.Header;
import org.apache.http.HttpStatus;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
/**
* Used to intercept and handle the responses from requests made using {@link com.loopj.android.http.AsyncHttpClient}, with
* automatic parsing into a {@link JSONObject} or {@link JSONArray}. <p>&nbsp;</p> This class is
* designed to be passed to get, post, put and delete requests with the {@link #onSuccess(int,
* org.apache.http.Header[], org.json.JSONArray)} or {@link #onSuccess(int,
* org.apache.http.Header[], org.json.JSONObject)} methods anonymously overridden. <p>&nbsp;</p>
* Additionally, you can override the other event methods from the parent class.
*/
public class JsonHttpResponseHandler extends TextHttpResponseHandler {
private static final String LOG_TAG = "JsonHttpResponseHandler";
/**
* Creates new JsonHttpResponseHandler, with JSON String encoding UTF-8
*/
public JsonHttpResponseHandler() {
super(DEFAULT_CHARSET);
}
/**
* Creates new JsonHttpRespnseHandler with given JSON String encoding
*
* @param encoding String encoding to be used when parsing JSON
*/
public JsonHttpResponseHandler(String encoding) {
super(encoding);
}
/**
* Returns when request succeeds
*
* @param statusCode http response status line
* @param headers response headers if any
* @param response parsed response if any
*/
public void onSuccess(int statusCode, Header[] headers, JSONObject response) {
Log.w(LOG_TAG, "onSuccess(int, Header[], JSONObject) was not overriden, but callback was received");
}
/**
* Returns when request succeeds
*
* @param statusCode http response status line
* @param headers response headers if any
* @param response parsed response if any
*/
public void onSuccess(int statusCode, Header[] headers, JSONArray response) {
Log.w(LOG_TAG, "onSuccess(int, Header[], JSONArray) was not overriden, but callback was received");
}
/**
* Returns when request failed
*
* @param statusCode http response status line
* @param headers response headers if any
* @param throwable throwable describing the way request failed
* @param errorResponse parsed response if any
*/
public void onFailure(int statusCode, Header[] headers, Throwable throwable, JSONObject errorResponse) {
Log.w(LOG_TAG, "onFailure(int, Header[], Throwable, JSONObject) was not overriden, but callback was received", throwable);
}
/**
* Returns when request failed
*
* @param statusCode http response status line
* @param headers response headers if any
* @param throwable throwable describing the way request failed
* @param errorResponse parsed response if any
*/
public void onFailure(int statusCode, Header[] headers, Throwable throwable, JSONArray errorResponse) {
Log.w(LOG_TAG, "onFailure(int, Header[], Throwable, JSONArray) was not overriden, but callback was received", throwable);
}
@Override
public void onFailure(int statusCode, Header[] headers, String responseString, Throwable throwable) {
Log.w(LOG_TAG, "onFailure(int, Header[], String, Throwable) was not overriden, but callback was received", throwable);
}
@Override
public void onSuccess(int statusCode, Header[] headers, String responseString) {
Log.w(LOG_TAG, "onSuccess(int, Header[], String) was not overriden, but callback was received");
}
@Override
public final void onSuccess(final int statusCode, final Header[] headers, final byte[] responseBytes) {
if (statusCode != HttpStatus.SC_NO_CONTENT) {
Runnable parser = new Runnable() {
@Override
public void run() {
try {
final Object jsonResponse = parseResponse(responseBytes);
postRunnable(new Runnable() {
@Override
public void run() {
if (jsonResponse instanceof JSONObject) {
onSuccess(statusCode, headers, (JSONObject) jsonResponse);
} else if (jsonResponse instanceof JSONArray) {
onSuccess(statusCode, headers, (JSONArray) jsonResponse);
} else if (jsonResponse instanceof String) {
onFailure(statusCode, headers, (String) jsonResponse, new JSONException("Response cannot be parsed as JSON data"));
} else {
onFailure(statusCode, headers, new JSONException("Unexpected response type " + jsonResponse.getClass().getName()), (JSONObject) null);
}
}
});
} catch (final JSONException ex) {
postRunnable(new Runnable() {
@Override
public void run() {
onFailure(statusCode, headers, ex, (JSONObject) null);
}
});
}
}
};
if (!getUseSynchronousMode()) {
new Thread(parser).start();
} else {
// In synchronous mode everything should be run on one thread
parser.run();
}
} else {
onSuccess(statusCode, headers, new JSONObject());
}
}
@Override
public final void onFailure(final int statusCode, final Header[] headers, final byte[] responseBytes, final Throwable throwable) {
if (responseBytes != null) {
Runnable parser = new Runnable() {
@Override
public void run() {
try {
final Object jsonResponse = parseResponse(responseBytes);
postRunnable(new Runnable() {
@Override
public void run() {
if (jsonResponse instanceof JSONObject) {
onFailure(statusCode, headers, throwable, (JSONObject) jsonResponse);
} else if (jsonResponse instanceof JSONArray) {
onFailure(statusCode, headers, throwable, (JSONArray) jsonResponse);
} else if (jsonResponse instanceof String) {
onFailure(statusCode, headers, (String) jsonResponse, throwable);
} else {
onFailure(statusCode, headers, new JSONException("Unexpected response type " + jsonResponse.getClass().getName()), (JSONObject) null);
}
}
});
} catch (final JSONException ex) {
postRunnable(new Runnable() {
@Override
public void run() {
onFailure(statusCode, headers, ex, (JSONObject) null);
}
});
}
}
};
if (!getUseSynchronousMode()) {
new Thread(parser).start();
} else {
// In synchronous mode everything should be run on one thread
parser.run();
}
} else {
Log.v(LOG_TAG, "response body is null, calling onFailure(Throwable, JSONObject)");
onFailure(statusCode, headers, throwable, (JSONObject) null);
}
// In all cases, call the default failure listener
onAllFailure(statusCode, headers, responseBytes, throwable);
}
public void onAllFailure(int statusCode, Header[] headers, byte[] responseBytes, Throwable throwable) {
// All failures go there
}
/**
* Returns Object of type {@link JSONObject}, {@link JSONArray}, String, Boolean, Integer, Long,
* Double or {@link JSONObject#NULL}, see {@link org.json.JSONTokener#nextValue()}
*
* @param responseBody response bytes to be assembled in String and parsed as JSON
* @return Object parsedResponse
* @throws org.json.JSONException exception if thrown while parsing JSON
*/
protected Object parseResponse(byte[] responseBody) throws JSONException {
if (null == responseBody)
return null;
Object result = null;
//trim the string to prevent start with blank, and test if the string is valid JSON, because the parser don't do this :(. If JSON is not valid this will return null
String jsonString = getResponseString(responseBody, getCharset());
if (jsonString != null) {
jsonString = jsonString.trim();
if (jsonString.startsWith(UTF8_BOM)) {
jsonString = jsonString.substring(1);
}
if (jsonString.startsWith("{") || jsonString.startsWith("[")) {
result = new JSONTokener(jsonString).nextValue();
}
}
if (result == null) {
result = jsonString;
}
return result;
}
}

View File

@@ -0,0 +1,37 @@
package com.sismics.docs.listener;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
public class RecyclerItemClickListener implements RecyclerView.OnItemTouchListener {
private OnItemClickListener mListener;
public interface OnItemClickListener {
public void onItemClick(View view, int position);
}
GestureDetector mGestureDetector;
public RecyclerItemClickListener(Context context, OnItemClickListener listener) {
mListener = listener;
mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override public boolean onSingleTapUp(MotionEvent e) {
return true;
}
});
}
@Override public boolean onInterceptTouchEvent(RecyclerView view, MotionEvent e) {
View childView = view.findChildViewUnder(e.getX(), e.getY());
if (childView != null && mListener != null && mGestureDetector.onTouchEvent(e)) {
mListener.onItemClick(childView, view.getChildPosition(childView));
}
return false;
}
@Override public void onTouchEvent(RecyclerView view, MotionEvent motionEvent) { }
}

View File

@@ -0,0 +1,100 @@
package com.sismics.docs.model.application;
import android.app.Activity;
import android.content.Context;
import com.sismics.docs.listener.CallbackListener;
import com.sismics.docs.listener.JsonHttpResponseHandler;
import com.sismics.docs.resource.UserResource;
import com.sismics.docs.util.PreferenceUtil;
import org.apache.http.Header;
import org.json.JSONObject;
/**
* Global context of the application.
*
* @author bgamard
*/
public class ApplicationContext {
/**
* Singleton's instance.
*/
private static ApplicationContext applicationContext;
/**
* Response of /user/info
*/
private JSONObject userInfo;
/**
* Private constructor.
*/
private ApplicationContext() {
}
/**
* Returns a singleton of ApplicationContext.
*
* @return Singleton of ApplicationContext
*/
public static ApplicationContext getInstance() {
if (applicationContext == null) {
applicationContext = new ApplicationContext();
}
return applicationContext;
}
/**
* Returns true if current user is logged in.
*
* @return True if the current user is logged in
*/
public boolean isLoggedIn() {
return userInfo != null && !userInfo.optBoolean("anonymous");
}
/**
* Getter of userInfo
*
* @return userInfo
*/
public JSONObject getUserInfo() {
return userInfo;
}
/**
* Setter of userInfo
*
* @param json userInfo
*/
public void setUserInfo(Context context, JSONObject json) {
this.userInfo = json;
PreferenceUtil.setCachedJson(context, PreferenceUtil.PREF_CACHED_USER_INFO_JSON, json);
}
/**
* Asynchronously get user info.
*
* @param activity Activity
* @param callbackListener CallbackListener
*/
public void fetchUserInfo(final Activity activity, final CallbackListener callbackListener) {
UserResource.info(activity.getApplicationContext(), new JsonHttpResponseHandler() {
@Override
public void onSuccess(int statusCode, Header[] headers, final JSONObject json) {
// Save data in application context
if (!json.optBoolean("anonymous", true)) {
setUserInfo(activity.getApplicationContext(), json);
}
}
@Override
public void onFinish() {
if (callbackListener != null) {
callbackListener.onComplete();
}
}
});
}
}

View File

@@ -0,0 +1,17 @@
package com.sismics.docs.provider;
import android.content.SearchRecentSuggestionsProvider;
/**
* Search recent suggestions provider.
*
* @author bgamard.
*/
public class RecentSuggestionsProvider extends SearchRecentSuggestionsProvider {
public final static String AUTHORITY = "com.sismics.docs.provider.RecentSuggestionsProvider";
public final static int MODE = DATABASE_MODE_QUERIES;
public RecentSuggestionsProvider() {
setupSuggestions(AUTHORITY, MODE);
}
}

View File

@@ -0,0 +1,138 @@
package com.sismics.docs.resource;
import android.content.Context;
import android.os.Build;
import com.androidquery.callback.AbstractAjaxCallback;
import com.loopj.android.http.AsyncHttpClient;
import com.loopj.android.http.PersistentCookieStore;
import com.sismics.docs.util.ApplicationUtil;
import com.sismics.docs.util.PreferenceUtil;
import org.apache.http.conn.ssl.SSLSocketFactory;
import java.io.IOException;
import java.net.Socket;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Locale;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
/**
* Base class for API access.
*
* @author bgamard
*/
public class BaseResource {
/**
* User-Agent to use.
*/
protected static String USER_AGENT = null;
/**
* Accept-Language header.
*/
protected static String ACCEPT_LANGUAGE = null;
/**
* HTTP client.
*/
protected static AsyncHttpClient client = new AsyncHttpClient();
static {
// 20sec default timeout
client.setTimeout(60000);
try {
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null, null);
MySSLSocketFactory sf = new MySSLSocketFactory(trustStore);
sf.setHostnameVerifier(MySSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
client.setSSLSocketFactory(sf);
AbstractAjaxCallback.setSSF(sf);
} catch (Exception e) {
// NOP
}
}
/**
* Resource initialization.
*
* @param context Context
*/
protected static void init(Context context) {
client.setCookieStore(new PersistentCookieStore(context));
if (USER_AGENT == null) {
USER_AGENT = "Sismics Docs Android " + ApplicationUtil.getVersionName(context) + "/Android " + Build.VERSION.RELEASE + "/" + Build.MODEL;
client.setUserAgent(USER_AGENT);
}
if (ACCEPT_LANGUAGE == null) {
Locale locale = Locale.getDefault();
ACCEPT_LANGUAGE = locale.getLanguage() + "_" + locale.getCountry();
client.addHeader("Accept-Language", ACCEPT_LANGUAGE);
}
}
/**
* Socket factory to allow self-signed certificates.
*
* @author bgamard
*/
public static class MySSLSocketFactory extends SSLSocketFactory {
SSLContext sslContext = SSLContext.getInstance("TLS");
public MySSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
super(truststore);
TrustManager tm = new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
public X509Certificate[] getAcceptedIssuers() {
return null;
}
};
sslContext.init(null, new TrustManager[] { tm }, null);
}
@Override
public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException {
return sslContext.getSocketFactory().createSocket(socket, host, port, autoClose);
}
@Override
public Socket createSocket() throws IOException {
return sslContext.getSocketFactory().createSocket();
}
}
/**
* Returns cleaned API URL.
*
* @param context Context
* @return Cleaned API URL
*/
protected static String getApiUrl(Context context) {
String serverUrl = PreferenceUtil.getServerUrl(context);
if (serverUrl == null) {
return null;
}
return serverUrl + "/api";
}
}

View File

@@ -0,0 +1,119 @@
package com.sismics.docs.resource;
import android.content.Context;
import com.loopj.android.http.RequestParams;
import com.sismics.docs.listener.JsonHttpResponseHandler;
import java.util.Set;
/**
* Access to /document API.
*
* @author bgamard
*/
public class DocumentResource extends BaseResource {
/**
* GET /document/list.
*
* @param context Context
* @param offset Offset
* @param query Search query
* @param responseHandler Callback
*/
public static void list(Context context, int offset, String query, JsonHttpResponseHandler responseHandler) {
init(context);
RequestParams params = new RequestParams();
params.put("limit", 20);
params.put("offset", offset);
params.put("sort_column", 3);
params.put("asc", false);
params.put("search", query);
client.get(getApiUrl(context) + "/document/list", params, responseHandler);
}
/**
* GET /document/id.
*
* @param context Context
* @param id ID
* @param responseHandler Callback
*/
public static void get(Context context, String id, JsonHttpResponseHandler responseHandler) {
init(context);
client.get(getApiUrl(context) + "/document/" + id, responseHandler);
}
/**
* DELETE /document/id.
*
* @param context Context
* @param id ID
* @param responseHandler Callback
*/
public static void delete(Context context, String id, JsonHttpResponseHandler responseHandler) {
init(context);
client.delete(getApiUrl(context) + "/document/" + id, responseHandler);
}
/**
* PUT /document.
*
* @param context Context
* @param title Title
* @param description Description
* @param tagIdList Tags ID list
* @param language Language
* @param createDate Create date
* @param responseHandler Callback
*/
public static void add(Context context, String title, String description,
Set<String> tagIdList, String language, long createDate, JsonHttpResponseHandler responseHandler) {
init(context);
RequestParams params = new RequestParams();
params.put("title", title);
params.put("description", description);
params.put("tags", tagIdList);
params.put("language", language);
params.put("create_date", createDate);
client.put(getApiUrl(context) + "/document", params, responseHandler);
}
/**
* POST /document/id.
*
* @param context Context
* @param id ID
* @param title Title
* @param description Description
* @param tagIdList Tags ID list
* @param language Language
* @param createDate Create date
* @param responseHandler Callback
*/
public static void edit(Context context, String id, String title, String description,
Set<String> tagIdList, String language, long createDate, JsonHttpResponseHandler responseHandler) {
init(context);
RequestParams params = new RequestParams();
params.put("title", title);
params.put("description", description);
params.put("tags", tagIdList);
params.put("language", language);
params.put("create_date", createDate);
client.post(getApiUrl(context) + "/document/" + id, params, responseHandler);
}
/**
* Cancel pending requests.
*
* @param context Context
*/
public static void cancel(Context context) {
client.cancelRequests(context, true);
}
}

View File

@@ -0,0 +1,82 @@
package com.sismics.docs.resource;
import android.content.Context;
import com.loopj.android.http.PersistentCookieStore;
import com.loopj.android.http.RequestParams;
import com.loopj.android.http.SyncHttpClient;
import com.sismics.docs.listener.JsonHttpResponseHandler;
import java.io.InputStream;
import java.security.KeyStore;
/**
* Access to /file API.
*
* @author bgamard
*/
public class FileResource extends BaseResource {
/**
* GET /file/list.
*
* @param context Context
* @param documentId Document ID
* @param responseHandler Callback
*/
public static void list(Context context, String documentId, JsonHttpResponseHandler responseHandler) {
init(context);
client.get(getApiUrl(context) + "/file/list?id=" + documentId, responseHandler);
}
/**
* DELETE /file/id.
*
* @param context Context
* @param id ID
* @param responseHandler Callback
*/
public static void delete(Context context, String id, JsonHttpResponseHandler responseHandler) {
init(context);
client.delete(getApiUrl(context) + "/file/" + id, responseHandler);
}
/**
* PUT /file.
*
* @param context Context
* @param documentId Document ID
* @param is Input stream
* @param responseHandler Callback
* @throws Exception
*/
public static void addSync(Context context, String documentId, InputStream is, JsonHttpResponseHandler responseHandler) throws Exception {
init(context);
SyncHttpClient client = new SyncHttpClient();
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null, null);
MySSLSocketFactory sf = new MySSLSocketFactory(trustStore);
sf.setHostnameVerifier(MySSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
client.setSSLSocketFactory(sf);
client.setCookieStore(new PersistentCookieStore(context));
client.setUserAgent(USER_AGENT);
client.addHeader("Accept-Language", ACCEPT_LANGUAGE);
RequestParams params = new RequestParams();
params.put("id", documentId);
params.put("file", is, "file", "application/octet-stream", true);
client.put(getApiUrl(context) + "/file", params, responseHandler);
}
/**
* Cancel pending requests.
*
* @param context Context
*/
public static void cancel(Context context) {
client.cancelRequests(context, true);
}
}

View File

@@ -0,0 +1,44 @@
package com.sismics.docs.resource;
import android.content.Context;
import com.loopj.android.http.RequestParams;
import com.sismics.docs.listener.JsonHttpResponseHandler;
/**
* Access to /tag API.
*
* @author bgamard
*/
public class ShareResource extends BaseResource {
/**
* PUT /share.
*
* @param context Context
* @param documentId Document ID
* @param name Name
* @param responseHandler Callback
*/
public static void add(Context context, String documentId, String name, JsonHttpResponseHandler responseHandler) {
init(context);
RequestParams params = new RequestParams();
params.put("id", documentId);
params.put("name", name);
client.put(getApiUrl(context) + "/share", params, responseHandler);
}
/**
* DELETE /share.
*
* @param context Context
* @param id ID
* @param responseHandler Callback
*/
public static void delete(Context context, String id, JsonHttpResponseHandler responseHandler) {
init(context);
client.delete(getApiUrl(context) + "/share/" + id, responseHandler);
}
}

View File

@@ -0,0 +1,25 @@
package com.sismics.docs.resource;
import android.content.Context;
import com.sismics.docs.listener.JsonHttpResponseHandler;
/**
* Access to /tag API.
*
* @author bgamard
*/
public class TagResource extends BaseResource {
/**
* GET /tag/stats.
*
* @param context Context
* @param responseHandler Callback
*/
public static void stats(Context context, JsonHttpResponseHandler responseHandler) {
init(context);
client.get(getApiUrl(context) + "/tag/stats", responseHandler);
}
}

View File

@@ -0,0 +1,58 @@
package com.sismics.docs.resource;
import android.content.Context;
import com.loopj.android.http.RequestParams;
import com.sismics.docs.listener.JsonHttpResponseHandler;
/**
* Access to /user API.
*
* @author bgamard
*/
public class UserResource extends BaseResource {
/**
* POST /user/login.
*
* @param context Context
* @param username Username
* @param password Password
* @param responseHandler Callback
*/
public static void login(Context context, String username, String password, JsonHttpResponseHandler responseHandler) {
init(context);
RequestParams params = new RequestParams();
params.put("username", username);
params.put("password", password);
params.put("remember", "true");
client.post(getApiUrl(context) + "/user/login", params, responseHandler);
}
/**
* GET /user.
*
* @param context Context
* @param responseHandler Callback
*/
public static void info(Context context, JsonHttpResponseHandler responseHandler) {
init(context);
RequestParams params = new RequestParams();
client.get(getApiUrl(context) + "/user", params, responseHandler);
}
/**
* POST /user/logout.
*
* @param context Context
* @param responseHandler Callback
*/
public static void logout(Context context, JsonHttpResponseHandler responseHandler) {
init(context);
RequestParams params = new RequestParams();
client.post(getApiUrl(context) + "/user/logout", params, responseHandler);
}
}

View File

@@ -0,0 +1,133 @@
package com.sismics.docs.service;
import android.app.IntentService;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Intent;
import android.net.Uri;
import android.os.PowerManager;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationCompat.Builder;
import android.util.Log;
import com.sismics.docs.R;
import com.sismics.docs.event.FileAddEvent;
import com.sismics.docs.listener.JsonHttpResponseHandler;
import com.sismics.docs.resource.FileResource;
import org.apache.http.Header;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import de.greenrobot.event.EventBus;
/**
* Service to upload a file to a document in the background.
*
* @author bgamard
*/
public class FileUploadService extends IntentService {
private static final String TAG = "FileUploadService";
private static final int UPLOAD_NOTIFICATION_ID = 1;
private static final int UPLOAD_NOTIFICATION_ID_DONE = 2;
public static final String PARAM_URI = "uri";
public static final String PARAM_DOCUMENT_ID = "documentId";
private NotificationManager notificationManager;
private Builder notification;
private PowerManager.WakeLock wakeLock;
public FileUploadService() {
super(FileUploadService.class.getName());
}
@Override
public void onCreate() {
super.onCreate();
notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
notification = new NotificationCompat.Builder(this);
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
}
@Override
protected void onHandleIntent(Intent intent) {
if (intent == null) {
return;
}
wakeLock.acquire();
try {
onStart();
handleFileUpload(intent.getStringExtra(PARAM_DOCUMENT_ID), (Uri) intent.getParcelableExtra(PARAM_URI));
} catch (Exception e) {
Log.e(TAG, "Error uploading the file", e);
onError();
} finally {
wakeLock.release();
}
}
/**
* Actually uploading the file.
*
* @param documentId Document ID
* @param uri Data URI
* @throws IOException
*/
private void handleFileUpload(final String documentId, final Uri uri) throws Exception {
final InputStream is = getContentResolver().openInputStream(uri);
FileResource.addSync(this, documentId, is, new JsonHttpResponseHandler() {
@Override
public void onSuccess(int statusCode, Header[] headers, JSONObject response) {
EventBus.getDefault().post(new FileAddEvent(documentId, response.optString("id")));
FileUploadService.this.onComplete();
}
@Override
public void onAllFailure(int statusCode, Header[] headers, byte[] responseBytes, Throwable throwable) {
FileUploadService.this.onError();
}
});
}
/**
* On upload start.
*/
private void onStart() {
notification.setContentTitle(getString(R.string.upload_notification_title))
.setContentText(getString(R.string.upload_notification_message))
.setContentIntent(PendingIntent.getBroadcast(this, 0, new Intent(), PendingIntent.FLAG_UPDATE_CURRENT))
.setSmallIcon(R.drawable.ic_file_upload_white_24dp)
.setProgress(100, 0, true)
.setOngoing(true);
startForeground(UPLOAD_NOTIFICATION_ID, notification.build());
}
/**
* On upload complete.
*/
private void onComplete() {
stopForeground(true);
}
/**
* On upload error.
*/
private void onError() {
stopForeground(false);
notification.setContentTitle(getString(R.string.upload_notification_title))
.setContentText(getString(R.string.upload_notification_error))
.setSmallIcon(R.drawable.ic_file_upload_white_24dp)
.setProgress(0, 0, false)
.setOngoing(false);
notificationManager.notify(UPLOAD_NOTIFICATION_ID_DONE, notification.build());
}
}

View File

@@ -0,0 +1,63 @@
package com.sismics.docs.ui.form;
import android.view.View;
import com.sismics.docs.ui.form.validator.ValidatorType;
public class Validable {
private final ValidatorType[] validatorTypes;
private View view;
private boolean isValidated = false;
public Validable(ValidatorType... validatorTypes) {
this.validatorTypes = validatorTypes;
}
/**
* Getter of view.
*
* @return view
*/
public View getView() {
return view;
}
/**
* Setter of view.
*
* @param view view
*/
public void setView(View view) {
this.view = view;
}
/**
* Getter of isValidated.
*
* @return isValidated
*/
public boolean isValidated() {
return isValidated;
}
/**
* Setter of isValidated.
*
* @param isValidated isValidated
*/
public void setValidated(boolean isValidated) {
this.isValidated = isValidated;
}
/**
* Getter of validatorTypes.
*
* @return validatorTypes
*/
public ValidatorType[] getValidatorTypes() {
return validatorTypes;
}
}

View File

@@ -0,0 +1,148 @@
package com.sismics.docs.ui.form;
import android.content.Context;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.widget.EditText;
import com.sismics.docs.listener.CallbackListener;
import com.sismics.docs.ui.form.validator.ValidatorType;
import java.util.HashMap;
import java.util.Map;
/**
* Utility for form validation.
*
* @author bgamard
*/
public class Validator {
/**
* List of validable elements.
*/
private Map<EditText, Validable> validables = new HashMap<EditText, Validable>();
/**
* Callback when the validation of one element has changed.
*/
private CallbackListener onValidationChanged;
/**
* True if the validator show validation errors.
*/
private boolean showErrors;
/**
* Context.
*/
private Context context;
/**
* Constructor.
*
* @param showErrors True to display validation errors
*/
public Validator(Context context, boolean showErrors) {
this.context = context;
this.showErrors = showErrors;
}
/**
* Setter of onValidationChanged.
* @param onValidationChanged onValidationChanged
*/
public void setOnValidationChanged(CallbackListener onValidationChanged) {
this.onValidationChanged = onValidationChanged;
onValidationChanged.onComplete();
}
/**
* Add a validable element.
*
* @param editText Edit text
* @param validatorTypes Validators
*/
public void addValidable(final EditText editText, final ValidatorType... validatorTypes) {
final Validable validable = new Validable(validatorTypes);
validables.put(editText, validable);
editText.addTextChangedListener(new TextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
// NOP
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
// NOP
}
@Override
public void afterTextChanged(Editable s) {
validate(editText, validable);
}
});
}
/**
* Returns true if the element is validated.
*
* @param view View
* @return True if the element is validated
*/
public boolean isValidated(View view) {
return validables.get(view).isValidated();
}
/**
* Validate a specific EditText.
*
* @param editText EditText
* @param validable Validable
*/
private void validate(EditText editText, Validable validable) {
validable.setValidated(true);
for (ValidatorType validatorType : validable.getValidatorTypes()) {
if (!validatorType.validate(editText.getEditableText().toString())) {
if (showErrors) {
editText.setError(validatorType.getErrorMessage(context));
}
validable.setValidated(false);
break;
}
}
if (validable.isValidated()) {
editText.setError(null);
}
if (onValidationChanged != null) {
onValidationChanged.onComplete();
}
}
/**
* Validate everything now.
*/
public void validate() {
for (Map.Entry<EditText, Validable> entry : validables.entrySet()) {
validate(entry.getKey(), entry.getValue());
}
}
/**
* Returns true if all elements are validated.
*
* @return True if all elements are validated
*/
public boolean isValidated() {
for (Validable validable : validables.values()) {
if (!validable.isValidated()) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,28 @@
package com.sismics.docs.ui.form.validator;
import android.content.Context;
import com.sismics.docs.R;
import java.util.regex.Pattern;
/**
* Alphanumeric validator.
*
* @author bgamard
*/
public class Alphanumeric implements ValidatorType {
private static Pattern ALPHANUMERIC_PATTERN = Pattern.compile("[a-zA-Z0-9_]+");
@Override
public boolean validate(String text) {
return ALPHANUMERIC_PATTERN.matcher(text).matches();
}
@Override
public String getErrorMessage(Context context) {
return context.getString(R.string.validate_error_alphanumeric);
}
}

View File

@@ -0,0 +1,30 @@
package com.sismics.docs.ui.form.validator;
import android.content.Context;
import com.sismics.docs.R;
import java.util.regex.Pattern;
/**
* Email validator.
*
* @author bgamard
*/
public class Email implements ValidatorType {
/**
* Pattern de validation.
*/
private static Pattern EMAIL_PATTERN = Pattern.compile(".+@.+\\..+");
@Override
public boolean validate(String text) {
return EMAIL_PATTERN.matcher(text).matches();
}
@Override
public String getErrorMessage(Context context) {
return context.getResources().getString(R.string.validate_error_email);
}
}

View File

@@ -0,0 +1,52 @@
package com.sismics.docs.ui.form.validator;
import android.content.Context;
import com.sismics.docs.R;
/**
* Text length validator.
*
* @author bgamard
*/
public class Length implements ValidatorType {
/**
* Minimum length.
*/
private int minLength = 0;
/**
* Maximum length.
*/
private int maxLength = 0;
/**
* True if the last validation error was about a string too short.
*/
private boolean tooShort;
/**
* Constructor.
* @param minLength Minimum length
* @param maxLength Maximum length
*/
public Length(int minLength, int maxLength) {
this.minLength = minLength;
this.maxLength = maxLength;
}
@Override
public boolean validate(String text) {
tooShort = text.trim().length() < minLength;
return text.trim().length() >= minLength && text.trim().length() <= maxLength;
}
@Override
public String getErrorMessage(Context context) {
if (tooShort) {
return context.getResources().getString(R.string.validate_error_length_min, minLength);
}
return context.getResources().getString(R.string.validate_error_length_max, maxLength);
}
}

View File

@@ -0,0 +1,24 @@
package com.sismics.docs.ui.form.validator;
import android.content.Context;
import com.sismics.docs.R;
/**
* Text presence validator.
*
* @author bgamard
*/
public class Required implements ValidatorType {
@Override
public boolean validate(String text) {
return text.trim().length() != 0;
}
@Override
public String getErrorMessage(Context context) {
return context.getString(R.string.validate_error_required);
}
}

View File

@@ -0,0 +1,25 @@
package com.sismics.docs.ui.form.validator;
import android.content.Context;
/**
* Interface for validation types.
*
* @author bgamard
*/
public interface ValidatorType {
/**
* Returns true if the validator is validated.
* @param text
* @return
*/
public boolean validate(String text);
/**
* Returns an error message.
* @param context
* @return
*/
public String getErrorMessage(Context context);
}

View File

@@ -0,0 +1,70 @@
package com.sismics.docs.ui.view;
import android.app.DatePickerDialog;
import android.content.Context;
import android.text.format.DateFormat;
import android.util.AttributeSet;
import android.view.View;
import android.widget.DatePicker;
import android.widget.TextView;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
/**
* Date picker widget.
*
* @author bgamard
*/
public class DatePickerView extends TextView implements DatePickerDialog.OnDateSetListener {
private Date date;
public DatePickerView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public DatePickerView(Context context, AttributeSet attrs) {
super(context, attrs);
setAttributes();
}
public DatePickerView(Context context) {
super(context);
setAttributes();
}
private void setAttributes() {
setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
final Calendar calendar = Calendar.getInstance();
if (date != null) {
calendar.setTime(date);
}
new DatePickerDialog(
DatePickerView.this.getContext(), DatePickerView.this,
calendar.get(Calendar.YEAR),
calendar.get(Calendar.MONTH),
calendar.get(Calendar.DAY_OF_MONTH)).show();
}
});
}
@Override
public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
Date date = new GregorianCalendar(year, monthOfYear, dayOfMonth).getTime();
setDate(date);
}
public void setDate(Date date) {
this.date = date;
String formattedDate = DateFormat.getDateFormat(getContext()).format(date);
setText(formattedDate);
}
public Date getDate() {
return date;
}
}

View File

@@ -0,0 +1,81 @@
package com.sismics.docs.ui.view;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.View;
/**
* Divider item decoration for recycler view.
*
* @author bgamard
*/
public class DividerItemDecoration extends RecyclerView.ItemDecoration {
private Drawable mDivider;
public DividerItemDecoration(Context context, AttributeSet attrs) {
final TypedArray a = context.obtainStyledAttributes(attrs, new int [] { android.R.attr.listDivider });
mDivider = a.getDrawable(0);
a.recycle();
}
public DividerItemDecoration(Drawable divider) { mDivider = divider; }
@Override
public void getItemOffsets (Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
if (mDivider == null) return;
if (parent.getChildPosition(view) < 1) return;
if (getOrientation(parent) == LinearLayoutManager.VERTICAL) outRect.top = mDivider.getIntrinsicHeight();
else outRect.left = mDivider.getIntrinsicWidth();
}
@Override
public void onDrawOver(Canvas c, RecyclerView parent) {
if (mDivider == null) { super.onDrawOver(c, parent); return; }
if (getOrientation(parent) == LinearLayoutManager.VERTICAL) {
final int left = parent.getPaddingLeft();
final int right = parent.getWidth() - parent.getPaddingRight();
final int childCount = parent.getChildCount();
for (int i=1; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
final int size = mDivider.getIntrinsicHeight();
final int top = child.getTop() - params.topMargin;
final int bottom = top + size;
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
} else { //horizontal
final int top = parent.getPaddingTop();
final int bottom = parent.getHeight() - parent.getPaddingBottom();
final int childCount = parent.getChildCount();
for (int i=1; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
final int size = mDivider.getIntrinsicWidth();
final int left = child.getLeft() - params.leftMargin;
final int right = left + size;
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
}
private int getOrientation(RecyclerView parent) {
if (parent.getLayoutManager() instanceof LinearLayoutManager) {
LinearLayoutManager layoutManager = (LinearLayoutManager) parent.getLayoutManager();
return layoutManager.getOrientation();
} else throw new IllegalStateException("DividerItemDecoration can only be used with a LinearLayoutManager.");
}
}

View File

@@ -0,0 +1,57 @@
package com.sismics.docs.ui.view;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.View;
/**
* RecyclerView with empty view support.
* Thanks to https://gist.github.com/adelnizamutdinov/31c8f054d1af4588dc5c
*
* @author Nizamutdinov Adel
*/
public class EmptyRecyclerView extends RecyclerView {
private View emptyView;
public EmptyRecyclerView(Context context) { super(context); }
public EmptyRecyclerView(Context context, AttributeSet attrs) { super(context, attrs); }
public EmptyRecyclerView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
void checkIfEmpty() {
if (emptyView != null) {
emptyView.setVisibility(getAdapter().getItemCount() > 0 ? GONE : VISIBLE);
}
}
final AdapterDataObserver observer = new AdapterDataObserver() {
@Override public void onChanged() {
super.onChanged();
checkIfEmpty();
}
};
@Override public void setAdapter(Adapter adapter) {
final Adapter oldAdapter = getAdapter();
if (oldAdapter != null) {
oldAdapter.unregisterAdapterDataObserver(observer);
}
super.setAdapter(adapter);
if (adapter != null) {
adapter.registerAdapterDataObserver(observer);
}
}
public void setEmptyView(View emptyView) {
// Hide the current empty view
if (this.emptyView != null) {
this.emptyView.setVisibility(GONE);
}
this.emptyView = emptyView;
checkIfEmpty();
}
}

View File

@@ -0,0 +1,33 @@
package com.sismics.docs.ui.view;
import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.View;
import it.sephiroth.android.library.imagezoom.ImageViewTouch;
/**
* ViewPager for files.
*
* @author bgamard.
*/
public class FileViewPager extends ViewPager {
public FileViewPager(Context context) {
super(context);
}
public FileViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
if (v instanceof ImageViewTouch) {
return ((ImageViewTouch) v).canScroll(dx);
} else {
return super.canScroll(v, checkV, dx, x, y);
}
}
}

View File

@@ -0,0 +1,66 @@
package com.sismics.docs.ui.view;
import android.app.Activity;
import android.content.Context;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.text.InputFilter;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.sismics.docs.R;
import com.tokenautocomplete.TokenCompleteTextView;
import org.json.JSONObject;
/**
* Auto-complete text view displaying tags.
*
* @author bgamard
*/
public class TagsCompleteTextView extends TokenCompleteTextView {
public TagsCompleteTextView(Context context) {
super(context);
init();
}
public TagsCompleteTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public TagsCompleteTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
setFilters(new InputFilter[] {});
}
@Override
protected View getViewForObject(Object object) {
JSONObject tag = (JSONObject) object;
LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
View view = inflater.inflate(R.layout.tag_autocomplete_token, (ViewGroup) getParent(), false);
TextView textView = (TextView) view.findViewById(R.id.tagTextView);
textView.setText(tag.optString("name"));
Drawable drawable = textView.getCompoundDrawables()[0].mutate();
drawable.setColorFilter(Color.parseColor(tag.optString("color")), PorterDuff.Mode.MULTIPLY);
textView.setCompoundDrawables(drawable, null, null, null);
textView.invalidate();
return view;
}
@Override
protected Object defaultObject(String completionText) {
return completionText;
}
}

View File

@@ -0,0 +1,43 @@
package com.sismics.docs.util;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
/**
* Utility class on general application data.
*
* @author bgamard
*/
public class ApplicationUtil {
/**
* Returns version name.
*
* @param context Context
* @return Nom de la version
*/
public static String getVersionName(Context context) {
try {
PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
return packageInfo.versionName;
} catch (NameNotFoundException e) {
return "";
}
}
/**
* Returns version number.
*
* @param context Context
* @return Numéro de version
*/
public static int getVersionCode(Context context) {
try {
PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
return packageInfo.versionCode;
} catch (NameNotFoundException e) {
return 0;
}
}
}

View File

@@ -0,0 +1,40 @@
package com.sismics.docs.util;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import com.sismics.docs.R;
/**
* Utility class for dialogs.
*
* @author bgamard
*/
public class DialogUtil {
/**
* Create a dialog with an OK button.
*
* @param activity Context activity
* @param title Dialog title
* @param message Dialog message
*/
public static void showOkDialog(Activity activity, int title, int message) {
if (activity == null || activity.isFinishing()) {
return;
}
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(title)
.setMessage(message)
.setCancelable(true)
.setNeutralButton(R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
dialog.dismiss();
}
}).create().show();
}
}

View File

@@ -0,0 +1,166 @@
package com.sismics.docs.util;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.preference.PreferenceManager;
import com.loopj.android.http.PersistentCookieStore;
import org.apache.http.cookie.Cookie;
import org.json.JSONObject;
import java.util.List;
/**
* Utility class on preferences.
*
* @author bgamard
*/
public class PreferenceUtil {
public static final String PREF_CACHED_USER_INFO_JSON = "pref_cachedUserInfoJson";
public static final String PREF_CACHED_TAGS_JSON = "pref_cachedTagsJson";
public static final String PREF_SERVER_URL = "pref_ServerUrl";
public static final String PREF_CACHE_SIZE = "pref_cacheSize";
/**
* Returns a preference of boolean type.
* @param context Context
* @param key Shared preference key
* @return Shared preference value
*/
public static boolean getBooleanPreference(Context context, String key, boolean defaultValue) {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
return sharedPreferences.getBoolean(key, defaultValue);
}
/**
* Returns a preference of string type.
* @param context Context
* @param key Shared preference key
* @return Shared preference value
*/
public static String getStringPreference(Context context, String key) {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
return sharedPreferences.getString(key, null);
}
/**
* Returns a preference of integer type.
* @param context Context
* @param key Shared preference key
* @return Shared preference value
*/
public static int getIntegerPreference(Context context, String key, int defaultValue) {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
try {
String pref = sharedPreferences.getString(key, "");
try {
return Integer.parseInt(pref);
} catch (NumberFormatException e) {
return defaultValue;
}
} catch (ClassCastException e) {
return sharedPreferences.getInt(key, defaultValue);
}
}
/**
* Update JSON cache.
* @param context Context
* @param key Shared preference key
* @param json JSON data
*/
public static void setCachedJson(Context context, String key, JSONObject json) {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
sharedPreferences.edit().putString(key, json != null ? json.toString() : null).apply();
}
/**
* Returns a JSON cache.
* @param context Context
* @param key Shared preference key
* @return JSON data
*/
public static JSONObject getCachedJson(Context context, String key) {
try {
return new JSONObject(getStringPreference(context, key));
} catch (Exception e) {
// The cache is not parsable, clean this up
setCachedJson(context, key, null);
return null;
}
}
/**
* Update server URL.
* @param context Context
*/
public static void setServerUrl(Context context, String serverUrl) {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
sharedPreferences.edit().putString(PREF_SERVER_URL, serverUrl).apply();
}
/**
* Empty user caches.
* @param context Context
*/
public static void resetUserCache(Context context) {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
Editor editor = sharedPreferences.edit();
editor
.putString(PREF_CACHED_USER_INFO_JSON, null)
.putString(PREF_CACHED_TAGS_JSON, null)
.apply();
}
/**
* Returns auth token cookie from shared preferences.
* @return Auth token
*/
public static String getAuthToken(Context context) {
PersistentCookieStore cookieStore = new PersistentCookieStore(context);
List<Cookie> cookieList = cookieStore.getCookies();
for (Cookie cookie : cookieList) {
if (cookie.getName().equals("auth_token")) {
return cookie.getValue();
}
}
return null;
}
/**
* Returns cleaned server URL.
* @param context Context
* @return Server URL
*/
public static String getServerUrl(Context context) {
String serverUrl = getStringPreference(context, PREF_SERVER_URL);
if (serverUrl == null) {
return null;
}
// Trim
serverUrl = serverUrl.trim();
if (!serverUrl.startsWith("http")) {
// Try to add http
serverUrl = "http://" + serverUrl;
}
if (serverUrl.endsWith("/")) {
// Delete last /
serverUrl = serverUrl.substring(0, serverUrl.length() - 1);
}
// Remove /api
if (serverUrl.endsWith("/api")) {
serverUrl = serverUrl.substring(0, serverUrl.length() - 4);
}
return serverUrl;
}
}

View File

@@ -0,0 +1,151 @@
package com.sismics.docs.util;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* Search query builder.
*
* @author bgamard.
*/
public class SearchQueryBuilder {
/**
* The query.
*/
private StringBuilder query;
/**
* Search separator.
*/
private static String SEARCH_SEPARATOR = " ";
/**
* Search date format.
*/
private SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
/**
* Build a query.
*/
public SearchQueryBuilder() {
query = new StringBuilder();
}
/**
* Add a simple search criteria.
*
* @param simpleSearch Simple search criteria
* @return The builder
*/
public SearchQueryBuilder simpleSearch(String simpleSearch) {
if (isValid(simpleSearch)) {
query.append(SEARCH_SEPARATOR).append(simpleSearch);
}
return this;
}
/**
* Add a fulltext search criteria.
*
* @param fulltextSearch Fulltext search criteria
* @return The builder
*/
public SearchQueryBuilder fulltextSearch(String fulltextSearch) {
if (isValid(fulltextSearch)) {
query.append(SEARCH_SEPARATOR)
.append("full:")
.append(fulltextSearch);
}
return this;
}
/**
* Add a language criteria.
*
* @param language Language criteria
* @return The builder
*/
public SearchQueryBuilder language(String language) {
if (isValid(language)) {
query.append(SEARCH_SEPARATOR)
.append("lang:")
.append(language);
}
return this;
}
/**
* Add a shared criteria.
*
* @param shared Shared criteria
* @return The builder
*/
public SearchQueryBuilder shared(boolean shared) {
if (shared) {
query.append(SEARCH_SEPARATOR).append("shared:yes");
}
return this;
}
/**
* Add a tag criteria.
*
* @param tag Tag criteria
* @return The builder
*/
public SearchQueryBuilder tag(String tag) {
query.append(SEARCH_SEPARATOR)
.append("tag:")
.append(tag);
return this;
}
/**
* Add a before date criteria.
*
* @param before Before date criteria
* @return The builder
*/
public SearchQueryBuilder before(Date before) {
if (before != null) {
query.append(SEARCH_SEPARATOR)
.append("before:")
.append(DATE_FORMAT.format(before));
}
return this;
}
/**
* Add an after date criteria.
*
* @param after After date criteria
* @return The builder
*/
public SearchQueryBuilder after(Date after) {
if (after != null) {
query.append(SEARCH_SEPARATOR)
.append("after:")
.append(DATE_FORMAT.format(after));
}
return this;
}
/**
* Build the query.
*
* @return The query
*/
public String build() {
return query.toString();
}
/**
* Return true if the search criteria is valid.
*
* @param criteria Search criteria
* @return True if the search criteria is valid
*/
private boolean isValid(String criteria) {
return criteria != null && !criteria.trim().isEmpty();
}
}

View File

@@ -0,0 +1,39 @@
package com.sismics.docs.util;
import android.graphics.Color;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan;
import org.json.JSONArray;
import org.json.JSONObject;
/**
* Utility class for tags.
*
* @author bgamard.
*/
public class TagUtil {
/**
* Create a colored spannable from tags.
*
* @param tags Tags
* @return Colored spannable
*/
public static Spannable buildSpannable(JSONArray tags) {
SpannableStringBuilder builder = new SpannableStringBuilder();
for (int i = 0; i < tags.length(); i++) {
JSONObject tag = tags.optJSONObject(i);
int start = builder.length();
builder.append(" ").append(tag.optString("name")).append(" ");
builder.setSpan(new ForegroundColorSpan(Color.WHITE), start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
builder.setSpan(new BackgroundColorSpan(Color.parseColor(tag.optString("color"))), start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
builder.append(" ");
}
return builder;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 B

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