Compare commits
101 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08e4f6ddae | ||
|
|
86cae53789 | ||
|
|
5cbdb5d87d | ||
|
|
f8d889bb1f | ||
|
|
4625f9e42a | ||
|
|
6add34bb33 | ||
|
|
ea4e3fd8f2 | ||
|
|
b2a38cea62 | ||
|
|
0228d43442 | ||
|
|
060e5e8e24 | ||
|
|
566c563786 | ||
|
|
8597eac9f9 | ||
|
|
b7f920f864 | ||
|
|
1e3282eff3 | ||
|
|
451d913442 | ||
|
|
e3962c4e3f | ||
|
|
25136bc146 | ||
|
|
c727ac3a56 | ||
|
|
52387d93ac | ||
|
|
072dd7b280 | ||
|
|
42320dc9b9 | ||
|
|
ff994ce63b | ||
|
|
82ba0b5761 | ||
|
|
fc1bb22d8d | ||
|
|
6ff639baac | ||
|
|
4c24e7921a | ||
|
|
f7f5f93a9e | ||
|
|
f1eb3795d9 | ||
|
|
1f092f7d93 | ||
|
|
9b4b13a721 | ||
|
|
a1af1f369f | ||
|
|
c283607063 | ||
|
|
b8061a6a1e | ||
|
|
1c4161981b | ||
|
|
5e3093d0d3 | ||
|
|
0d4643cc93 | ||
|
|
80a2e0d055 | ||
|
|
06e97824df | ||
|
|
3461804399 | ||
|
|
bfc70baefb | ||
|
|
aa4b73b730 | ||
|
|
07247854ac | ||
|
|
9b5780bbb0 | ||
|
|
bad42d96f3 | ||
|
|
6ff1570b3c | ||
|
|
24345bc176 | ||
|
|
75cde7e9d6 | ||
|
|
192c2030d3 | ||
|
|
18cedaef2c | ||
|
|
d0c259ead2 | ||
|
|
2347483676 | ||
|
|
6c976087de | ||
|
|
c36014b46f | ||
|
|
5cf0532db7 | ||
|
|
6edae27d26 | ||
|
|
9ae8303b18 | ||
|
|
a0356845b1 | ||
|
|
6fa0b8494e | ||
|
|
c9210c39c4 | ||
|
|
790453047d | ||
|
|
5befef2992 | ||
|
|
dd59172a19 | ||
|
|
17e5c65d04 | ||
|
|
a762ce4715 | ||
|
|
89d66eca4a | ||
|
|
323b95ad7a | ||
|
|
b42b195245 | ||
|
|
a7987386e1 | ||
|
|
a181eac9a5 | ||
|
|
5662c080d6 | ||
|
|
745766a2c3 | ||
|
|
f44d30d890 | ||
|
|
ea0c7982ac | ||
|
|
2abf0c6eab | ||
|
|
13e8b828ac | ||
|
|
551c10e7a3 | ||
|
|
e17abfe411 | ||
|
|
3330acfc75 | ||
|
|
2837b21a86 | ||
|
|
5666d9b8b5 | ||
|
|
824e37b8ea | ||
|
|
1d08508e51 | ||
|
|
a84748f075 | ||
|
|
a6c123ad03 | ||
|
|
407564f28c | ||
|
|
6a9a166670 | ||
|
|
1773998ca0 | ||
|
|
c610364ef7 | ||
|
|
bc719b3165 | ||
|
|
92b2219bed | ||
|
|
8c5c54125f | ||
|
|
2ce5749226 | ||
|
|
76d195a344 | ||
|
|
04752cab0c | ||
|
|
ffa7d796b5 | ||
|
|
5119d32714 | ||
|
|
c0a963e845 | ||
|
|
20c2b0460b | ||
|
|
2ee979c012 | ||
|
|
bc3683990f | ||
|
|
c06e2971bc |
1
.gitignore
vendored
@@ -4,6 +4,7 @@
|
||||
/*/bin
|
||||
/*/gen
|
||||
/*/target
|
||||
/*/build
|
||||
/*/*.iml
|
||||
/out
|
||||
/.idea
|
||||
|
||||
10
Dockerfile
Normal 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
|
||||
29
README.md
@@ -1,7 +1,13 @@
|
||||
Sismics Docs
|
||||
============
|
||||
|
||||

|
||||
_Web interface_
|
||||
|
||||

|
||||
|
||||
_Android application_
|
||||
|
||||
  
|
||||
|
||||
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>.
|
||||
|
||||
@@ -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>
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
BIN
docs-android/app/libs/android-query.0.26.8.jar
Normal file
BIN
docs-android/app/libs/tokenautocomplete-1.2.1.jar
Normal 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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.sismics.docs.listener;
|
||||
|
||||
/**
|
||||
* Simple listener.
|
||||
*
|
||||
* @author bgamard
|
||||
*/
|
||||
public interface CallbackListener {
|
||||
public void onComplete();
|
||||
}
|
||||
@@ -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> </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> </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;
|
||||
}
|
||||
}
|
||||
@@ -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) { }
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
BIN
docs-android/app/src/main/res/drawable-xhdpi/eng.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
docs-android/app/src/main/res/drawable-xhdpi/fra.png
Normal file
|
After Width: | Height: | Size: 352 B |
|
After Width: | Height: | Size: 198 B |
|
After Width: | Height: | Size: 379 B |
|
After Width: | Height: | Size: 271 B |
|
After Width: | Height: | Size: 363 B |
|
After Width: | Height: | Size: 283 B |
|
After Width: | Height: | Size: 275 B |
|
After Width: | Height: | Size: 271 B |
|
After Width: | Height: | Size: 543 B |
|
After Width: | Height: | Size: 410 B |
|
After Width: | Height: | Size: 530 B |
|
After Width: | Height: | Size: 374 B |
|
Before Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 503 B |
|
After Width: | Height: | Size: 476 B |
|
After Width: | Height: | Size: 629 B |
|
After Width: | Height: | Size: 306 B |
BIN
docs-android/app/src/main/res/drawable-xhdpi/jpn.png
Normal file
|
After Width: | Height: | Size: 558 B |
BIN
docs-android/app/src/main/res/drawable-xxhdpi/eng.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
docs-android/app/src/main/res/drawable-xxhdpi/fra.png
Normal file
|
After Width: | Height: | Size: 505 B |
|
After Width: | Height: | Size: 222 B |
|
After Width: | Height: | Size: 493 B |
|
After Width: | Height: | Size: 341 B |
|
After Width: | Height: | Size: 476 B |
|
After Width: | Height: | Size: 353 B |
|
After Width: | Height: | Size: 339 B |
|
After Width: | Height: | Size: 325 B |