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

302 Commits
v1.2 ... v1.4

Author SHA1 Message Date
jendib
c695572b28 Release 1.4 2016-05-09 22:25:12 +02:00
jendib
79141edf70 Closes #97: Handle write permission in #/tag and #/tag/id 2016-05-09 22:09:29 +02:00
jendib
b1e58396d1 Closes #98: Fix inherited permissions table 2016-05-09 21:53:15 +02:00
jendib
b9cd113dc0 Closes #99: Update /app/batch/clean_storage & /app/batch/acl_tags 2016-05-09 19:11:44 +02:00
jendib
4a512af178 Bump version to 1.4-SNAPSHOT 2016-05-09 15:23:02 +02:00
jendib
9506e9b8b4 UI: minor spacing 2016-05-09 10:28:20 +02:00
jendib
3ff41d2002 Fix inherited ACLs displayed on documents 2016-05-08 23:40:08 +02:00
jendib
f41dafe76d Theme images expiration date 2016-05-08 23:31:33 +02:00
jendib
6f89a50fe5 Fix batch for ACLs on tags 2016-05-08 23:20:58 +02:00
jendib
e234440ce6 Closes #93: Edit tag color and title in #/tag/id 2016-05-08 23:05:44 +02:00
jendib
26685334a1 Closes #79: UI: Change background and logo image 2016-05-08 18:57:32 +02:00
jendib
4d79dd7076 #79: Change background and logo image 2016-05-08 17:25:21 +02:00
jendib
f5394534f7 #79: Change custom CSS and app name 2016-05-08 15:38:47 +02:00
jendib
faa66e01b6 Tag color in #/tag/id 2016-05-08 13:47:35 +02:00
jendib
bf4cb02de5 Closes #91: Display ACL inherited from tags in document permissions 2016-05-08 13:45:46 +02:00
jendib
642b9a63d3 Cleanup ACL checks 2016-05-08 12:14:06 +02:00
Benjamin Gamard
1ed7422171 Merge pull request #92 from sismics/tags_acl
Tags as source for ACL
2016-05-08 01:11:32 +02:00
jendib
3dd8a52f7d #83: Fix test for tag parent 2016-05-08 01:03:15 +02:00
jendib
a55c55bbdb Closes #83: Edit ACLs for tags in UI + batch for old DB 2016-05-08 00:46:32 +02:00
jendib
b851fd0ecc #83: GET /tag/id 2016-05-07 18:20:01 +02:00
jendib
c8f7fe15ef #83: Don't return non-visible tag parent 2016-05-07 15:53:13 +02:00
jendib
73133f5ba5 #83: Remove GET /tag/stats 2016-05-07 15:41:19 +02:00
jendib
eaf2e816b4 Imports 2016-05-06 00:55:00 +02:00
jendib
62020864ef #83: Fix ACL resource test 2016-05-06 00:49:41 +02:00
jendib
f12e3ec663 #83: Access documents by a shared tag 2016-05-06 00:36:54 +02:00
jendib
5226df53a2 Fix pom.xml after removing docs-parent 2016-05-05 22:43:18 +02:00
jendib
a59c67d774 docs-parent folder removed 2016-05-05 22:37:27 +02:00
jendib
1b1d5e9b4c #83: Use ACLs for tag operations 2016-05-05 22:36:53 +02:00
jendib
37fc2d09bb Entropy source for Travis 2016-05-05 22:18:07 +02:00
jendib
bc94466cf7 Entropy source for Travis 2016-05-05 22:16:04 +02:00
jendib
6af7b6fce9 Reduce tests verbosity 2016-05-05 21:59:50 +02:00
jendib
f2ae899938 Don't dump entities in JUnit 2016-05-05 21:33:31 +02:00
jendib
c398a3c4f5 #83: Tag name duplicates now allowed 2016-05-05 21:12:14 +02:00
jendib
27027ec412 #83: Tag DAO refactoring 2016-05-05 02:34:33 +02:00
jendib
ddf9e83a9b #83: Permission check for tags 2016-05-01 22:03:39 +02:00
jendib
0f661e5a34 #83: Handles tags as source ACL for single document 2016-04-30 02:17:04 +02:00
jendib
09a53d5c4e #83: Handles tags as source ACL in GET /document/list 2016-04-30 01:52:24 +02:00
jendib
542ab737a2 #79: POST /theme, GET /theme 2016-04-27 00:05:25 +02:00
jendib
6e1276293f #79: Change theme color UI 2016-04-23 23:47:33 +02:00
jendib
4e768e9103 #79: POST /theme/color to change the main color 2016-04-18 00:00:46 +02:00
jendib
55cdca0c7d Android: allow install on sdcard 2016-04-17 23:54:58 +02:00
jendib
9b52395786 Fix dependencies 2016-04-16 20:54:23 +02:00
jendib
50b02c800c git ignore 2016-04-14 20:51:23 +02:00
jendib
2bdae5ea5c cleanup 2016-04-14 20:49:39 +02:00
jendib
c49827ce25 Closes #90: Android: Fill audit log date 2016-04-14 00:43:29 +02:00
jendib
64db701498 Closes #84: Android: Ask a validation code on login 2016-04-14 00:37:01 +02:00
jendib
e16ce4b4f1 Init e2e testing 2016-04-13 01:30:02 +02:00
jendib
77d1e87fdb Android: upgrade Gradle 2016-04-13 01:29:27 +02:00
jendib
7d7adeeca0 #79: CSS generator 2016-04-13 01:29:03 +02:00
jendib
8ad9c529b6 #79: Resource to generate a dynamic CSS 2016-04-09 21:23:55 +02:00
jendib
274512a58e Fix if a file is deleted before text extraction is finished 2016-03-24 00:41:31 +01:00
jendib
ef16561272 Fix PDF export if description is null 2016-03-24 00:35:53 +01:00
jendib
98350860eb #84: Ask for a TOTP validation code (web UI) 2016-03-24 00:03:29 +01:00
jendib
1343948d33 #84: Enable/disable TOTP in UI 2016-03-23 23:48:54 +01:00
jendib
e616add75a #84: Init 2FA view + controllers refactoring 2016-03-23 22:31:09 +01:00
jendib
b33b7115ef #84: POST /user/disable_totp 2016-03-23 22:03:45 +01:00
jendib
fb0bb62eaf #84: TOTP key generation and validation code checking on login 2016-03-22 23:08:49 +01:00
jendib
5f84da61c8 Closes #88: XHR line loader with ngProgress 2016-03-22 22:35:42 +01:00
jendib
6e6babd2e3 #84: Import sources from https://github.com/wstrange/GoogleAuth 2016-03-22 22:15:19 +01:00
Benjamin Gamard
b28e08e2c7 Update README.md 2016-03-22 14:21:10 +01:00
jendib
718728a672 #84: Generate TOTP secret key 2016-03-22 01:18:18 +01:00
jendib
5de77e35dc Closes #87: Fix delete vocabulary after adding it 2016-03-22 00:38:56 +01:00
jendib
5a41e9555e Closes #82: Add role to groups 2016-03-20 22:18:58 +01:00
jendib
6598b585a2 Closes #18: Android: Group profile 2016-03-20 21:44:53 +01:00
jendib
a81474b40a Fix authentication cookie extraction 2016-03-20 19:39:52 +01:00
jendib
ee159f5b36 #18: Groups profile (web) 2016-03-20 19:12:38 +01:00
jendib
ced64a5d1f #18: Add/remove users from groups 2016-03-20 17:30:36 +01:00
jendib
689a4e6aae #18: Add/update/delete groups 2016-03-20 15:09:34 +01:00
jendib
21b3ba2bf6 #18: Handle new audit log for groups, filter users by group 2016-03-20 12:20:12 +01:00
jendib
7be2e1b9e5 #18: Add/display group ACL in web UI 2016-03-20 01:20:37 +01:00
jendib
c1c2228937 #18: GET /group + fix JUnit 2016-03-19 23:42:36 +01:00
jendib
3b9a66d1d8 #18: administrators group 2016-03-19 19:56:02 +01:00
jendib
a5ce5bf9ec #18: Group resource, groups handling in ACL, groups returned in users 2016-03-19 19:41:28 +01:00
jendib
43a1575187 #18: PUT /group 2016-03-17 01:43:10 +01:00
jendib
eb5f207cc1 #18: Group and user group DB model 2016-03-16 22:14:25 +01:00
jendib
de3f055323 #18: ACL check for groups 2016-03-15 22:44:50 +01:00
jendib
6012cdd9a5 fix junit 2016-03-15 21:25:47 +01:00
jendib
0fab8ff935 Nullable document metadata can be emptied 2016-03-15 00:58:55 +01:00
jendib
00ee2d3bf6 Closes #77: Metadata in PDF export 2016-03-15 00:43:27 +01:00
jendib
c2a2e9f585 DAO/event refactoring 2016-03-14 01:39:29 +01:00
jendib
31fff7e021 Update TODO 2016-03-13 23:18:33 +01:00
jendib
0dda01269f Search logs by min level instead of exact level 2016-03-13 23:13:12 +01:00
jendib
d58b0e8f74 Closes #73: Android: User profile 2016-03-13 19:23:52 +01:00
jendib
1bbb21c7c6 #73: Android: Display creator 2016-03-12 23:39:57 +01:00
jendib
24713f54e2 Close #72: Android: Audit log 2016-03-12 23:25:31 +01:00
jendib
5e2bd76e10 Close #71: Android: Advanced search by creator 2016-03-12 21:27:53 +01:00
jendib
78d4b5797b Information when the current user can't access a document 2016-03-12 20:31:39 +01:00
jendib
ff91521a67 Closes #67: Relations between document (client-side) 2016-03-12 20:29:02 +01:00
jendib
0525754337 #67: relations between documents (server-side) 2016-03-06 21:06:23 +01:00
jendib
ca8c525de0 Closes #80: Android: Use support design library for FAB 2016-03-06 14:51:19 +01:00
jendib
1e7d2fcfd9 Upgrade jersey, joda-time, hibernate 2016-03-05 19:53:41 +01:00
jendib
7e983bebb9 #67: Relations database schema 2016-03-03 23:54:48 +01:00
jendib
f927193ae9 Closes #78: login page design 2016-03-03 23:16:50 +01:00
jendib
a102bf04f4 #68: Contributors in share UI 2016-03-02 00:52:49 +01:00
Benjamin Gamard
919948489d Merge pull request #75 from sismics/lucene5
Closes #68: Display contributors in UI
2016-03-02 00:38:03 +01:00
jendib
12efd5c11f Closes #68: Display contributors in UI 2016-03-02 00:35:38 +01:00
Benjamin Gamard
25a2144b31 Merge pull request #74 from sismics/lucene5
Migration to Lucene 5
2016-03-01 23:54:17 +01:00
jendib
59682b5ba6 Closes #62: logs for index checking, explicit commit on close 2016-03-01 23:52:15 +01:00
Jean-Marc Tremeaux
7deaeca7b5 Make Docker use a volume instead of a volume container 2016-03-01 11:32:07 +01:00
jendib
a7a6adfa34 #62: Rebuild index if too old or corrupted 2016-03-01 01:24:26 +01:00
jendib
7f19f8c112 #62: Migration to Lucene 5 (without rebuilding old index) 2016-03-01 01:01:10 +01:00
jendib
943465a390 Closes #68: Add contributors list on documents 2016-02-21 23:43:35 +01:00
jendib
2824878065 Android: Upgrade Gradle tools 2016-02-21 15:04:26 +01:00
jendib
508a1230e9 Document updated event on file create/delete 2016-02-21 14:21:20 +01:00
jendib
0ad7ef43d5 #68: User ID available in events fired by a user 2016-02-21 14:11:17 +01:00
jendib
67171e05b9 Closes #70: User profile metadata 2016-02-20 23:49:54 +01:00
jendib
adebb7ff6d #70: User profiles UI 2016-02-17 00:28:48 +01:00
jendib
6fbcd46a76 #70: Init user profiles UI 2016-02-16 01:12:27 +01:00
jendib
ef3a592807 Closes #66: Search by creator 2016-02-15 23:09:45 +01:00
jendib
d8d01b077d Closes #69: Save and display originating user in audit log 2016-02-15 22:28:13 +01:00
jendib
831e2e60ed #65: Update README.md with Dublin Core metadata 2016-02-14 23:14:39 +01:00
jendib
2d858e6e11 #65: Limit vocabulary values to 500 characters 2016-02-14 23:11:24 +01:00
jendib
f9c3715d8d Closes #65: Type, coverage, rights metadata 2016-02-14 23:08:27 +01:00
jendib
359f5b5f49 #65: Publisher, format, source metadata 2016-02-14 22:47:49 +01:00
jendib
ed51b77b0e #65: Vocabulary admin UI 2016-02-14 21:51:46 +01:00
jendib
47082ceee9 #65: Vocabulary modification for admin only 2016-02-14 21:06:39 +01:00
jendib
98497f2a37 #65: Vocabulary resource 2016-02-14 21:00:21 +01:00
jendib
d3a74ed361 #65: PUT /vocabulary resource 2016-02-14 19:23:44 +01:00
jendib
7f2f480b25 #65: Init vocabulary resource 2016-02-14 01:58:32 +01:00
jendib
34d1422868 #65: Add subject and identifier metadata 2016-02-13 18:47:13 +01:00
jendib
509ab82745 Closes #55: Android: PDF download 2016-02-11 23:29:52 +01:00
jendib
7f325e3eb5 Android: EventBus 3 2016-02-09 22:44:24 +01:00
jendib
e23ca4b8c1 #63: Android: Null check for description in edit activity 2016-02-06 18:04:33 +01:00
jendib
a0f309c957 Upgrade libraries 2016-01-29 01:55:59 +01:00
jendib
0db4f1643d Bootstrap 3.3.6 2016-01-29 00:28:01 +01:00
jendib
cfa5888be9 #57: Android: Remove android-async-http for OkHttp 2016-01-26 00:57:48 +01:00
jendib
3172a5f216 Closes #59: Use TwelveMonkeys' ImageIO plugin for JPEG 2016-01-24 15:44:40 +01:00
jendib
456fc5b991 #57: Android: Migrate document resource to OkHttp
Closes #58: Android: OkHttpClient and cache as singleton
2016-01-23 23:20:09 +01:00
jendib
d9509474b0 #57: Android: Migrate GET /document/list to OkHttp 2016-01-21 23:48:40 +01:00
jendib
e7a289ffb5 Android: switch from AQuery to Picasso (+OkHttp) 2016-01-16 22:06:48 +01:00
jendib
b9a4f0f1e0 #55: Android: Export PDF dialog 2016-01-14 00:19:31 +01:00
jendib
0f4e5a8f6d Build against API 23 2016-01-13 23:27:39 +01:00
jendib
83e1191a8a #55: Export document in PDF (Share UI) 2016-01-01 21:38:25 +01:00
jendib
2c791f5123 #55: Export document in PDF (REST resource + export options UI) 2016-01-01 01:56:54 +01:00
jendib
25a17ae2da #55: Export document in PDF (UI) 2015-12-20 16:52:39 +01:00
jendib
0591f8a39f #55: Refactoring 2015-12-20 02:23:35 +01:00
jendib
eb61b06784 Android: update Gradle plugin 2015-12-20 02:12:59 +01:00
jendib
0d1a4ec7ea #55: Export document in PDF (utilities) 2015-12-13 22:29:23 +01:00
jendib
5f82752416 Quota updates are not polluting the audit log anymore 2015-12-12 01:56:54 +01:00
Benjamin Gamard
332de409b8 Update README.md 2015-12-11 22:28:46 +01:00
jendib
24d8784e1b Fix Junit for Unix systems 2015-12-11 22:22:21 +01:00
jendib
7708f61343 Closes #53: Build thumbnails for DOCX and ODT files 2015-12-11 22:00:44 +01:00
jendib
1a37d97a61 #53: Handle and extract text content from DOCX and ODT files 2015-12-07 23:53:30 +01:00
jendib
046984a447 Closes #51: File sizes displayed in kB or MB 2015-12-05 20:00:51 +01:00
jendib
5f516047bd #48: Soft delete before hard delete 2015-12-01 01:16:57 +01:00
jendib
e930ce4d47 Closes #48: Delete linked data properly + batch to clean orphan data 2015-12-01 00:32:57 +01:00
jendib
3dbdf88124 Closes #49: T_FILE.FIL_IDUSER_C non nullable 2015-11-30 01:02:54 +01:00
jendib
d428ce162b Merge branch 'master' of https://github.com/sismics/docs.git 2015-11-30 00:29:02 +01:00
jendib
bc323e9945 #41: Don't update deleted user quota in batch 2015-11-30 00:28:51 +01:00
Benjamin Gamard
ef69feeae7 Update README.md 2015-11-30 00:12:29 +01:00
jendib
e36143b61c Closes #41: File upload error handling + used storage updated 2015-11-30 00:08:47 +01:00
jendib
aa97253ec7 #41: Batch to rebuild quota storage + UI: show and edit quota 2015-11-29 23:14:33 +01:00
jendib
0fab0e4fc0 RAM Lucene storage for Junit + Surefire 2.18.1 forking mode 2015-11-29 20:22:24 +01:00
jendib
90a4949d76 #41: Quota increase/decrease when file is added/delete
+ java.nio-ization
2015-11-29 19:42:49 +01:00
jendib
1466fb4d6c maven central -> jcenter 2015-11-29 01:53:16 +01:00
jendib
d41172abb6 Cleanup 2015-11-29 01:53:03 +01:00
jendib
24ca81e91c #41: Storage quota editable only by admin role 2015-11-24 00:31:04 +01:00
jendib
1cae964c09 #41: DB: Storage quota and current usage, accessible from /user 2015-11-24 00:30:01 +01:00
jendib
dd671795e6 Android Studio 2 + small colors 2015-11-23 20:32:32 +01:00
jendib
2948c0c860 Update README.md 2015-11-23 00:01:39 +01:00
jendib
978fbf2cf9 Closes #45: Android: Delete comments 2015-11-22 23:59:21 +01:00
jendib
60ee000b6c #45: Android: Add comments 2015-11-22 20:32:26 +01:00
jendib
634ab7ec38 #45: Android: Show comments 2015-11-22 13:31:23 +01:00
jendib
7e5aa9aecf Closes #44: Comments visible from share app
+ metadata-complete="true" in web.xml to skip annotations scanning
(second try with Jetty 9)
2015-11-21 20:31:21 +01:00
jendib
1c7381376c Fix links to quick uploaded files from audit log 2015-11-21 17:50:10 +01:00
jendib
c7ce42fb3f Fix: handle deleted tag links in documents search 2015-11-19 00:10:04 +01:00
jendib
fc3a8bb4ae Closes #42: Gravatar images in comments 2015-11-19 00:05:04 +01:00
jendib
82b39586f0 Closes #32 : Display comments 2015-11-18 01:13:57 +01:00
jendib
c365c6f6e0 #32 : Comments layout
+ fix file viewer navigation
2015-11-17 02:48:07 +01:00
jendib
9afd52108b Merge branch 'master' of https://github.com/sismics/docs.git 2015-11-16 02:23:00 +01:00
jendib
97252bb5da #32: Comments system (server side) 2015-11-16 02:22:51 +01:00
Benjamin Gamard
7eeaeb01a0 Update README.md 2015-11-13 00:46:00 +01:00
jendib
b3e44b84d2 Closes #35: Android: Tag depth shown in tags tree 2015-11-03 00:04:09 +01:00
jendib
af23cd4948 Parent tag in GET /tag/stats 2015-11-02 23:54:07 +01:00
jendib
dc05ca0484 No wrap in tag tree 2015-11-02 22:19:58 +01:00
jendib
f94e069792 Closes #36: Android: Group ACLs by name 2015-11-02 22:12:26 +01:00
jendib
cd32f452e9 Closes #38: Handle JBIG2 images in PDF 2015-11-01 18:10:16 +01:00
jendib
66cb7333c1 Closes #33: subviews for /document/view/id 2015-11-01 14:48:07 +01:00
jendib
08633a993d Closes #34: nothing displayed if no description 2015-11-01 13:30:46 +01:00
jendib
2c782a23d8 Closes #37: Search terms in URL
+ empty tag tree
+ transitionTo -> go
+ audit log message can be empty
2015-09-22 01:34:01 +02:00
jendib
c7b7527183 Closes #23: Tag tree search 2015-09-15 23:03:42 +02:00
jendib
80bd11b44e #23: Edit tag parent 2015-09-15 00:14:13 +02:00
jendib
99a596b2e1 Merge branch 'master' of https://github.com/sismics/docs.git 2015-09-13 23:54:14 +02:00
jendib
cfde218d32 #23: Tag tree (server) 2015-09-13 23:54:06 +02:00
Benjamin Gamard
d8cefddebd Update README.md 2015-09-12 21:31:51 +02:00
jendib
50c7066f88 user agent and ip are nullable 2015-09-08 22:25:30 +02:00
jendib
a95dcf488d Closes #30: Delete locale & theme concept 2015-09-07 23:49:12 +02:00
jendib
0fe51d355c Closes #29: Upgrade to Jersey 2 2015-09-07 21:51:13 +02:00
jendib
97694d5d59 Closes #26: Cleanup Maven dependencies 2015-09-06 15:21:20 +02:00
jendib
e72fe3683c #4: Upgrade to unrelease PDFBox 2 2015-09-05 23:12:01 +02:00
jendib
44c10b60cd File update log is useless 2015-09-05 20:06:21 +02:00
jendib
467d14bacb Closes #24: Change to H2 database + indexes tweaks + queries tweaks
Tested up to 100k documents
2015-09-05 12:36:01 +02:00
jendib
6d73554967 #24: High performance is not going to happen on HSQLDB 2015-09-02 01:12:33 +02:00
jendib
36b5bf3bb2 Merge branch 'master' of https://github.com/sismics/docs.git 2015-08-31 22:53:44 +02:00
jendib
d14db1d3fb #24: Quick & dirty stress tester (slow at 60k docs with mem db) 2015-08-31 22:53:33 +02:00
jendib
9c97ab14f8 Catch all Tesseract related errors 2015-08-29 01:20:06 +02:00
jendib
6558ff7e05 tabs -> spaces 2015-08-29 00:14:47 +02:00
jendib
86473d5639 Merge branch 'master' of https://github.com/sismics/docs 2015-08-29 00:12:40 +02:00
jendib
374310d13c Init stress app 2015-08-29 00:12:15 +02:00
Benjamin Gamard
4396ef83a3 Update README.md 2015-08-28 01:32:29 +02:00
Benjamin Gamard
6b9ef4ab31 Update README.md 2015-08-28 01:18:18 +02:00
jendib
08e4f6ddae Closes #14: Soft delete on DocumentTag + audit log ordering 2015-08-28 01:02:33 +02:00
jendib
86cae53789 Closes #20: Clean error message if document or file does not exist 2015-08-26 22:11:39 +02:00
jendib
5cbdb5d87d Merge branch 'master' of https://github.com/sismics/docs.git 2015-08-24 22:18:55 +02:00
jendib
f8d889bb1f Closes #22: incorrect composite ID for DocumentTag 2015-08-24 22:18:47 +02:00
Benjamin Gamard
4625f9e42a Tesseract package for japanese language 2015-08-21 00:16:33 +02:00
jendib
6add34bb33 #20: Display logs on documents 2015-05-23 19:16:38 +02:00
jendib
ea4e3fd8f2 #20: Audit log displayed on main screen 2015-05-17 22:20:34 +02:00
jendib
b2a38cea62 Closes #21: Save IP and UA on login 2015-05-15 17:30:21 +02:00
jendib
0228d43442 Design fix 2015-05-11 11:34:49 +02:00
jendib
060e5e8e24 Android: ACLs in right drawer 2015-05-10 21:44:39 +02:00
jendib
566c563786 Android: metadata in right drawer 2015-05-10 14:44:45 +02:00
jendib
8597eac9f9 Fix ACL resource 2015-05-10 13:55:49 +02:00
jendib
b7f920f864 Native query for GET /document/id 2015-05-10 13:45:39 +02:00
jendib
1e3282eff3 Fix non-deletable ACL 2015-05-10 13:19:16 +02:00
jendib
451d913442 Simplify adding READ+WRITE ACL 2015-05-10 13:12:23 +02:00
jendib
e3962c4e3f Merge branch 'master' of https://github.com/sismics/docs.git 2015-05-09 23:35:20 +02:00
jendib
25136bc146 Refactor, TODO 2015-05-09 23:35:10 +02:00
jendib
c727ac3a56 Refactor, TODO 2015-05-09 23:34:43 +02:00
jendib
52387d93ac Closes #13: Don't show tags from other users 2015-05-09 21:52:01 +02:00
jendib
072dd7b280 Android: handle read-only documents, use ACLs for sharing 2015-05-09 21:51:06 +02:00
jendib
42320dc9b9 #13: Fix performance issue 2015-05-09 18:00:03 +02:00
jendib
ff994ce63b #13: Disable shared status in GET /document/list (too slow) 2015-05-09 16:48:01 +02:00
jendib
82ba0b5761 #13: Display the document's creator 2015-05-09 16:21:59 +02:00
jendib
fc1bb22d8d #13: ACL system 2015-05-09 14:44:19 +02:00
jendib
6ff639baac Android: advanced search (done) 2015-05-07 01:56:03 +02:00
jendib
4c24e7921a Android: advanced search UI 2015-05-05 23:00:23 +02:00
jendib
f7f5f93a9e Android: advanced search UI 2015-05-04 23:05:03 +02:00
jendib
f1eb3795d9 Android: login activity design 2015-05-04 20:51:32 +02:00
jendib
1f092f7d93 Android: signing 2015-05-02 19:37:56 +02:00
jendib
9b4b13a721 Convert JS click event to <a/> links 2015-05-02 17:15:37 +02:00
jendib
a1af1f369f Fixes #10: Page size for pagination 2015-05-01 19:44:20 +02:00
jendib
c283607063 Don't crash if a file is deleted before OCR is completed 2015-04-29 01:28:42 +02:00
jendib
b8061a6a1e Android: upgrade to AppCompat 22.1 2015-04-22 23:18:14 +02:00
jendib
1c4161981b Upload drag & dropped files sequentially 2015-03-29 16:05:42 +02:00
jendib
5e3093d0d3 Attach orphan files to a new document 2015-03-28 18:02:21 +01:00
jendib
0d4643cc93 File modal refactoring + orphan files selection 2015-03-28 00:09:28 +01:00
jendib
80a2e0d055 Drag & drop files to documents 2015-03-27 23:03:55 +01:00
Benjamin Gamard
06e97824df JCE prerequisite 2015-03-27 00:57:40 +01:00
jendib
3461804399 Drag & drop to upload orphan files 2015-03-27 00:50:00 +01:00
jendib
bfc70baefb Fix Junit 2015-03-24 23:59:06 +01:00
jendib
aa4b73b730 Android: don't fail build on lint errors 2015-03-24 23:53:28 +01:00
jendib
07247854ac #4 : Upgrade PDFBox 2015-03-24 22:20:54 +01:00
jendib
9b5780bbb0 Android: API 22 2015-03-24 01:07:32 +01:00
Jean-Marc Tremeaux
bad42d96f3 chmod +x 2015-03-13 15:58:32 +01:00
Jean-Marc Tremeaux
6ff1570b3c Deploy script 2015-03-11 22:51:24 +01:00
Jean-Marc Tremeaux
24345bc176 Deploy script 2015-03-11 22:37:35 +01:00
Walter
75cde7e9d6 Docker: Using sismics/data for data image 2015-03-11 14:31:10 +01:00
Walter
192c2030d3 Dockerization + Fix for Tesseract 3.03 2015-03-11 00:35:42 +01:00
jendib
18cedaef2c Orphan files are linked to a specific user 2015-03-06 22:40:33 +01:00
jendib
d0c259ead2 List orphan files 2015-03-06 21:23:50 +01:00
jendib
2347483676 Order of files attached to document 2015-03-06 21:13:09 +01:00
jendib
6c976087de Missing file + TODO 2015-03-03 00:26:40 +01:00
jendib
c36014b46f Ability to upload files without document (no OCR, no Lucene)
+ New resource to attach a document to a file and OCR/Lucene it
2015-03-03 00:23:30 +01:00
jendib
5cf0532db7 Page size selector 2015-03-02 00:07:42 +01:00
jendib
6edae27d26 Fixes #3: Race condition on settings screen 2015-01-25 19:20:49 +01:00
jendib
9ae8303b18 Android: sort tags by count desc 2015-01-15 01:57:29 +01:00
jendib
a0356845b1 Android: event listeners on main thread, files UI polish 2015-01-15 01:49:11 +01:00
jendib
6fa0b8494e Android: file delete 2015-01-14 23:31:53 +01:00
jendib
c9210c39c4 Android: file upload 2015-01-11 01:53:40 +01:00
jendib
790453047d Android: file upload background service 2015-01-08 00:34:57 +01:00
jendib
5befef2992 Android: Document deleting 2014-12-13 16:27:27 +01:00
jendib
dd59172a19 Android: Document form validation 2014-12-11 22:29:04 +01:00
jendib
17e5c65d04 Android: Document adding/editing 2014-12-11 22:13:06 +01:00
jendib
a762ce4715 Android: Update view after an editing 2014-12-05 00:12:42 +01:00
jendib
89d66eca4a Android: Document edit activity filling 2014-12-04 22:32:53 +01:00
jendib
323b95ad7a Android: language selection 2014-12-04 03:41:35 +01:00
jendib
b42b195245 Android: tags autocompletion 2014-12-04 02:28:44 +01:00
jendib
a7987386e1 Android: tags autocompletion (in progress) 2014-12-03 00:28:26 +01:00
jendib
a181eac9a5 Android: settings activity 2014-12-01 22:20:23 +01:00
jendib
5662c080d6 Merge branch 'master' of https://github.com/sismics/docs.git 2014-12-01 01:21:48 +01:00
jendib
745766a2c3 Fix error handling on DELETE /share/id 2014-12-01 01:21:38 +01:00
Benjamin Gamard
f44d30d890 Update README.md 2014-11-30 23:13:08 +01:00
Benjamin Gamard
ea0c7982ac Update README.md 2014-11-30 23:12:28 +01:00
jendib
2abf0c6eab Android: drawer background 2014-11-30 22:49:35 +01:00
jendib
13e8b828ac Android: filtering intents with an URL to Docs is not possible
The Android platform forces to specify a full hostname which is variable in our case
2014-11-30 22:02:07 +01:00
jendib
551c10e7a3 Android: file upload (in progress), title marquee 2014-11-30 16:11:35 +01:00
jendib
e17abfe411 Android: share editing 2014-11-29 23:50:56 +01:00
jendib
3330acfc75 Android: tags caching 2014-11-28 01:40:54 +01:00
jendib
2837b21a86 Android: init document edit activity 2014-11-26 23:30:25 +01:00
jendib
5666d9b8b5 Android: floating action button 2014-11-26 02:08:20 +01:00
jendib
824e37b8ea Android: swipe refresh on documents list 2014-11-26 01:01:36 +01:00
jendib
1d08508e51 Android: empty lists and loading error feedback 2014-11-25 23:53:52 +01:00
jendib
a84748f075 Android: better flags, tags loading feedback 2014-11-25 00:09:12 +01:00
jendib
a6c123ad03 Android: display shared status 2014-11-24 20:07:06 +01:00
jendib
407564f28c Android: show all documents from drawer 2014-11-23 22:10:24 +01:00
jendib
6a9a166670 Android: tags in drawer 2014-11-23 19:55:08 +01:00
jendib
1773998ca0 Android: Searching 2014-11-23 00:49:56 +01:00
jendib
c610364ef7 Android: Infinite scrolling 2014-11-22 22:43:31 +01:00
jendib
bc719b3165 Android: File(s) download 2014-11-22 22:18:21 +01:00
jendib
92b2219bed Android: Display tags, language and shared status on document 2014-11-22 14:06:08 +01:00
jendib
8c5c54125f Android: document details 2014-11-22 01:09:12 +01:00
jendib
2ce5749226 Android: documents listing: tags and date 2014-11-21 03:18:20 +01:00
jendib
76d195a344 Android: documents listing from API 2014-11-20 02:11:18 +01:00
jendib
04752cab0c Android: material design, API 21 2014-11-19 01:18:42 +01:00
jendib
ffa7d796b5 Upgrade Android build tools 2014-05-27 16:53:15 +02:00
jendib
5119d32714 Init login 2014-05-26 22:22:13 +02:00
jendib
c0a963e845 Button to clear the search field, bug fix on share modal 2014-05-12 21:39:20 +02:00
jendib
20c2b0460b TODO 2014-05-11 15:05:06 +02:00
jendib
2ee979c012 TODO 2014-05-11 15:02:02 +02:00
jendib
bc3683990f Merge branch 'master' of https://github.com/sismics/docs 2014-05-05 23:14:02 +02:00
jendib
c06e2971bc AS/Gradle upgrade 2014-05-05 23:13:23 +02:00
488 changed files with 34240 additions and 7117 deletions

5
.gitignore vendored
View File

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

11
.travis.yml Normal file
View File

@@ -0,0 +1,11 @@
sudo: required
dist: trusty
language: java
before_install:
- sudo apt-get -qq update
- sudo apt-get -y -q install tesseract-ocr tesseract-ocr-fra tesseract-ocr-jpn
- sudo apt-get -y -q install haveged && sudo service haveged start
env:
global:
- TESSDATA_PREFIX=/usr/share/tesseract-ocr
- LC_NUMERIC=C

10
Dockerfile Normal file
View File

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

View File

@@ -1,7 +1,13 @@
Sismics Docs
Sismics Docs [![Build Status](https://secure.travis-ci.org/sismics/docs.png)](http://travis-ci.org/sismics/docs)
============
![](http://www.bgamard.org/img/projects/sismicsdocs.jpg)
_Web interface_
![Web interface](http://sismics.com/docs/screenshot1.png)
_Android application_
![Android documents list](http://sismics.com/docs/android1.png) ![Android navigation](http://sismics.com/docs/android2.png) ![Android document details](http://sismics.com/docs/android3.png) ![Android document actions](http://sismics.com/docs/android4.png)
What is Docs?
---------------
@@ -15,29 +21,35 @@ Features
- Responsive user interface
- Optical character recognition
- Support image and PDF files
- Support image, PDF, ODT and DOCX files
- Flexible search engine
- Full text search in image and PDF
- SHA-256 encryption
- Tag system
- Multi-users
- Document sharing
- Full text search in all supported files
- All [Dublin Core](http://dublincore.org/) metadata
- 256-bit AES encryption of stored files
- Tag system with nesting
- User/group permission system
- Hierarchical groups
- Audit log
- Comments
- Storage quota per user
- Document sharing by URL
- RESTful Web API
- Fully featured Android client
- Tested to 100k documents
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:
- docs-parent
- docs-core
- docs-web
- docs-web-common
@@ -47,9 +59,8 @@ or download the sources from GitHub.
#### Launch the build
From the `docs-parent` directory:
From the root directory:
mvn -Pinit validate -N
mvn clean -DskipTests install
#### Run a stand-alone version
@@ -64,4 +75,10 @@ From the `docs-web` directory:
mvn -Pprod -DskipTests clean install
You will get your deployable WAR in the `target` directory.
You will get your deployable WAR in the `docs-web/target` directory.
License
-------
Docs is released under the terms of the GPL license. See `COPYING` for more
information or see <http://opensource.org/licenses/GPL-2.0>.

2
build.sh Executable file
View File

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

View File

@@ -1,76 +0,0 @@
<?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">
<component name="FacetManager">
<facet type="android" name="Android">
<configuration>
<option name="SELECTED_BUILD_VARIANT" value="debug" />
<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="SOURCE_GEN_TASK_NAME" value="generateDebugSources" />
<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" />
<option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/src/main/res" />
<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" />
<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$/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/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" />
<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/incremental" />
<excludeFolder url="file://$MODULE_DIR$/build/libs" />
<excludeFolder url="file://$MODULE_DIR$/build/manifests" />
<excludeFolder url="file://$MODULE_DIR$/build/res" />
<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="sourceFolder" forTests="false" />
<orderEntry type="library" exported="" name="support-v4-18.0.0" level="project" />
</component>
</module>

View File

@@ -1,24 +1,24 @@
buildscript {
repositories {
mavenCentral()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:0.7.+'
classpath 'com.android.tools.build:gradle:2.1.0-beta1'
}
}
apply plugin: 'android'
apply plugin: 'com.android.application'
repositories {
mavenCentral()
jcenter()
}
android {
compileSdkVersion 19
buildToolsVersion "19.0.0"
compileSdkVersion 23
buildToolsVersion '23.0.3'
defaultConfig {
minSdkVersion 14
targetSdkVersion 19
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
@@ -27,14 +27,36 @@ 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:23.3.0'
compile 'com.android.support:recyclerview-v7:23.3.0'
compile 'com.android.support:design:23.3.0'
compile 'it.sephiroth.android.library.imagezoom:imagezoom:1.0.5'
compile 'org.greenrobot:eventbus:3.0.0'
compile 'com.squareup.picasso:picasso:2.5.2'
compile 'com.squareup.okhttp3:okhttp:3.1.1'
compile "com.squareup.okhttp3:okhttp-urlconnection:3.1.1"
compile 'com.jakewharton.picasso:picasso2-okhttp3-downloader:1.0.2'
}

Binary file not shown.

View File

@@ -1,29 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.sismics.docs" >
package="com.sismics.docs"
android:installLocation="auto">
<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.AuditLogActivity"
android:label="@string/latest_activity">
</activity>
<activity
android:name=".activity.UserProfileActivity">
</activity>
<activity
android:name=".activity.GroupProfileActivity">
</activity>
<activity
android:name=".activity.SettingsActivity"
android:label="@string/settings">
</activity>
<provider android:name=".provider.RecentSuggestionsProvider"
android:exported="false"
android:authorities="com.sismics.docs.provider.RecentSuggestionsProvider" />
<service
android:name=".service.FileUploadService"
android:enabled="true"
android:exported="false" >
<intent-filter>
<action android:name="com.sismics.docs.file.upload"/>
</intent-filter>
</service>
</application>
</manifest>

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,26 @@
package com.sismics.docs;
import android.app.Application;
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 Provide documents to intent action get content
super.onCreate();
}
}

View File

@@ -0,0 +1,121 @@
package com.sismics.docs.activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.app.AppCompatActivity;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.ProgressBar;
import com.sismics.docs.R;
import com.sismics.docs.adapter.AuditLogListAdapter;
import com.sismics.docs.listener.HttpCallback;
import com.sismics.docs.model.application.ApplicationContext;
import com.sismics.docs.resource.AuditLogResource;
import org.json.JSONObject;
/**
* Audit log activity.
*
* @author bgamard.
*/
public class AuditLogActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Check if logged in
if (!ApplicationContext.getInstance().isLoggedIn()) {
startActivity(new Intent(this, LoginActivity.class));
finish();
return;
}
// Handle activity context
if (getIntent() == null) {
finish();
return;
}
// Input document ID (optional)
final String documentId = getIntent().getStringExtra("documentId");
// Setup the activity
setContentView(R.layout.auditlog_activity);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setHomeButtonEnabled(true);
}
// Configure the swipe refresh layout
SwipeRefreshLayout swipeRefreshLayout = (SwipeRefreshLayout) 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() {
refreshView(documentId);
}
});
// Navigate to user profile on click
final ListView auditLogListView = (ListView) findViewById(R.id.auditLogListView);
auditLogListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
if (auditLogListView.getAdapter() == null) {
return;
}
AuditLogListAdapter adapter = (AuditLogListAdapter) auditLogListView.getAdapter();
String username = adapter.getItem(position).optString("username");
Intent intent = new Intent(AuditLogActivity.this, UserProfileActivity.class);
intent.putExtra("username", username);
startActivity(intent);
}
});
// Get audit log list
refreshView(documentId);
}
/**
* Refresh the view.
*/
private void refreshView(String documentId) {
final SwipeRefreshLayout swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipeRefreshLayout);
final ProgressBar progressBar = (ProgressBar) findViewById(R.id.progressBar);
final ListView auditLogListView = (ListView) findViewById(R.id.auditLogListView);
progressBar.setVisibility(View.VISIBLE);
auditLogListView.setVisibility(View.GONE);
AuditLogResource.list(this, documentId, new HttpCallback() {
@Override
public void onSuccess(JSONObject response) {
auditLogListView.setAdapter(new AuditLogListAdapter(response.optJSONArray("logs")));
}
@Override
public void onFinish() {
progressBar.setVisibility(View.GONE);
auditLogListView.setVisibility(View.VISIBLE);
swipeRefreshLayout.setRefreshing(false);
}
});
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}

View File

@@ -0,0 +1,231 @@
package com.sismics.docs.activity;
import android.app.ProgressDialog;
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.HttpCallback;
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.greenrobot.eventbus.EventBus;
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;
/**
* 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.isNull("description") ? "" : 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);
// Server callback
HttpCallback callback = new HttpCallback() {
@Override
public void onSuccess(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 onFailure(JSONObject json, Exception e) {
Toast.makeText(DocumentEditActivity.this, R.string.error_editing_document, Toast.LENGTH_LONG).show();
}
@Override
public void onFinish() {
progressDialog.dismiss();
}
};
// Actual server call
if (document == null) {
DocumentResource.add(this, title, description, tagIdList, langId, createDate, callback);
} else {
DocumentResource.edit(this, document.optString("id"), title, description, tagIdList, langId, createDate, callback);
}
return true;
case android.R.id.home:
finish();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
}

View File

@@ -0,0 +1,808 @@
package com.sismics.docs.activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.ClipData;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
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.ContextMenu;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
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.CommentListAdapter;
import com.sismics.docs.adapter.FilePagerAdapter;
import com.sismics.docs.event.CommentAddEvent;
import com.sismics.docs.event.CommentDeleteEvent;
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.DocExportPdfFragment;
import com.sismics.docs.fragment.DocShareFragment;
import com.sismics.docs.listener.HttpCallback;
import com.sismics.docs.model.application.ApplicationContext;
import com.sismics.docs.resource.CommentResource;
import com.sismics.docs.resource.DocumentResource;
import com.sismics.docs.resource.FileResource;
import com.sismics.docs.service.FileUploadService;
import com.sismics.docs.util.NetworkUtil;
import com.sismics.docs.util.PreferenceUtil;
import com.sismics.docs.util.TagUtil;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* 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;
/**
* Comment list adapter.
*/
private CommentListAdapter commentListAdapter;
/**
* 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(final 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.isEmpty() || document.isNull("description")) {
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 export PDF
button = (Button) findViewById(R.id.actionExportPdf);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
DialogFragment dialog = DocExportPdfFragment.newInstance(
document.optString("id"), document.optString("title"));
dialog.show(getSupportFragmentManager(), "DocExportPdfFragment");
}
});
// Action share
button = (Button) findViewById(R.id.actionSharing);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
DialogFragment dialog = DocShareFragment.newInstance(document.optString("id"));
dialog.show(getSupportFragmentManager(), "DocShareFragment");
}
});
// Action audit log
button = (Button) findViewById(R.id.actionAuditLog);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(DocumentViewActivity.this, AuditLogActivity.class);
intent.putExtra("documentId", document.optString("id"));
startActivity(intent);
}
});
// Button add a comment
ImageButton imageButton = (ImageButton) findViewById(R.id.addCommentBtn);
imageButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
final EditText commentEditText = (EditText) findViewById(R.id.commentEditText);
if (commentEditText.getText().length() == 0) {
// No content for the new comment
return;
}
Toast.makeText(DocumentViewActivity.this, R.string.adding_comment, Toast.LENGTH_LONG).show();
CommentResource.add(DocumentViewActivity.this,
DocumentViewActivity.this.document.optString("id"),
commentEditText.getText().toString(),
new HttpCallback() {
public void onSuccess(JSONObject response) {
EventBus.getDefault().post(new CommentAddEvent(response));
commentEditText.setText("");
}
@Override
public void onFailure(JSONObject json, Exception e) {
Toast.makeText(DocumentViewActivity.this, R.string.comment_add_failure, Toast.LENGTH_LONG).show();
}
});
}
});
// Grab the comments
updateComments();
// Grab the attached files
updateFiles();
// Grab the full document (used for ACLs, remaining metadata 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.comments:
drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
if (drawerLayout.isDrawerVisible(GravityCompat.START)) {
drawerLayout.closeDrawer(GravityCompat.START);
} else {
drawerLayout.openDrawer(GravityCompat.START);
}
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 = document.optString("title") + "-" + position + "." + ext;
// Download the file
String fileUrl = PreferenceUtil.getServerUrl(this) + "/api/file/" + file.optString("id") + "/data";
NetworkUtil.downloadFile(this, fileUrl, fileName, document.optString("title"), getString(R.string.download_file_title));
}
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);
// Actual delete server call
final String fileId = file.optString("id");
FileResource.delete(DocumentViewActivity.this, fileId, new HttpCallback() {
@Override
public void onSuccess(JSONObject response) {
EventBus.getDefault().post(new FileDeleteEvent(fileId));
}
@Override
public void onFailure(JSONObject json, Exception e) {
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 = document.optString("title") + ".zip";
NetworkUtil.downloadFile(this, url, fileName, document.optString("title"), getString(R.string.download_document_title));
}
/**
* 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);
// Actual delete server call
final String documentId = document.optString("id");
DocumentResource.delete(DocumentViewActivity.this, documentId, new HttpCallback() {
@Override
public void onSuccess(JSONObject response) {
EventBus.getDefault().post(new DocumentDeleteEvent(documentId));
}
@Override
public void onFailure(JSONObject json, Exception e) {
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
*/
@Subscribe(threadMode = ThreadMode.MAIN)
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
*/
@Subscribe(threadMode = ThreadMode.MAIN)
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
*/
@Subscribe(threadMode = ThreadMode.MAIN)
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
*/
@Subscribe(threadMode = ThreadMode.MAIN)
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
*/
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(FileAddEvent event) {
if (document == null) return;
if (document.optString("id").equals(event.getDocumentId())) {
updateFiles();
}
}
/**
* A comment add event has been fired.
*
* @param event Comment add event
*/
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(CommentAddEvent event) {
if (commentListAdapter == null) return;
TextView emptyView = (TextView) findViewById(R.id.commentEmptyView);
ListView listView = (ListView) findViewById(R.id.commentListView);
emptyView.setVisibility(View.GONE);
listView.setVisibility(View.VISIBLE);
commentListAdapter.add(event.getComment());
}
/**
* A comment delete event has been fired.
*
* @param event Comment add event
*/
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(CommentDeleteEvent event) {
if (commentListAdapter == null) return;
TextView emptyView = (TextView) findViewById(R.id.commentEmptyView);
ListView listView = (ListView) findViewById(R.id.commentListView);
commentListAdapter.remove(event.getCommentId());
if (commentListAdapter.getCount() == 0) {
emptyView.setVisibility(View.VISIBLE);
listView.setVisibility(View.GONE);
}
}
@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 HttpCallback() {
@Override
public void onSuccess(JSONObject response) {
document = response;
boolean writable = document.optBoolean("writable");
if (menu != null) {
menu.findItem(R.id.delete_file).setVisible(writable);
}
// Action only available if the document is 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);
final AclListAdapter aclListAdapter = new AclListAdapter(document.optJSONArray("acls"));
aclListView.setAdapter(aclListAdapter);
aclListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
AclListAdapter.AclItem acl = aclListAdapter.getItem(position);
if (acl.getType().equals("USER")) {
Intent intent = new Intent(DocumentViewActivity.this, UserProfileActivity.class);
intent.putExtra("username", acl.getName());
startActivity(intent);
} else if (acl.getType().equals("GROUP")) {
Intent intent = new Intent(DocumentViewActivity.this, GroupProfileActivity.class);
intent.putExtra("name", acl.getName());
startActivity(intent);
}
}
});
// Remaining metadata
TextView creatorTextView = (TextView) findViewById(R.id.creatorTextView);
final String creator = document.optString("creator");
creatorTextView.setText(creator);
creatorTextView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(DocumentViewActivity.this, UserProfileActivity.class);
intent.putExtra("username", creator);
startActivity(intent);
}
});
}
});
}
@Override
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
switch (view.getId()) {
case R.id.commentListView:
if (commentListAdapter == null || document == null) return;
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
JSONObject comment = commentListAdapter.getItem(info.position);
boolean writable = document.optBoolean("writable");
String creator = comment.optString("creator");
String username = ApplicationContext.getInstance().getUserInfo().optString("username");
if (writable || creator.equals(username)) {
menu.add(Menu.NONE, 0, 0, getString(R.string.comment_delete));
}
}
}
@Override
public boolean onContextItemSelected(MenuItem item) {
// Use real ids if more than one item someday
if (item.getItemId() == 0) {
// Delete a comment
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo)item.getMenuInfo();
if (commentListAdapter == null) return false;
JSONObject comment = commentListAdapter.getItem(info.position);
final String commentId = comment.optString("id");
Toast.makeText(DocumentViewActivity.this, R.string.deleting_comment, Toast.LENGTH_LONG).show();
CommentResource.remove(DocumentViewActivity.this, commentId, new HttpCallback() {
@Override
public void onSuccess(JSONObject response) {
EventBus.getDefault().post(new CommentDeleteEvent(commentId));
}
@Override
public void onFailure(JSONObject json, Exception e) {
Toast.makeText(DocumentViewActivity.this, R.string.error_deleting_comment, Toast.LENGTH_LONG).show();
}
});
return true;
}
return false;
}
/**
* Refresh comments list.
*/
private void updateComments() {
if (document == null) return;
final View progressBar = findViewById(R.id.commentProgressView);
final TextView emptyView = (TextView) findViewById(R.id.commentEmptyView);
final ListView listView = (ListView) findViewById(R.id.commentListView);
progressBar.setVisibility(View.VISIBLE);
emptyView.setVisibility(View.GONE);
listView.setVisibility(View.GONE);
registerForContextMenu(listView);
CommentResource.list(this, document.optString("id"), new HttpCallback() {
@Override
public void onSuccess(JSONObject response) {
JSONArray comments = response.optJSONArray("comments");
commentListAdapter = new CommentListAdapter(DocumentViewActivity.this, comments);
listView.setAdapter(commentListAdapter);
listView.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.GONE);
if (comments.length() == 0) {
listView.setVisibility(View.GONE);
emptyView.setVisibility(View.VISIBLE);
}
}
@Override
public void onFailure(JSONObject json, Exception e) {
emptyView.setText(R.string.error_loading_comments);
progressBar.setVisibility(View.GONE);
listView.setVisibility(View.GONE);
emptyView.setVisibility(View.VISIBLE);
}
});
}
/**
* 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 HttpCallback() {
@Override
public void onSuccess(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 onFailure(JSONObject json, Exception e) {
filesEmptyView.setText(R.string.error_loading_files);
progressBar.setVisibility(View.GONE);
filesEmptyView.setVisibility(View.VISIBLE);
}
});
}
@Override
protected void onDestroy() {
EventBus.getDefault().unregister(this);
super.onDestroy();
}
}

View File

@@ -0,0 +1,92 @@
package com.sismics.docs.activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.MenuItem;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.sismics.docs.R;
import com.sismics.docs.listener.HttpCallback;
import com.sismics.docs.model.application.ApplicationContext;
import com.sismics.docs.resource.UserResource;
import org.json.JSONArray;
import org.json.JSONObject;
/**
* Group profile activity.
*
* @author bgamard.
*/
public class GroupProfileActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Check if logged in
if (!ApplicationContext.getInstance().isLoggedIn()) {
startActivity(new Intent(this, LoginActivity.class));
finish();
return;
}
// Handle activity context
if (getIntent() == null) {
finish();
return;
}
// Input name
final String name = getIntent().getStringExtra("name");
if (name == null) {
finish();
return;
}
// Setup the activity
setTitle(name);
setContentView(R.layout.groupprofile_activity);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setHomeButtonEnabled(true);
}
// Get the group and populate the view
final ProgressBar progressBar = (ProgressBar) findViewById(R.id.progressBar);
final View layoutView = findViewById(R.id.layout);
progressBar.setVisibility(View.VISIBLE);
layoutView.setVisibility(View.GONE);
UserResource.get(this, name, new HttpCallback() {
@Override
public void onSuccess(JSONObject json) {
TextView membersTextView = (TextView) findViewById(R.id.membersTextView);
JSONArray members = json.optJSONArray("members");
String output = "";
for (int i = 0; i < members.length(); i++) {
output += members.optString(i) + "; ";
}
membersTextView.setText(output);
}
@Override
public void onFinish() {
progressBar.setVisibility(View.GONE);
layoutView.setVisibility(View.VISIBLE);
}
});
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}

View File

@@ -0,0 +1,187 @@
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 android.widget.TextView;
import com.sismics.docs.R;
import com.sismics.docs.listener.CallbackListener;
import com.sismics.docs.listener.HttpCallback;
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.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);
TextView loginExplainTextView = (TextView) findViewById(R.id.loginExplain);
loginExplainTextView.setText(Html.fromHtml(getString(R.string.login_explain)));
loginExplainTextView.setMovementMethod(LinkMovementMethod.getInstance());
final EditText txtServer = (EditText) findViewById(R.id.txtServer);
final EditText txtUsername = (EditText) findViewById(R.id.txtUsername);
final EditText txtPassword = (EditText) findViewById(R.id.txtPassword);
final EditText txtValidationCode = (EditText) findViewById(R.id.txtValidationCode);
final Button btnConnect = (Button) findViewById(R.id.btnConnect);
loginForm = findViewById(R.id.loginForm);
progressBar = findViewById(R.id.progressBar);
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(), txtValidationCode.getText().toString(),
new HttpCallback() {
@Override
public void onSuccess(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 onFailure(JSONObject json, Exception e) {
loginForm.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.GONE);
if (json != null && json.optString("type").equals("ForbiddenError")) {
DialogUtil.showOkDialog(LoginActivity.this, R.string.login_fail_title, R.string.login_fail);
} else if (json != null && json.optString("type").equals("ValidationCodeRequired")) {
txtValidationCode.setVisibility(View.VISIBLE);
validator.addValidable(txtValidationCode, new Required());
validator.validate();
} 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 HttpCallback() {
@Override
public void onSuccess(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 onFailure(JSONObject json, Exception e) {
DialogUtil.showOkDialog(LoginActivity.this, R.string.network_error_title, R.string.network_error);
loginForm.setVisibility(View.VISIBLE);
}
@Override
public void onFinish() {
progressBar.setVisibility(View.GONE);
}
});
}
}
}

View File

@@ -0,0 +1,287 @@
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.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.HttpCallback;
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.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.json.JSONObject;
/**
* 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.addDrawerListener(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 HttpCallback() {
@Override
public void onSuccess(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 onFailure(JSONObject json, Exception e) {
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;
TagListAdapter.TagItem tagItem = adapter.getItem(position);
if (tagItem == null) return;
searchQuery("tag:" + tagItem.getName());
}
});
// 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");
}
});
// Click on Latest activity
View auditLogLayout = findViewById(R.id.auditLogLayout);
auditLogLayout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(MainActivity.this, AuditLogActivity.class));
}
});
handleIntent(getIntent());
EventBus.getDefault().register(this);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.logout:
UserResource.logout(getApplicationContext(), new HttpCallback() {
@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
*/
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(AdvancedSearchEvent event) {
searchQuery(event.getQuery());
}
@Override
protected void onDestroy() {
EventBus.getDefault().unregister(this);
super.onDestroy();
}
}

View File

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

View File

@@ -0,0 +1,91 @@
package com.sismics.docs.activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.MenuItem;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.sismics.docs.R;
import com.sismics.docs.listener.HttpCallback;
import com.sismics.docs.model.application.ApplicationContext;
import com.sismics.docs.resource.UserResource;
import org.json.JSONObject;
/**
* User profile activity.
*
* @author bgamard.
*/
public class UserProfileActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Check if logged in
if (!ApplicationContext.getInstance().isLoggedIn()) {
startActivity(new Intent(this, LoginActivity.class));
finish();
return;
}
// Handle activity context
if (getIntent() == null) {
finish();
return;
}
// Input username
final String username = getIntent().getStringExtra("username");
if (username == null) {
finish();
return;
}
// Setup the activity
setTitle(username);
setContentView(R.layout.userprofile_activity);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setHomeButtonEnabled(true);
}
// Get the user and populate the view
final ProgressBar progressBar = (ProgressBar) findViewById(R.id.progressBar);
final View layoutView = findViewById(R.id.layout);
progressBar.setVisibility(View.VISIBLE);
layoutView.setVisibility(View.GONE);
UserResource.get(this, username, new HttpCallback() {
@Override
public void onSuccess(JSONObject json) {
TextView emailTextView = (TextView) findViewById(R.id.emailTextView);
emailTextView.setText(json.optString("email"));
TextView quotaTextView = (TextView) findViewById(R.id.quotaTextView);
quotaTextView.setText(getString(R.string.storage_display,
Math.round(json.optLong("storage_current") / 1000000),
Math.round(json.optLong("storage_quota") / 1000000)));
}
@Override
public void onFinish() {
progressBar.setVisibility(View.GONE);
layoutView.setVisibility(View.VISIBLE);
}
});
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}

View File

@@ -0,0 +1,119 @@
package com.sismics.docs.adapter;
import android.content.Context;
import android.text.TextUtils;
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<AclItem> aclItemList;
/**
* ACL list adapter.
*
* @param acls ACLs
*/
public AclListAdapter(JSONArray acls) {
this.aclItemList = new ArrayList<>();
// Group ACLs
for (int i = 0; i < acls.length(); i++) {
JSONObject acl = acls.optJSONObject(i);
String type = acl.optString("type");
String name = acl.optString("name");
String perm = acl.optString("perm");
boolean found = false;
for (AclItem aclItem : aclItemList) {
if (aclItem.type.equals(type) && aclItem.name.equals(name)) {
aclItem.permList.add(perm);
found = true;
}
}
if (!found) {
AclItem aclItem = new AclItem();
aclItem.type = type;
aclItem.name = name;
aclItem.permList.add(perm);
this.aclItemList.add(aclItem);
}
}
}
@Override
public int getCount() {
return aclItemList.size();
}
@Override
public AclItem getItem(int position) {
return aclItemList.get(position);
}
@Override
public long getItemId(int position) {
return getItem(position).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 AclItem aclItem = getItem(position);
TextView typeTextView = (TextView) view.findViewById(R.id.typeTextView);
typeTextView.setText(aclItem.type);
TextView nameTextView = (TextView) view.findViewById(R.id.nameTextView);
nameTextView.setText(aclItem.name);
TextView permTextView = (TextView) view.findViewById(R.id.permTextView);
permTextView.setText(TextUtils.join(" + ", aclItem.permList));
return view;
}
/**
* An ACL item in the list.
* Permissions are grouped together.
*/
public static class AclItem {
private String type;
private String name;
private List<String> permList = new ArrayList<>();
public String getType() {
return type;
}
public String getName() {
return name;
}
@Override
public int hashCode() {
return (type + name).hashCode();
}
}
}

View File

@@ -0,0 +1,98 @@
package com.sismics.docs.adapter;
import android.content.Context;
import android.content.Intent;
import android.text.TextUtils;
import android.text.format.DateFormat;
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.Date;
import java.util.List;
/**
* Audit log list adapter.
*
* @author bgamard.
*/
public class AuditLogListAdapter extends BaseAdapter {
/**
* Shares.
*/
private List<JSONObject> logList;
/**
* Audit log list adapter.
*
* @param logs Logs
*/
public AuditLogListAdapter(JSONArray logs) {
this.logList = new ArrayList<>();
for (int i = 0; i < logs.length(); i++) {
logList.add(logs.optJSONObject(i));
}
}
@Override
public int getCount() {
return logList.size();
}
@Override
public JSONObject getItem(int position) {
return logList.get(position);
}
@Override
public long getItemId(int position) {
return getItem(position).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.auditlog_list_item, parent, false);
}
// Build message
final JSONObject log = getItem(position);
StringBuilder message = new StringBuilder(log.optString("class"));
switch (log.optString("type")) {
case "CREATE": message.append(" created"); break;
case "UPDATE": message.append(" updated"); break;
case "DELETE": message.append(" deleted"); break;
}
switch (log.optString("class")) {
case "Document":
case "Acl":
case "Tag":
case "User":
case "Group":
message.append(" : ");
message.append(log.optString("message"));
break;
}
// Fill the view
TextView usernameTextView = (TextView) view.findViewById(R.id.usernameTextView);
TextView messageTextView = (TextView) view.findViewById(R.id.messageTextView);
TextView dateTextView = (TextView) view.findViewById(R.id.dateTextView);
usernameTextView.setText(log.optString("username"));
messageTextView.setText(message);
String date = DateFormat.getDateFormat(parent.getContext()).format(new Date(log.optLong("create_date")));
dateTextView.setText(date);
return view;
}
}

View File

@@ -0,0 +1,115 @@
package com.sismics.docs.adapter;
import android.content.Context;
import android.text.format.DateFormat;
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 com.sismics.docs.util.OkHttpUtil;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* Comment list adapter.
*
* @author bgamard.
*/
public class CommentListAdapter extends BaseAdapter {
/**
* Tags.
*/
private List<JSONObject> commentList = new ArrayList<>();
/**
* Context.
*/
private Context context;
/**
* Comment list adapter.
*
* @param commentsArray Comments
*/
public CommentListAdapter(Context context, JSONArray commentsArray) {
this.context = context;
for (int i = 0; i < commentsArray.length(); i++) {
commentList.add(commentsArray.optJSONObject(i));
}
}
@Override
public int getCount() {
return commentList.size();
}
@Override
public JSONObject getItem(int position) {
return commentList.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.comment_list_item, parent, false);
}
// Fill the view
JSONObject comment = getItem(position);
TextView creatorTextView = (TextView) view.findViewById(R.id.creatorTextView);
TextView dateTextView = (TextView) view.findViewById(R.id.dateTextView);
TextView contentTextView = (TextView) view.findViewById(R.id.contentTextView);
ImageView gravatarImageView = (ImageView) view.findViewById(R.id.gravatarImageView);
creatorTextView.setText(comment.optString("creator"));
dateTextView.setText(DateFormat.getDateFormat(dateTextView.getContext()).format(new Date(comment.optLong("create_date"))));
contentTextView.setText(comment.optString("content"));
// Gravatar image
String gravatarUrl = "http://www.gravatar.com/avatar/" + comment.optString("creator_gravatar") + "?s=128d=identicon";
OkHttpUtil.picasso(context)
.load(gravatarUrl)
.into(gravatarImageView);
return view;
}
/**
* Add a new comment.
*
* @param comment Comment
*/
public void add(JSONObject comment) {
commentList.add(comment);
notifyDataSetChanged();
}
/**
* Remove a comment.
*
* @param commentId Comment ID
*/
public void remove(String commentId) {
for (JSONObject comment : commentList) {
if (comment.optString("id").equals(commentId)) {
commentList.remove(comment);
notifyDataSetChanged();
return;
}
}
}
}

View File

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

View File

@@ -0,0 +1,134 @@
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.sismics.docs.R;
import com.sismics.docs.util.OkHttpUtil;
import com.sismics.docs.util.PreferenceUtil;
import com.squareup.picasso.Callback;
import com.squareup.picasso.MemoryPolicy;
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;
/**
* Context.
*/
private Context context;
/**
* 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;
}
@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);
final 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";
// Load image
OkHttpUtil.picasso(context)
.load(fileUrl)
.memoryPolicy(MemoryPolicy.NO_CACHE, MemoryPolicy.NO_STORE) // Don't memory cache the images
.into(fileImageView, new Callback.EmptyCallback() {
@Override
public void onSuccess() {
progressBar.setVisibility(View.GONE);
}
});
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 || position < 0 || position >= files.size()) {
return null;
}
return files.get(position);
}
/**
* Remove a file.
*
* @param fileId File ID
*/
public void remove(String fileId) {
if (files == null || fileId == null) return;
for (JSONObject file : files) {
if (fileId.equals(file.optString("id"))) {
files.remove(file);
notifyDataSetChanged();
break;
}
}
}
@Override
public int getItemPosition(Object object) {
return POSITION_NONE;
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,136 @@
package com.sismics.docs.adapter;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.util.TypedValue;
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.List;
import java.util.Locale;
/**
* Tag list adapter.
*
* @author bgamard.
*/
public class TagListAdapter extends BaseAdapter {
/**
* Tags.
*/
private List<TagItem> tagItemList = new ArrayList<>();
/**
* Tag list adapter.
*
* @param tagsArray Tags
*/
public TagListAdapter(JSONArray tagsArray) {
List<JSONObject> tags = new ArrayList<>();
for (int i = 0; i < tagsArray.length(); i++) {
tags.add(tagsArray.optJSONObject(i));
}
// Reorder tags by parent/child relation and compute depth
int depth = 0;
initTags(tags, JSONObject.NULL.toString(), depth);
}
/**
* Init tags model recursively.
*
* @param tags All tags from server
* @param parentId Parent ID
* @param depth Depth
*/
private void initTags(List<JSONObject> tags, String parentId, int depth) {
// Get all tags with this parent
for (JSONObject tag : tags) {
String tagParentId = tag.optString("parent");
if (tagParentId.equals(parentId)) {
TagItem tagItem = new TagItem();
tagItem.id = tag.optString("id");
tagItem.name = tag.optString("name");
tagItem.count = tag.optInt("count");
tagItem.color = tag.optString("color");
tagItem.depth = depth;
tagItemList.add(tagItem);
initTags(tags, tagItem.id, depth + 1);
}
}
}
@Override
public int getCount() {
return tagItemList.size();
}
@Override
public TagItem getItem(int position) {
return tagItemList.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.tag_list_item, parent, false);
}
// Fill the view
TagItem tagItem = getItem(position);
TextView tagTextView = (TextView) view.findViewById(R.id.tagTextView);
tagTextView.setText(tagItem.name);
TextView tagCountTextView = (TextView) view.findViewById(R.id.tagCountTextView);
tagCountTextView.setText(String.format(Locale.ENGLISH, "%d", tagItem.count));
// Label color filtering
ImageView labelImageView = (ImageView) view.findViewById(R.id.labelImageView);
Drawable labelDrawable = labelImageView.getDrawable().mutate();
labelDrawable.setColorFilter(Color.parseColor(tagItem.color), PorterDuff.Mode.MULTIPLY);
labelImageView.setImageDrawable(labelDrawable);
labelImageView.invalidate();
// Offset according to depth
Resources resources = parent.getContext().getResources();
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) labelImageView.getLayoutParams();
layoutParams.leftMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, tagItem.depth * 12, resources.getDisplayMetrics());
labelImageView.setLayoutParams(layoutParams);
labelImageView.requestLayout();
return view;
}
/**
* A tag item in the tags list.
*/
public static class TagItem {
private String id;
private String name;
private int count;
private String color;
private int depth;
public String getName() {
return name;
}
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,31 @@
package com.sismics.docs.event;
/**
* Comment delete event.
*
* @author bgamard.
*/
public class CommentDeleteEvent {
/**
* Comment ID.
*/
private String commentId;
/**
* Create a comment add event.
*
* @param commentId Comment ID
*/
public CommentDeleteEvent(String commentId) {
this.commentId = commentId;
}
/**
* Getter of commentId.
*
* @return commentId
*/
public String getCommentId() {
return commentId;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,95 @@
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.widget.CheckBox;
import android.widget.SeekBar;
import android.widget.TextView;
import com.sismics.docs.R;
import com.sismics.docs.util.NetworkUtil;
import com.sismics.docs.util.PreferenceUtil;
import java.util.Locale;
/**
* Export PDF dialog fragment.
*
* @author bgamard.
*/
public class DocExportPdfFragment extends DialogFragment {
/**
* Export PDF dialog fragment.
*
* @param id Document ID
* @param title Document title
*/
public static DocExportPdfFragment newInstance(String id, String title) {
DocExportPdfFragment fragment = new DocExportPdfFragment();
Bundle args = new Bundle();
args.putString("id", id);
args.putString("title", title);
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_export_pdf_dialog, null);
final SeekBar marginSeekBar = (SeekBar) view.findViewById(R.id.marginSeekBar);
final CheckBox exportMetadataCheckbox = (CheckBox) view.findViewById(R.id.exportMetadataCheckbox);
final CheckBox exportCommentsCheckbox = (CheckBox) view.findViewById(R.id.exportCommentsCheckbox);
final CheckBox fitToPageCheckbox = (CheckBox) view.findViewById(R.id.fitToPageCheckbox);
final TextView marginValueText = (TextView) view.findViewById(R.id.marginValueText);
// Margin label follow seekbar value
marginSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
marginValueText.setText(String.format(Locale.ENGLISH, "%d", progress));
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
// Build the dialog
builder.setView(view)
.setPositiveButton(R.string.download, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
// Download the PDF
String pdfUrl = PreferenceUtil.getServerUrl(getActivity()) + "/api/document/" + getArguments().getString("id") + "/pdf?" +
"metadata=" + exportMetadataCheckbox.isChecked() + "&comments=" + exportCommentsCheckbox.isChecked() + "&fitimagetopage=" + fitToPageCheckbox.isChecked() +
"&margin=" + marginSeekBar.getProgress();
String title = getArguments().getString("title");
NetworkUtil.downloadFile(getActivity(), pdfUrl, title + ".pdf", title, getString(R.string.download_pdf_title));
getDialog().cancel();
}
})
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
getDialog().cancel();
}
});
return builder.create();
}
}

View File

@@ -0,0 +1,250 @@
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.HttpCallback;
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.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.json.JSONObject;
/**
* @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.addOnScrollListener(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
*/
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(SearchEvent event) {
query = event.getQuery();
loadDocuments(getView(), true);
}
/**
* A document edit event has been fired.
*
* @param event Document edit event
*/
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(DocumentEditEvent event) {
adapter.updateDocument(event.getDocument());
}
/**
* A document delete event has been fired.
*
* @param event Document delete event
*/
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(DocumentDeleteEvent event) {
adapter.deleteDocument(event.getDocumentId());
}
/**
* A document add event has been fired.
*
* @param event Document add event
*/
@Subscribe(threadMode = ThreadMode.MAIN)
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 HttpCallback() {
@Override
public void onSuccess(JSONObject response) {
adapter.addDocuments(response.optJSONArray("documents"));
documentsEmptyView.setText(R.string.no_documents);
recyclerView.setEmptyView(documentsEmptyView);
}
@Override
public void onFailure(JSONObject response, Exception e) {
documentsEmptyView.setText(R.string.error_loading_documents);
recyclerView.setEmptyView(documentsEmptyView);
if (!reset) {
// We are loading a new page, so the empty view won't be visible, pop a toast
Toast.makeText(getActivity(), R.string.error_loading_documents, Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFinish() {
swipeRefreshLayout.setRefreshing(false);
}
});
}
}

View File

@@ -0,0 +1,199 @@
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.HttpCallback;
import com.sismics.docs.resource.DocumentResource;
import com.sismics.docs.resource.ShareResource;
import com.sismics.docs.util.PreferenceUtil;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.json.JSONArray;
import org.json.JSONObject;
/**
* 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 HttpCallback() {
@Override
public void onSuccess(JSONObject response) {
shareNameEditText.setText("");
loadShares(getDialog().getWindow().getDecorView());
}
@Override
public void onFailure(JSONObject json, Exception e) {
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 HttpCallback() {
@Override
public void onSuccess(JSONObject response) {
document = response;
JSONArray acls = response.optJSONArray("acls");
shareProgressBar.setVisibility(View.GONE);
shareListView.setEmptyView(shareEmptyView);
shareListView.setAdapter(new ShareListAdapter(acls));
}
@Override
public void onFailure(JSONObject json, Exception e) {
getDialog().cancel();
Toast.makeText(getActivity(), R.string.error_loading_shares, Toast.LENGTH_SHORT).show();
}
});
}
/**
* A share delete event has been fired.
*
* @param event Share delete event
*/
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(ShareDeleteEvent event) {
ShareResource.delete(getActivity(), event.getId(), new HttpCallback() {
@Override
public void onSuccess(JSONObject response) {
loadShares(getDialog().getWindow().getDecorView());
}
@Override
public void onFailure(JSONObject json, Exception e) {
Toast.makeText(getActivity(), R.string.error_deleting_share, Toast.LENGTH_SHORT).show();
}
});
}
/**
* A share send event has been fired.
*
* @param event Share send event
*/
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(ShareSendEvent event) {
if (document == null) return;
// Build the share link
String serverUrl = PreferenceUtil.getServerUrl(getActivity());
String link = serverUrl + "/share.html#/share/" + document.optString("id") + "/" + event.getAcl().optString("id");
// Build the intent
Context context = getActivity();
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_SUBJECT, document.optString("title"));
intent.putExtra(Intent.EXTRA_TEXT, link);
intent.setType("text/plain");
// Open the target chooser
context.startActivity(Intent.createChooser(intent, context.getText(R.string.send_share_to)));
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EventBus.getDefault().register(this);
}
@Override
public void onDestroy() {
EventBus.getDefault().unregister(this);
super.onDestroy();
}
}

View File

@@ -0,0 +1,129 @@
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.greenrobot.eventbus.EventBus;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
/**
* 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 EditText creatorEditText = (EditText) view.findViewById(R.id.creatorEditText);
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())
.creator(creatorEditText.getText().toString())
.shared(sharedCheckbox.isChecked())
.language(((LanguageAdapter.Language) languageSpinner.getSelectedItem()).getId())
.before(beforeDatePicker.getDate())
.after(afterDatePicker.getDate());
// Fulltext criteria
String fulltextCriteria = fulltextEditText.getText().toString();
if (!fulltextCriteria.trim().isEmpty()) {
String[] criterias = fulltextCriteria.split(" ");
for (String criteria : criterias) {
queryBuilder.fulltextSearch(criteria);
}
}
// Tags criteria
for (Object object : tagsEditText.getObjects()) {
JSONObject tag = (JSONObject) object;
queryBuilder.tag(tag.optString("name"));
}
// Send the advanced search event
EventBus.getDefault().post(new AdvancedSearchEvent(queryBuilder.build()));
getDialog().cancel();
}
})
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
getDialog().cancel();
}
});
Dialog dialog = builder.create();
dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
return dialog;
}
}

View File

@@ -0,0 +1,88 @@
package com.sismics.docs.fragment;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;
import android.provider.SearchRecentSuggestions;
import android.widget.Toast;
import com.sismics.docs.R;
import com.sismics.docs.provider.RecentSuggestionsProvider;
import com.sismics.docs.util.ApplicationUtil;
import com.sismics.docs.util.OkHttpUtil;
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) {
OkHttpUtil.clearCache(getActivity());
Toast.makeText(getActivity(), R.string.pref_clear_cache_success, Toast.LENGTH_LONG).show();
return true;
}
});
// Initialize static text preferences
Preference versionPref = findPreference("pref_version");
versionPref.setSummary(getString(R.string.version) + " " + ApplicationUtil.getVersionName(getActivity())
+ " | " + getString(R.string.build) + " " + ApplicationUtil.getVersionCode(getActivity()));
}
@Override
public void onResume() {
super.onResume();
getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
}
@Override
public void onPause() {
getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
super.onPause();
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
Preference pref = findPreference(key);
if (pref instanceof ListPreference) {
ListPreference listPref = (ListPreference) pref;
pref.setSummary(listPref.getEntry());
}
}
}

View File

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

View File

@@ -0,0 +1,78 @@
package com.sismics.docs.listener;
import android.os.Handler;
import android.os.Looper;
import org.json.JSONObject;
import java.io.IOException;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.Response;
/**
* An HTTP callback.
*
* @author bgamard.
*/
public class HttpCallback {
public void onSuccess(JSONObject json) {
// Implement me
}
public void onFailure(JSONObject json, Exception e) {
// Implement me
}
public void onFinish() {
// Implement me
}
/**
* Build an OkHttp Callback from a HttpCallback.
*
* @param httpCallback HttpCallback
* @return OkHttp Callback
*/
public static Callback buildOkHttpCallback(final HttpCallback httpCallback) {
return new Callback() {
@Override
public void onResponse(final Call call, final Response response) throws IOException {
final String body = response.body().string();
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
if (response.isSuccessful()) {
try {
httpCallback.onSuccess(new JSONObject(body));
} catch (Exception e) {
httpCallback.onFailure(null, e);
}
} else {
try {
httpCallback.onFailure(new JSONObject(body), null);
} catch (Exception e) {
httpCallback.onFailure(null, e);
}
}
httpCallback.onFinish();
}
});
}
@Override
public void onFailure(final Call call, final IOException e) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
httpCallback.onFailure(null, e);
httpCallback.onFinish();
}
});
}
};
}
}

View File

@@ -0,0 +1,42 @@
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 {
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.getChildAdapterPosition(childView));
}
return false;
}
@Override
public void onTouchEvent(RecyclerView view, MotionEvent motionEvent) { }
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { }
}

View File

@@ -0,0 +1,99 @@
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.HttpCallback;
import com.sismics.docs.resource.UserResource;
import com.sismics.docs.util.PreferenceUtil;
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 HttpCallback() {
@Override
public void onSuccess(JSONObject json) {
// Save data in application context
if (!json.optBoolean("anonymous", true)) {
setUserInfo(activity.getApplicationContext(), json);
}
}
@Override
public void onFinish() {
if (callbackListener != null) {
callbackListener.onComplete();
}
}
});
}
}

View File

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

View File

@@ -0,0 +1,38 @@
package com.sismics.docs.resource;
import android.content.Context;
import com.sismics.docs.listener.HttpCallback;
import com.sismics.docs.util.OkHttpUtil;
import okhttp3.HttpUrl;
import okhttp3.Request;
/**
* Access to /auditlog API.
*
* @author bgamard
*/
public class AuditLogResource extends BaseResource {
/**
* GET /auditlog.
*
* @param context Context
* @param documentId Document ID
* @param callback Callback
*/
public static void list(Context context, String documentId, HttpCallback callback) {
HttpUrl.Builder httpUrlBuilder = HttpUrl.parse(getApiUrl(context) + "/auditlog")
.newBuilder();
if (documentId != null) {
httpUrlBuilder.addQueryParameter("document", documentId);
}
Request request = new Request.Builder()
.url(httpUrlBuilder.build())
.get()
.build();
OkHttpUtil.buildClient(context)
.newCall(request)
.enqueue(HttpCallback.buildOkHttpCallback(callback));
}
}

View File

@@ -0,0 +1,28 @@
package com.sismics.docs.resource;
import android.content.Context;
import com.sismics.docs.util.PreferenceUtil;
/**
* Base class for API access.
*
* @author bgamard
*/
public class BaseResource {
/**
* Returns cleaned API URL.
*
* @param context Context
* @return Cleaned API URL
*/
protected static String getApiUrl(Context context) {
String serverUrl = PreferenceUtil.getServerUrl(context);
if (serverUrl == null) {
return null;
}
return serverUrl + "/api";
}
}

View File

@@ -0,0 +1,73 @@
package com.sismics.docs.resource;
import android.content.Context;
import com.sismics.docs.listener.HttpCallback;
import com.sismics.docs.util.OkHttpUtil;
import okhttp3.FormBody;
import okhttp3.HttpUrl;
import okhttp3.Request;
/**
* Access to /comment API.
*
* @author bgamard
*/
public class CommentResource extends BaseResource {
/**
* GET /comment/id.
*
* @param context Context
* @param documentId Document ID
* @param callback Callback
*/
public static void list(Context context, String documentId, HttpCallback callback) {
Request request = new Request.Builder()
.url(HttpUrl.parse(getApiUrl(context) + "/comment/" + documentId))
.get()
.build();
OkHttpUtil.buildClient(context)
.newCall(request)
.enqueue(HttpCallback.buildOkHttpCallback(callback));
}
/**
* PUT /comment.
*
* @param context Context
* @param documentId Document ID
* @param content Comment content
* @param callback Callback
*/
public static void add(Context context, String documentId, String content, HttpCallback callback) {
Request request = new Request.Builder()
.url(HttpUrl.parse(getApiUrl(context) + "/comment"))
.put(new FormBody.Builder()
.add("id", documentId)
.add("content", content)
.build())
.build();
OkHttpUtil.buildClient(context)
.newCall(request)
.enqueue(HttpCallback.buildOkHttpCallback(callback));
}
/**
* DELETE /comment/id.
*
* @param context Context
* @param commentId Comment ID
* @param callback Callback
*/
public static void remove(Context context, String commentId, HttpCallback callback) {
Request request = new Request.Builder()
.url(HttpUrl.parse(getApiUrl(context) + "/comment/" + commentId))
.delete()
.build();
OkHttpUtil.buildClient(context)
.newCall(request)
.enqueue(HttpCallback.buildOkHttpCallback(callback));
}
}

View File

@@ -0,0 +1,141 @@
package com.sismics.docs.resource;
import android.content.Context;
import com.sismics.docs.listener.HttpCallback;
import com.sismics.docs.util.OkHttpUtil;
import java.util.Set;
import okhttp3.FormBody;
import okhttp3.HttpUrl;
import okhttp3.Request;
/**
* 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 callback Callback
*/
public static void list(Context context, int offset, String query, HttpCallback callback) {
Request request = new Request.Builder()
.url(HttpUrl.parse(getApiUrl(context) + "/document/list")
.newBuilder()
.addQueryParameter("limit", "20")
.addQueryParameter("offset", Integer.toString(offset))
.addQueryParameter("sort_column", "3")
.addQueryParameter("asc", "false")
.addQueryParameter("search", query)
.build())
.get()
.build();
OkHttpUtil.buildClient(context)
.newCall(request)
.enqueue(HttpCallback.buildOkHttpCallback(callback));
}
/**
* GET /document/id.
*
* @param context Context
* @param id ID
* @param callback Callback
*/
public static void get(Context context, String id, HttpCallback callback) {
Request request = new Request.Builder()
.url(HttpUrl.parse(getApiUrl(context) + "/document/" + id))
.get()
.build();
OkHttpUtil.buildClient(context)
.newCall(request)
.enqueue(HttpCallback.buildOkHttpCallback(callback));
}
/**
* DELETE /document/id.
*
* @param context Context
* @param id ID
* @param callback Callback
*/
public static void delete(Context context, String id, HttpCallback callback) {
Request request = new Request.Builder()
.url(HttpUrl.parse(getApiUrl(context) + "/document/" + id))
.delete()
.build();
OkHttpUtil.buildClient(context)
.newCall(request)
.enqueue(HttpCallback.buildOkHttpCallback(callback));
}
/**
* PUT /document.
*
* @param context Context
* @param title Title
* @param description Description
* @param tagIdList Tags ID list
* @param language Language
* @param createDate Create date
* @param callback Callback
*/
public static void add(Context context, String title, String description,
Set<String> tagIdList, String language, long createDate, HttpCallback callback) {
FormBody.Builder formBuilder = new FormBody.Builder()
.add("title", title)
.add("description", description)
.add("language", language)
.add("create_date", Long.toString(createDate));
for( String tagId : tagIdList) {
formBuilder.add("tags", tagId);
}
Request request = new Request.Builder()
.url(HttpUrl.parse(getApiUrl(context) + "/document"))
.put(formBuilder.build())
.build();
OkHttpUtil.buildClient(context)
.newCall(request)
.enqueue(HttpCallback.buildOkHttpCallback(callback));
}
/**
* 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 callback Callback
*/
public static void edit(Context context, String id, String title, String description,
Set<String> tagIdList, String language, long createDate, HttpCallback callback) {
FormBody.Builder formBuilder = new FormBody.Builder()
.add("title", title)
.add("description", description)
.add("language", language)
.add("create_date", Long.toString(createDate));
for( String tagId : tagIdList) {
formBuilder.add("tags", tagId);
}
Request request = new Request.Builder()
.url(HttpUrl.parse(getApiUrl(context) + "/document/" + id))
.post(formBuilder.build())
.build();
OkHttpUtil.buildClient(context)
.newCall(request)
.enqueue(HttpCallback.buildOkHttpCallback(callback));
}
}

View File

@@ -0,0 +1,123 @@
package com.sismics.docs.resource;
import android.content.Context;
import com.sismics.docs.listener.HttpCallback;
import com.sismics.docs.util.OkHttpUtil;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.internal.Util;
import okio.BufferedSink;
import okio.Okio;
import okio.Source;
/**
* Access to /file API.
*
* @author bgamard
*/
public class FileResource extends BaseResource {
/**
* GET /file/list.
*
* @param context Context
* @param documentId Document ID
* @param callback Callback
*/
public static void list(Context context, String documentId, HttpCallback callback) {
Request request = new Request.Builder()
.url(HttpUrl.parse(getApiUrl(context) + "/file/list")
.newBuilder()
.addQueryParameter("id", documentId)
.build())
.get()
.build();
OkHttpUtil.buildClient(context)
.newCall(request)
.enqueue(HttpCallback.buildOkHttpCallback(callback));
}
/**
* DELETE /file/id.
*
* @param context Context
* @param id ID
* @param callback Callback
*/
public static void delete(Context context, String id, HttpCallback callback) {
Request request = new Request.Builder()
.url(HttpUrl.parse(getApiUrl(context) + "/file/" + id))
.delete()
.build();
OkHttpUtil.buildClient(context)
.newCall(request)
.enqueue(HttpCallback.buildOkHttpCallback(callback));
}
/**
* PUT /file.
*
* @param context Context
* @param documentId Document ID
* @param is Input stream
* @param callback Callback
* @throws Exception
*/
public static void addSync(Context context, String documentId, final InputStream is, HttpCallback callback) throws Exception {
Request request = new Request.Builder()
.url(HttpUrl.parse(getApiUrl(context) + "/file"))
.put(new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("id", documentId)
.addFormDataPart("file", "file", new RequestBody() {
@Override
public MediaType contentType() {
return MediaType.parse("application/octet-stream");
}
@Override
public void writeTo(BufferedSink sink) throws IOException {
Source source = Okio.source(is);
try {
sink.writeAll(source);
} finally {
Util.closeQuietly(source);
}
}
})
.build())
.build();
Response response = OkHttpUtil.buildClient(context)
.newCall(request)
.execute();
// Call the right callback
final String body = response.body().string();
if (response.isSuccessful()) {
try {
callback.onSuccess(new JSONObject(body));
} catch (Exception e) {
callback.onFailure(null, e);
}
} else {
try {
callback.onFailure(new JSONObject(body), null);
} catch (Exception e) {
callback.onFailure(null, e);
}
}
callback.onFinish();
}
}

View File

@@ -0,0 +1,56 @@
package com.sismics.docs.resource;
import android.content.Context;
import com.sismics.docs.listener.HttpCallback;
import com.sismics.docs.util.OkHttpUtil;
import okhttp3.FormBody;
import okhttp3.HttpUrl;
import okhttp3.Request;
/**
* Access to /tag API.
*
* @author bgamard
*/
public class ShareResource extends BaseResource {
/**
* PUT /share.
*
* @param context Context
* @param documentId Document ID
* @param name Name
* @param callback Callback
*/
public static void add(Context context, String documentId, String name, HttpCallback callback) {
Request request = new Request.Builder()
.url(HttpUrl.parse(getApiUrl(context) + "/share"))
.put(new FormBody.Builder()
.add("id", documentId)
.add("name", name)
.build())
.build();
OkHttpUtil.buildClient(context)
.newCall(request)
.enqueue(HttpCallback.buildOkHttpCallback(callback));
}
/**
* DELETE /share.
*
* @param context Context
* @param id ID
* @param callback Callback
*/
public static void delete(Context context, String id, HttpCallback callback) {
Request request = new Request.Builder()
.url(HttpUrl.parse(getApiUrl(context) + "/share/" + id))
.delete()
.build();
OkHttpUtil.buildClient(context)
.newCall(request)
.enqueue(HttpCallback.buildOkHttpCallback(callback));
}
}

View File

@@ -0,0 +1,33 @@
package com.sismics.docs.resource;
import android.content.Context;
import com.sismics.docs.listener.HttpCallback;
import com.sismics.docs.util.OkHttpUtil;
import okhttp3.HttpUrl;
import okhttp3.Request;
/**
* Access to /tag API.
*
* @author bgamard
*/
public class TagResource extends BaseResource {
/**
* GET /tag/stats.
*
* @param context Context
* @param callback Callback
*/
public static void stats(Context context, HttpCallback callback) {
Request request = new Request.Builder()
.url(HttpUrl.parse(getApiUrl(context) + "/tag/stats"))
.get()
.build();
OkHttpUtil.buildClient(context)
.newCall(request)
.enqueue(HttpCallback.buildOkHttpCallback(callback));
}
}

View File

@@ -0,0 +1,90 @@
package com.sismics.docs.resource;
import android.content.Context;
import com.sismics.docs.listener.HttpCallback;
import com.sismics.docs.util.OkHttpUtil;
import okhttp3.FormBody;
import okhttp3.HttpUrl;
import okhttp3.Request;
/**
* Access to /user API.
*
* @author bgamard
*/
public class UserResource extends BaseResource {
/**
* POST /user/login.
*
* @param context Context
* @param username Username
* @param password Password
* @param callback Callback
*/
public static void login(Context context, String username, String password, String code, HttpCallback callback) {
Request request = new Request.Builder()
.url(HttpUrl.parse(getApiUrl(context) + "/user/login"))
.post(new FormBody.Builder()
.add("username", username)
.add("password", password)
.add("code", code)
.add("remember", "true")
.build())
.build();
OkHttpUtil.buildClient(context)
.newCall(request)
.enqueue(HttpCallback.buildOkHttpCallback(callback));
}
/**
* GET /user.
*
* @param context Context
* @param callback Callback
*/
public static void info(Context context, HttpCallback callback) {
Request request = new Request.Builder()
.url(HttpUrl.parse(getApiUrl(context) + "/user"))
.get()
.build();
OkHttpUtil.buildClient(context)
.newCall(request)
.enqueue(HttpCallback.buildOkHttpCallback(callback));
}
/**
* GET /user/username.
*
* @param context Context
* param username Username
* @param callback Callback
*/
public static void get(Context context, String username, HttpCallback callback) {
Request request = new Request.Builder()
.url(HttpUrl.parse(getApiUrl(context) + "/user/" + username))
.get()
.build();
OkHttpUtil.buildClient(context)
.newCall(request)
.enqueue(HttpCallback.buildOkHttpCallback(callback));
}
/**
* POST /user/logout.
*
* @param context Context
* @param callback Callback
*/
public static void logout(Context context, HttpCallback callback) {
Request request = new Request.Builder()
.url(HttpUrl.parse(getApiUrl(context) + "/user/logout"))
.post(new FormBody.Builder().build())
.build();
OkHttpUtil.buildClient(context)
.newCall(request)
.enqueue(HttpCallback.buildOkHttpCallback(callback));
}
}

View File

@@ -0,0 +1,229 @@
package com.sismics.docs.resource.cookie;
import android.content.Context;
import android.content.SharedPreferences;
import android.text.TextUtils;
import android.util.Log;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.CookieStore;
import java.net.HttpCookie;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* A persistent cookie store which implements the Apache HttpClient CookieStore interface.
* Cookies are stored and will persist on the user's device between application sessions since they
* are serialized and stored in SharedPreferences.
*/
public class PersistentCookieStore implements CookieStore {
private static final String LOG_TAG = "PersistentCookieStore";
private static final String COOKIE_PREFS = "CookiePrefsFileOkHttp";
private static final String COOKIE_NAME_PREFIX = "cookie_okhttp_";
private final HashMap<String, ConcurrentHashMap<String, HttpCookie>> cookies;
private final SharedPreferences cookiePrefs;
/**
* Construct a persistent cookie store.
*
* @param context Context to attach cookie store to
*/
public PersistentCookieStore(Context context) {
cookiePrefs = context.getSharedPreferences(COOKIE_PREFS, 0);
cookies = new HashMap<>();
// Load any previously stored cookies into the store
Map<String, ?> prefsMap = cookiePrefs.getAll();
for (Map.Entry<String, ?> entry : prefsMap.entrySet()) {
if (entry.getValue() != null && !((String) entry.getValue()).startsWith(COOKIE_NAME_PREFIX)) {
String[] cookieNames = TextUtils.split((String) entry.getValue(), ",");
for (String name : cookieNames) {
String encodedCookie = cookiePrefs.getString(COOKIE_NAME_PREFIX + name, null);
if (encodedCookie != null) {
HttpCookie decodedCookie = decodeCookie(encodedCookie);
if (decodedCookie != null) {
if (!cookies.containsKey(entry.getKey()))
cookies.put(entry.getKey(), new ConcurrentHashMap<String, HttpCookie>());
cookies.get(entry.getKey()).put(name, decodedCookie);
}
}
}
}
}
}
@Override
public void add(URI uri, HttpCookie cookie) {
String name = getCookieToken(uri, cookie);
// Save cookie into local store, or remove if expired
if (!cookie.hasExpired()) {
if (!cookies.containsKey(uri.getHost()))
cookies.put(uri.getHost(), new ConcurrentHashMap<String, HttpCookie>());
cookies.get(uri.getHost()).put(name, cookie);
} else {
if (cookies.containsKey(uri.toString()))
cookies.get(uri.getHost()).remove(name);
}
// Save cookie into persistent store
SharedPreferences.Editor prefsWriter = cookiePrefs.edit();
prefsWriter.putString(uri.getHost(), TextUtils.join(",", cookies.get(uri.getHost()).keySet()));
prefsWriter.putString(COOKIE_NAME_PREFIX + name, encodeCookie(new SerializableHttpCookie(cookie)));
prefsWriter.apply();
}
protected String getCookieToken(URI uri, HttpCookie cookie) {
return cookie.getName() + cookie.getDomain();
}
@Override
public List<HttpCookie> get(URI uri) {
ArrayList<HttpCookie> ret = new ArrayList<>();
if (cookies.containsKey(uri.getHost()))
ret.addAll(cookies.get(uri.getHost()).values());
return ret;
}
@Override
public boolean removeAll() {
SharedPreferences.Editor prefsWriter = cookiePrefs.edit();
prefsWriter.clear();
prefsWriter.apply();
cookies.clear();
return true;
}
@Override
public boolean remove(URI uri, HttpCookie cookie) {
String name = getCookieToken(uri, cookie);
if (cookies.containsKey(uri.getHost()) && cookies.get(uri.getHost()).containsKey(name)) {
cookies.get(uri.getHost()).remove(name);
SharedPreferences.Editor prefsWriter = cookiePrefs.edit();
if (cookiePrefs.contains(COOKIE_NAME_PREFIX + name)) {
prefsWriter.remove(COOKIE_NAME_PREFIX + name);
}
prefsWriter.putString(uri.getHost(), TextUtils.join(",", cookies.get(uri.getHost()).keySet()));
prefsWriter.apply();
return true;
} else {
return false;
}
}
@Override
public List<HttpCookie> getCookies() {
ArrayList<HttpCookie> ret = new ArrayList<>();
for (String key : cookies.keySet())
ret.addAll(cookies.get(key).values());
return ret;
}
@Override
public List<URI> getURIs() {
ArrayList<URI> ret = new ArrayList<>();
for (String key : cookies.keySet())
try {
ret.add(new URI(key));
} catch (URISyntaxException e) {
e.printStackTrace();
}
return ret;
}
/**
* Serializes Cookie object into String
*
* @param cookie cookie to be encoded, can be null
* @return cookie encoded as String
*/
protected String encodeCookie(SerializableHttpCookie cookie) {
if (cookie == null)
return null;
ByteArrayOutputStream os = new ByteArrayOutputStream();
try {
ObjectOutputStream outputStream = new ObjectOutputStream(os);
outputStream.writeObject(cookie);
} catch (IOException e) {
Log.d(LOG_TAG, "IOException in encodeCookie", e);
return null;
}
return byteArrayToHexString(os.toByteArray());
}
/**
* Returns cookie decoded from cookie string
*
* @param cookieString string of cookie as returned from http request
* @return decoded cookie or null if exception occured
*/
protected HttpCookie decodeCookie(String cookieString) {
byte[] bytes = hexStringToByteArray(cookieString);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
HttpCookie cookie = null;
try {
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
cookie = ((SerializableHttpCookie) objectInputStream.readObject()).getCookie();
} catch (IOException e) {
Log.d(LOG_TAG, "IOException in decodeCookie", e);
} catch (ClassNotFoundException e) {
Log.d(LOG_TAG, "ClassNotFoundException in decodeCookie", e);
}
return cookie;
}
/**
* Using some super basic byte array &lt;-&gt; hex conversions so we don't have to rely on any
* large Base64 libraries. Can be overridden if you like!
*
* @param bytes byte array to be converted
* @return string containing hex values
*/
protected String byteArrayToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte element : bytes) {
int v = element & 0xff;
if (v < 16) {
sb.append('0');
}
sb.append(Integer.toHexString(v));
}
return sb.toString().toUpperCase(Locale.US);
}
/**
* Converts hex values from strings to byte arra
*
* @param hexString string of hex-encoded values
* @return decoded byte array
*/
protected byte[] hexStringToByteArray(String hexString) {
int len = hexString.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + Character.digit(hexString.charAt(i + 1), 16));
}
return data;
}
}

View File

@@ -0,0 +1,55 @@
package com.sismics.docs.resource.cookie;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.net.HttpCookie;
public class SerializableHttpCookie implements Serializable {
private static final long serialVersionUID = 6374381323722046732L;
private transient final HttpCookie cookie;
private transient HttpCookie clientCookie;
public SerializableHttpCookie(HttpCookie cookie) {
this.cookie = cookie;
}
public HttpCookie getCookie() {
HttpCookie bestCookie = cookie;
if (clientCookie != null) {
bestCookie = clientCookie;
}
return bestCookie;
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeObject(cookie.getName());
out.writeObject(cookie.getValue());
out.writeObject(cookie.getComment());
out.writeObject(cookie.getCommentURL());
out.writeObject(cookie.getDomain());
out.writeLong(cookie.getMaxAge());
out.writeObject(cookie.getPath());
out.writeObject(cookie.getPortlist());
out.writeInt(cookie.getVersion());
out.writeBoolean(cookie.getSecure());
out.writeBoolean(cookie.getDiscard());
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
String name = (String) in.readObject();
String value = (String) in.readObject();
clientCookie = new HttpCookie(name, value);
clientCookie.setComment((String) in.readObject());
clientCookie.setCommentURL((String) in.readObject());
clientCookie.setDomain((String) in.readObject());
clientCookie.setMaxAge(in.readLong());
clientCookie.setPath((String) in.readObject());
clientCookie.setPortlist((String) in.readObject());
clientCookie.setVersion(in.readInt());
clientCookie.setSecure(in.readBoolean());
clientCookie.setDiscard(in.readBoolean());
}
}

View File

@@ -0,0 +1,138 @@
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.HttpCallback;
import com.sismics.docs.resource.FileResource;
import org.greenrobot.eventbus.EventBus;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import okhttp3.internal.Util;
/**
* 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 HttpCallback() {
@Override
public void onSuccess(JSONObject response) {
EventBus.getDefault().post(new FileAddEvent(documentId, response.optString("id")));
FileUploadService.this.onComplete();
}
@Override
public void onFailure(JSONObject json, Exception e) {
FileUploadService.this.onError();
}
@Override
public void onFinish() {
Util.closeQuietly(is);
}
});
}
/**
* On upload start.
*/
private void onStart() {
notification.setContentTitle(getString(R.string.upload_notification_title))
.setContentText(getString(R.string.upload_notification_message))
.setContentIntent(PendingIntent.getBroadcast(this, 0, new Intent(), PendingIntent.FLAG_UPDATE_CURRENT))
.setSmallIcon(R.drawable.ic_file_upload_white_24dp)
.setProgress(100, 0, true)
.setOngoing(true);
startForeground(UPLOAD_NOTIFICATION_ID, notification.build());
}
/**
* On upload complete.
*/
private void onComplete() {
stopForeground(true);
}
/**
* On upload error.
*/
private void onError() {
stopForeground(false);
notification.setContentTitle(getString(R.string.upload_notification_title))
.setContentText(getString(R.string.upload_notification_error))
.setSmallIcon(R.drawable.ic_file_upload_white_24dp)
.setProgress(0, 0, false)
.setOngoing(false);
notificationManager.notify(UPLOAD_NOTIFICATION_ID_DONE, notification.build());
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,33 @@
package com.sismics.docs.util;
import android.app.DownloadManager;
import android.content.Context;
import android.net.Uri;
import android.os.Environment;
/**
* Utility class for network actions.
*
* @author bgamard.
*/
public class NetworkUtil {
/**
* 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
*/
public static void downloadFile(Context context, String url, String fileName, String title, String description) {
String authToken = PreferenceUtil.getAuthToken(context);
DownloadManager downloadManager = (DownloadManager) context.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);
}
}

View File

@@ -0,0 +1,187 @@
package com.sismics.docs.util;
import android.content.Context;
import android.os.Build;
import android.util.Log;
import com.jakewharton.picasso.OkHttp3Downloader;
import com.sismics.docs.resource.cookie.PersistentCookieStore;
import com.squareup.picasso.Picasso;
import java.io.IOException;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.security.cert.CertificateException;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import okhttp3.Cache;
import okhttp3.Interceptor;
import okhttp3.JavaNetCookieJar;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
/**
* Utilities for OkHttp.
*
* @author bgamard.
*/
public class OkHttpUtil {
/**
* OkHttp singleton client.
*/
private static OkHttpClient okHttpClient = new OkHttpClient();
/**
* Singleton cache.
*/
private static Cache cache = null;
/**
* User-Agent to use.
*/
protected static String userAgent = null;
/**
* Accept-Language header.
*/
protected static String acceptLanguage = null;
static {
// OkHttp configuration
try {
// Create a trust manager that does not validate certificate chains
final TrustManager[] trustAllCerts = new TrustManager[] {
new X509TrustManager() {
@Override
public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return null;
}
}
};
// Install the all-trusting trust manager
final SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
final javax.net.ssl.SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
// Configure OkHttpClient
okHttpClient = okHttpClient.newBuilder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.sslSocketFactory(sslSocketFactory)
.build();
} catch (Exception e) {
// NOP
}
}
/**
* Build a Picasso object with base config.
*
* @param context Context
* @return Picasso object
*/
public static Picasso picasso(Context context) {
OkHttpClient okHttpClient = buildClient(context)
.newBuilder()
.addInterceptor(new Interceptor() {
@Override
public Response intercept(Interceptor.Chain chain) throws IOException { // Override cache configuration
final Request original = chain.request();
return chain.proceed(original.newBuilder()
.header("Cache-Control", "max-age=" + (3600 * 24 * 365))
.method(original.method(), original.body())
.build());
}
})
.cache(getCache(context))
.build();
Picasso picasso = new Picasso.Builder(context)
.downloader(new OkHttp3Downloader(okHttpClient))
.build();
picasso.setIndicatorsEnabled(false); // Debug stuff
return picasso;
}
/**
* Get and eventually build the singleton cache.
*
* @param context Context
* @return Cache
*/
private static Cache getCache(Context context) {
if (cache == null) {
cache = new Cache(context.getCacheDir(),
PreferenceUtil.getIntegerPreference(context, PreferenceUtil.PREF_CACHE_SIZE, 0) * 1000000);
}
return cache;
}
/**
* Clear the HTTP cache.
*
* @param context Context
*/
public static void clearCache(Context context) {
Cache cache = getCache(context);
try {
cache.evictAll();
} catch (IOException e) {
Log.e("OKHttpUtil", "Error clearing cache", e);
}
}
/**
* Build an OkHttpClient.
*
* @param context Context
* @return OkHttpClient
*/
public static OkHttpClient buildClient(final Context context) {
// One-time header computation
if (userAgent == null) {
userAgent = "Sismics Docs Android " + ApplicationUtil.getVersionName(context) + "/Android " + Build.VERSION.RELEASE + "/" + Build.MODEL;
}
if (acceptLanguage == null) {
Locale locale = Locale.getDefault();
acceptLanguage = locale.getLanguage() + "_" + locale.getCountry();
}
// Cookie handling
PersistentCookieStore cookieStore = new PersistentCookieStore(context);
CookieManager cookieManager = new CookieManager(cookieStore, CookiePolicy.ACCEPT_ALL);
// Runtime configuration
return okHttpClient.newBuilder()
.cookieJar(new JavaNetCookieJar(cookieManager))
.addNetworkInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request original = chain.request();
return chain.proceed(original.newBuilder()
.header("User-Agent", userAgent)
.header("Accept-Language", acceptLanguage)
.method(original.method(), original.body())
.build());
}
})
.build();
}
}

View File

@@ -0,0 +1,175 @@
package com.sismics.docs.util;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.preference.PreferenceManager;
import com.sismics.docs.resource.cookie.PersistentCookieStore;
import org.json.JSONObject;
import java.net.HttpCookie;
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<HttpCookie> cookieList = cookieStore.getCookies();
for (HttpCookie cookie : cookieList) {
if (cookie.getName().equals("auth_token")) {
return cookie.getValue();
}
}
return null;
}
/**
* Returns cleaned server URL.
*
* @param context Context
* @return Server URL
*/
public static String getServerUrl(Context context) {
String serverUrl = getStringPreference(context, PREF_SERVER_URL);
if (serverUrl == null) {
return null;
}
// Trim
serverUrl = serverUrl.trim();
if (!serverUrl.startsWith("http")) {
// Try to add http
serverUrl = "http://" + serverUrl;
}
if (serverUrl.endsWith("/")) {
// Delete last /
serverUrl = serverUrl.substring(0, serverUrl.length() - 1);
}
// Remove /api
if (serverUrl.endsWith("/api")) {
serverUrl = serverUrl.substring(0, serverUrl.length() - 4);
}
return serverUrl;
}
}

View File

@@ -0,0 +1,166 @@
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 creator criteria.
*
* @param creator Creator criteria
* @return The builder
*/
public SearchQueryBuilder creator(String creator) {
if (isValid(creator)) {
query.append(SEARCH_SEPARATOR)
.append("by:")
.append(creator);
}
return this;
}
/**
* Add a language criteria.
*
* @param language Language criteria
* @return The builder
*/
public SearchQueryBuilder language(String language) {
if (isValid(language)) {
query.append(SEARCH_SEPARATOR)
.append("lang:")
.append(language);
}
return this;
}
/**
* Add a shared criteria.
*
* @param shared Shared criteria
* @return The builder
*/
public SearchQueryBuilder shared(boolean shared) {
if (shared) {
query.append(SEARCH_SEPARATOR).append("shared:yes");
}
return this;
}
/**
* Add a tag criteria.
*
* @param tag Tag criteria
* @return The builder
*/
public SearchQueryBuilder tag(String tag) {
query.append(SEARCH_SEPARATOR)
.append("tag:")
.append(tag);
return this;
}
/**
* Add a before date criteria.
*
* @param before Before date criteria
* @return The builder
*/
public SearchQueryBuilder before(Date before) {
if (before != null) {
query.append(SEARCH_SEPARATOR)
.append("before:")
.append(DATE_FORMAT.format(before));
}
return this;
}
/**
* Add an after date criteria.
*
* @param after After date criteria
* @return The builder
*/
public SearchQueryBuilder after(Date after) {
if (after != null) {
query.append(SEARCH_SEPARATOR)
.append("after:")
.append(DATE_FORMAT.format(after));
}
return this;
}
/**
* Build the query.
*
* @return The query
*/
public String build() {
return query.toString();
}
/**
* Return true if the search criteria is valid.
*
* @param criteria Search criteria
* @return True if the search criteria is valid
*/
private boolean isValid(String criteria) {
return criteria != null && !criteria.trim().isEmpty();
}
}

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 B

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