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

579 Commits
v1.0 ... v1.5

Author SHA1 Message Date
Benjamin Gamard
ea210d6aac v1.5 2018-03-01 14:18:23 +01:00
Benjamin Gamard
f70cded634 file importer 2018-03-01 12:28:29 +01:00
Benjamin Gamard
cc6e1b4052 init files importer 2018-02-28 22:53:06 +01:00
Benjamin Gamard
1ce5ba4f06 #180: expain that only unread emails will be imported, fix logo display 2018-02-28 13:08:30 +01:00
Benjamin Gamard
2b23a1d048 #180: fix tests 2018-02-27 20:17:09 +01:00
Benjamin Gamard
37f262177d #180: advertise inbox scanning 2018-02-27 20:06:34 +01:00
Benjamin Gamard
7ded510625 Closes #180: IMAP inbox synching (ui) 2018-02-27 20:05:10 +01:00
Benjamin Gamard
797a987e2b refresh ui css + init inbox scanning settings 2018-02-27 19:02:23 +01:00
Benjamin Gamard
062dee987f #180: last inbox sync infos 2018-02-27 17:11:04 +01:00
Benjamin Gamard
1054931e63 #180: fix tests for travis 2018-02-27 15:34:20 +01:00
Benjamin Gamard
3720a881a4 #180: IMAP inbox synching (api) 2018-02-27 14:58:37 +01:00
Benjamin Gamard
f379b4e5ab #179: default language (ui) 2018-02-25 16:55:45 +01:00
Benjamin Gamard
9a9e86829e #179: default language (api) 2018-02-22 12:57:33 +01:00
Benjamin Gamard
66cd4abd8b #177: truncate data to fit the database 2018-02-22 11:42:41 +01:00
Benjamin Gamard
ba4470f155 Closes #177: import document from EML file (UI done) 2018-02-22 11:35:34 +01:00
Benjamin Gamard
b95ec019de #177: import document from EML file (api done) 2018-02-22 10:46:32 +01:00
Benjamin Gamard
d3a40ebca8 #177: import document from EML file (wip) 2018-02-21 21:47:57 +01:00
Benjamin Gamard
5d335049a2 fix api doc + fix translations 2018-02-15 20:38:03 +01:00
Benjamin Gamard
bd51e2ab55 fix password reset button 2018-02-14 12:32:50 +01:00
Benjamin Gamard
f8278bd44e update translations 2018-02-02 18:12:14 +01:00
Benjamin Gamard
4797cd1eca Closes #139: screenshot on README.md 2018-02-02 17:28:23 +01:00
Benjamin Gamard
706d244ff8 Closes #159: cancel routes + email at route step validation 2018-02-02 17:18:34 +01:00
Benjamin Gamard
5b8cd18128 #159: display and validate route steps 2018-02-02 12:37:56 +01:00
Benjamin Gamard
8a854bb37d #159: get routes on a document 2018-02-01 23:26:29 +01:00
Benjamin Gamard
6e6f892cb0 fix 2018-02-01 18:26:33 +01:00
Benjamin Gamard
2b4ddfa072 #159: validate route steps 2018-02-01 18:01:11 +01:00
Benjamin Gamard
c9adff5a25 #159: add temporary READ ACL for route step 2018-02-01 11:48:02 +01:00
Benjamin Gamard
503cfff82e #159: return the active step in GET /document/id 2018-01-31 22:07:38 +01:00
Benjamin Gamard
5e713f0c2a #159: start a route on a document 2018-01-29 23:34:43 +01:00
Benjamin Gamard
e035007070 #159: route model steps validation 2018-01-28 14:52:13 +01:00
Benjamin Gamard
17a94395f3 #159: route model api 2018-01-28 12:44:11 +01:00
Benjamin Gamard
0ab6c8e4b0 #159: workflow steps ui 2018-01-28 12:24:40 +01:00
Benjamin Gamard
8284169923 #159: workflow steps ui 2018-01-27 23:41:57 +01:00
Benjamin Gamard
5f9094c540 #159: workflow ui init 2018-01-27 17:33:46 +01:00
Benjamin Gamard
332ac9c109 #159: workflow db model 2018-01-26 11:26:34 +01:00
Benjamin Gamard
9ba49f35ff enable cors 2018-01-24 15:39:27 +01:00
Benjamin Gamard
d0646f12e6 translations 2018-01-08 12:02:01 +01:00
Benjamin Gamard
a8817c75b0 Update README.md 2018-01-08 11:51:15 +01:00
Benjamin Gamard
7fc50f2629 Closes #164: admin can send a password reset email to users 2018-01-07 19:56:11 +01:00
Benjamin Gamard
b2d9684738 russian translation 2018-01-06 11:50:03 +01:00
Benjamin Gamard
8ab284ff98 russian translation 2018-01-06 11:44:44 +01:00
Benjamin Gamard
a099d29524 Merge remote-tracking branch 'origin/master' 2018-01-04 16:18:24 +01:00
Benjamin Gamard
cf44af0065 Closes #169 feedback for username/group name already taken 2018-01-04 16:18:06 +01:00
Benjamin Gamard
b5bf8b6545 info about Sismics Docs Cloud in README.md 2018-01-04 15:23:55 +01:00
Benjamin Gamard
4cc3fa4d89 fix zh_TW translation 2018-01-02 19:25:41 +01:00
Benjamin Gamard
ed50e202d7 zh_CN/zh_TW translations 2018-01-02 19:24:32 +01:00
Benjamin Gamard
6d865af15a Closes #173: fix pagination default page size 2018-01-01 23:24:24 +01:00
Benjamin Gamard
66d331ddb8 env var for admin password expected hashed 2018-01-01 17:14:12 +01:00
Benjamin Gamard
0903c03a29 new android icon 2017-12-21 12:51:47 +01:00
Benjamin Gamard
5f546b6c6d new logo 2017-12-21 12:28:33 +01:00
Benjamin Gamard
9bb73bb35b Merge remote-tracking branch 'origin/master' 2017-12-11 10:17:19 +01:00
Benjamin Gamard
2ca5b04de2 initial admin password by env variable 2017-12-11 10:17:08 +01:00
bgamard
e883c1e678 Closes #171: strip alpha channel from png 2017-11-23 15:40:53 +01:00
bgamard
4fc434a222 Merge remote-tracking branch 'origin/master' 2017-11-23 15:32:31 +01:00
bgamard
ecfa747a2c Closes #172: smart images caching 2017-11-23 15:32:20 +01:00
Benjamin Gamard
dc28ebfa50 fix file modal + fix file link in audit log + high quality thumbs 2017-11-23 01:16:54 +01:00
Benjamin Gamard
6596eba6ca advertise demo app 2017-11-21 23:56:32 +01:00
Benjamin Gamard
7194f9aac0 update fr translation 2017-11-21 20:23:35 +01:00
Benjamin Gamard
e4fe1cfa90 fix active user count 2017-11-21 19:37:29 +01:00
Benjamin Gamard
5bc73548b3 Closes #162: feedback box 2017-11-21 12:01:53 +01:00
Benjamin Gamard
2156848e4a GET /app returns document count 2017-11-21 09:49:33 +01:00
Benjamin Gamard
3f807b3e51 edit -> save 2017-11-20 21:25:52 +01:00
Benjamin Gamard
d786862a60 Closes #167: disable users 2017-11-20 21:21:50 +01:00
Benjamin Gamard
fb75bafe96 Closes #166: global quota 2017-11-20 20:34:29 +01:00
Benjamin Gamard
66f781b716 cleanup logs for Travis 2017-11-18 20:14:50 +01:00
Benjamin Gamard
b3dc409926 cleanup logs for Travis + new process for each test 2017-11-18 20:01:11 +01:00
Benjamin Gamard
287ed06b6a prevent lastpass autofill in non editable fields 2017-11-18 19:45:08 +01:00
Benjamin Gamard
df1d013b1c Closes #165: smtp hostname/port/username/password configurables with env 2017-11-18 19:34:13 +01:00
Benjamin Gamard
fdb95484c1 fix sending email to an unauthenticated smtp server 2017-11-17 23:47:53 +01:00
Benjamin Gamard
4cf1f29e0a Closes #161: password recovery by email 2017-11-17 23:17:05 +01:00
Benjamin Gamard
332fd9d1f6 fix tests 2017-11-17 22:26:20 +01:00
Benjamin Gamard
039d881a07 #161: password recovery by email (wip, server part done) 2017-11-17 22:03:54 +01:00
bgamard
b8176a9fe9 fix tests 2017-11-17 17:10:05 +01:00
bgamard
65937d6f4c #161: password recovery by email (wip) 2017-11-17 17:01:08 +01:00
bgamard
590bf74e98 display two-factor authentication activation in admin area 2017-11-17 15:18:16 +01:00
bgamard
642a3e10ce fix for mobile 2017-11-17 14:05:19 +01:00
Benjamin Gamard
52a8cf92c8 fix homepage dom 2017-11-15 23:07:34 +01:00
bgamard
fc68ee56d5 fix en translation 2017-11-15 10:49:47 +01:00
Benjamin Gamard
644f4803df Merge remote-tracking branch 'origin/master' 2017-11-15 00:19:36 +01:00
Benjamin Gamard
eda6106de8 upgrade android build 2017-11-15 00:19:12 +01:00
bgamard
3a1691066e Closes #155: localize share app 2017-11-14 15:44:40 +01:00
Benjamin Gamard
b02039bad4 Fix zh_CN translation 2017-11-14 01:14:29 +01:00
Benjamin Gamard
6527a9e9bb Closes #156: Localize Android app 2017-11-14 01:07:16 +01:00
Benjamin Gamard
c59ad4d446 fix pagination 2017-11-13 22:50:21 +01:00
Benjamin Gamard
f475cbc5d8 oops 2017-11-13 22:44:06 +01:00
Benjamin Gamard
00452cc505 Closes #158: advanced search form 2017-11-13 22:37:03 +01:00
Benjamin Gamard
23660961bd #158: advanced search form (wip) 2017-11-13 18:11:54 +01:00
Benjamin Gamard
742ff183bf #158: advanced search form (wip) 2017-11-12 23:24:10 +01:00
Benjamin Gamard
dca8c28b84 Closes #157: Deskew before OCR 2017-11-12 14:49:52 +01:00
Benjamin Gamard
46079393d5 Fix file modal routing 2017-11-12 02:18:02 +01:00
Benjamin Gamard
517e4a4507 Closes #150: Display file name in audit log 2017-11-12 02:06:41 +01:00
Benjamin Gamard
6f3ae6da9d Better thumbnails UI 2017-11-11 23:25:52 +01:00
Benjamin Gamard
273136ab23 Closes #152 closes #154: localize date and time format 2017-11-10 23:43:35 +01:00
Benjamin Gamard
e74f86e118 Closes #153: fix missing localization string 2017-11-10 22:56:42 +01:00
Benjamin Gamard
84d4d3b165 Closes #151: upgrade JS libraries 2017-11-10 22:00:34 +01:00
bgamard
c355cb8bd5 Closes #148: force Qihoo 360 to use webkit rendering 2017-11-09 14:43:37 +01:00
bgamard
2957034286 Closes #147: fix IE file upload 2017-11-09 14:39:25 +01:00
bgamard
36b4fbd303 ie fix 2017-11-09 13:36:41 +01:00
bgamard
f57cf46313 Closes #146: no cache 2017-11-09 13:36:10 +01:00
Benjamin Gamard
244ddc7ce2 Closes #141: Never close full file content in memory 2017-11-06 16:45:47 +01:00
Benjamin Gamard
4d161aea07 Closes #117: fix templates minification 2017-11-06 00:48:55 +01:00
Benjamin Gamard
cf9101d157 Closes #143: Select the default language for new documents from browser language 2017-11-05 22:28:23 +01:00
Benjamin Gamard
614c8a1d13 log to stdout 2017-11-05 21:27:54 +01:00
Benjamin Gamard
879ab7951d fix build 2017-11-05 17:30:45 +01:00
Benjamin Gamard
311b42ad25 fix flash on untranslated content 2017-11-05 16:59:04 +01:00
Benjamin Gamard
0ebbbac9a6 optimize docker image 2017-11-04 20:50:57 +01:00
Benjamin Gamard
d2f9fcdda0 zh_CN translation + footer fix 2017-11-04 20:39:39 +01:00
bgamard
cfe5690a73 Closes #142: design cleanup 2017-11-03 14:53:09 +01:00
bgamard
a055b3ff5c #117: more logs + possible fix 2017-11-03 11:13:50 +01:00
bgamard
18f37ec2a8 Closes #131: validate only dirty forms 2017-11-03 11:05:04 +01:00
Benjamin Gamard
14b4e5aeec design refresh 2017-11-03 00:10:17 +01:00
Benjamin Gamard
a980930e69 Closes #137: upload files without drag & drop 2017-11-02 23:36:38 +01:00
Benjamin Gamard
1b4eb70d8d Closes #136 #138: ui fixes 2017-11-02 23:14:00 +01:00
Benjamin Gamard
1856ccc3aa less padding 2017-11-02 21:16:47 +01:00
Benjamin Gamard
3217c67ff6 flat design 2017-11-02 21:00:32 +01:00
bgamard
54d5f1cb1b #111: french translation 2017-11-02 17:14:34 +01:00
bgamard
e49d002941 #111: translate templates 2017-11-02 15:39:50 +01:00
Benjamin Gamard
4822b8bf23 Update README.md 2017-11-01 19:50:42 +01:00
Benjamin Gamard
198a6d5665 #111: translate templates (wip) 2017-11-01 19:48:50 +01:00
Benjamin Gamard
c7b9ec3a4c #111: translate controllers 2017-11-01 14:34:15 +01:00
Benjamin Gamard
f46e10e11c support more languages 2017-10-31 21:20:22 +01:00
Benjamin Gamard
b9acc4ecf8 support more languages 2017-10-31 21:16:46 +01:00
Benjamin Gamard
403d094a3d support more languages 2017-10-31 21:06:12 +01:00
Benjamin Gamard
3b1f11e5a8 Merge branch 'master' into sismics_prod 2017-10-31 21:01:32 +01:00
Benjamin Gamard
ddba06cca3 support more languages 2017-10-31 21:01:23 +01:00
Benjamin Gamard
bf89af0da9 Merge branch 'master' into sismics_prod 2017-10-31 20:39:10 +01:00
Benjamin Gamard
5a30164848 support more languages 2017-10-31 20:36:55 +01:00
Benjamin Gamard
0c4e200900 support more languages 2017-10-31 20:34:54 +01:00
Benjamin Gamard
fe8e8f041c Merge remote-tracking branch 'origin/sismics_prod' into sismics_prod 2017-07-31 14:49:17 +02:00
Benjamin Gamard
43084d9d86 Merge branch 'master' into sismics_prod 2017-07-31 14:48:59 +02:00
Benjamin Gamard
e1e1b4e278 embed free monospaced font 2017-07-31 14:40:35 +02:00
Benjamin Gamard
14f8239ba3 register fonts 2017-07-31 14:09:56 +02:00
Benjamin Gamard
e660a70d00 travis: install microsoft fonts 2017-07-31 13:52:32 +02:00
Benjamin Gamard
119d30bb16 fix plain text file save 2017-07-31 12:08:42 +02:00
Benjamin Gamard
5686de56e2 Merge branch 'master' into sismics_prod 2017-07-31 01:51:39 +02:00
Benjamin Gamard
e0214a6a9f Closes #118: create pdf from text plain files 2017-07-31 01:51:23 +02:00
Benjamin Gamard
330de495db #118: extract text content from text plain files (WIP) 2017-06-11 11:33:30 +02:00
jendib
dcc7fe55f4 Closes #125: Confirmation before deleting a comment 2017-05-07 01:39:20 +02:00
jendib
3274b4c79a Closes #130: Fix document language icon 2017-05-07 01:34:21 +02:00
jendib
cbfa4b1c41 Closes #127: Edit -> Save 2017-05-07 01:32:55 +02:00
jendib
5f7d2f2a68 Closes #129: bigger checkbox 2017-05-07 01:31:59 +02:00
jendib
e38bdbe508 Closes #128: Delete cursor on comment delete button 2017-05-07 01:27:24 +02:00
jendib
c352b94b38 Closes #126: click to copy 2017-05-07 01:25:20 +02:00
Benjamin Gamard
6b0106e385 update readme with docker instructions 2017-04-25 11:09:18 +02:00
Benjamin Gamard
60021e5123 build prod package before pushing to dockerhub 2017-04-25 10:39:58 +02:00
Benjamin Gamard
76d3157247 travis push to dockerhub 2017-04-25 10:26:48 +02:00
Benjamin Gamard
7c24778460 Merge branch 'master' into sismics_prod 2017-03-21 09:14:19 +01:00
Jean-Marc Tremeaux
8231db4a5a Fix dockerfile 2017-03-21 08:56:00 +01:00
jendib
fe5dd5e8dc Merge branch 'master' of https://github.com/sismics/docs 2017-01-24 22:00:35 +01:00
jendib
5872928812 Hide filename if not available + upgrade Gradle 2017-01-24 22:00:24 +01:00
Benjamin Gamard
8a8f4bb388 Merge pull request #121 from sismics/master
Update Jetty version
2017-01-03 11:43:54 +01:00
Benjamin Gamard
0e8d5fd5ec Update Jetty version 2017-01-03 11:43:31 +01:00
Benjamin Gamard
b9344149b0 Merge pull request #120 from sismics/master
Update base image
2017-01-03 11:11:44 +01:00
Benjamin Gamard
5053a6852f Update base image 2017-01-03 11:11:12 +01:00
jendib
bb3faca533 Closes #119: Keep and display original file name 2016-12-07 01:28:52 +01:00
jendib
4f7fcbfdf0 Merge branch 'master' into sismics_prod 2016-12-05 19:30:15 +01:00
jendib
87c1cc88be #116: Allow all file types 2016-12-05 19:25:52 +01:00
jendib
1d78551f4c Fix tests, add logs for #117 2016-11-20 18:52:47 +01:00
jendib
b36d08db8e Closes #116: Allow all file types 2016-11-20 18:41:42 +01:00
Benjamin Gamard
c99b1a1867 cleanup private scripts 2016-10-24 16:55:56 +02:00
Benjamin Gamard
fc4380e5cc Merge pull request #115 from sismics/master
Edit production domain
2016-10-06 15:35:11 +02:00
Benjamin Gamard
850ed7f76b Edit production domain 2016-10-06 15:34:38 +02:00
jendib
0f6aa3befb Concatenate Angular templates in minified JS file 2016-08-31 19:34:37 +02:00
jendib
ddd976162c Merge branch 'master' into sismics_prod 2016-08-26 22:00:58 +02:00
jendib
cdd19e182b Closes #113: Fire async events after request transaction commit 2016-08-26 21:22:27 +02:00
jendib
afc22a547e Closes #112: Don't update auth token on each request 2016-08-26 20:34:23 +02:00
jendib
79ca54c5af Android: Ask permission to write files 2016-07-30 02:52:53 +02:00
jendib
0aacf20c16 Android: upgrade to Nougat 2016-07-09 19:35:05 +02:00
jendib
cdfb43dbd8 Cleanup Lucene DAO 2016-06-28 23:31:59 +02:00
jendib
35ec8b951c Build fails if grunt fails 2016-06-16 22:15:54 +02:00
Benjamin Gamard
05bfaa0035 Merge pull request #110 from sismics/master
Push to production
2016-06-16 21:38:14 +02:00
jendib
f5705b1153 Minor UI tweaks 2016-06-16 20:31:39 +02:00
jendib
ed1353a4eb Android: upgrade build tools 2016-06-16 20:13:34 +02:00
jendib
a79922d7c9 Android: upgrade okhttp (fix for Android 6) 2016-06-06 20:51:05 +02:00
jendib
7a7cbd570c Closes #85: UI for login as guest 2016-05-29 18:34:51 +02:00
jendib
d7865cfaf0 #85: Login as guest 2016-05-29 16:37:26 +02:00
jendib
ead01ce1d0 #85: Guest login configuration 2016-05-28 23:09:52 +02:00
jendib
8aca012c99 Reduce log verbosity 2016-05-16 21:21:19 +02:00
jendib
67a4dc63ca Closes #106: Header base authentication 2016-05-16 21:07:01 +02:00
jendib
ce0678784b #81: Android: Display dublincore metadata 2016-05-15 19:50:12 +02:00
jendib
cbc4bbb818 API documentation introduction 2016-05-14 23:10:29 +02:00
jendib
1c558a884d Closes #105: Upgrade grunt dependencies 2016-05-14 14:21:00 +02:00
jendib
d84d1428b2 Android: Better layout for read-only documents 2016-05-14 02:05:03 +02:00
Benjamin Gamard
b870ee8d62 Merge pull request #104 from sismics/master
Push to production
2016-05-13 00:46:18 +02:00
jendib
ef18581e71 #103: API documentation for /document 2016-05-13 00:45:08 +02:00
jendib
177bbceaf4 #103: API documentation for all resources except /document 2016-05-12 01:26:02 +02:00
jendib
a13174ac4d Android: use GET /tag/list instead of /tag/stats 2016-05-11 00:55:27 +02:00
jendib
e181b7d24b #103: API documentation for /user and /vocabulary 2016-05-10 23:30:28 +02:00
jendib
e631aa0e8a Closes #101: Allow export buttons for read-only documents 2016-05-10 21:18:16 +02:00
jendib
575ad75a0a Closes #102: Android: Clear all auth tokens on logout 2016-05-10 20:00:14 +02:00
jendib
394f667ab0 Prepare 1.5 development cycle 2016-05-09 22:52:34 +02:00
jendib
c695572b28 Release 1.4 2016-05-09 22:25:12 +02:00
Benjamin Gamard
f80cf43ae8 Merge pull request #100 from sismics/master
Push to production
2016-05-09 22:10:49 +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
Benjamin Gamard
6b57d29f51 Merge pull request #96 from sismics/master
Push to production
2016-05-08 23:40:50 +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
Benjamin Gamard
f2c4dde56e Merge pull request #95 from sismics/master
Fix batch for ACLs on tags
2016-05-08 23:21:36 +02:00
jendib
6f89a50fe5 Fix batch for ACLs on tags 2016-05-08 23:20:58 +02:00
Benjamin Gamard
3a22132363 Merge pull request #94 from sismics/master
Push to production
2016-05-08 23:07:43 +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
Benjamin Gamard
767099b7ea Merge pull request #89 from sismics/master
Push to production
2016-03-24 00:06:52 +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
Benjamin Gamard
6aef7246a0 Merge pull request #86 from sismics/master
Push to production
2016-03-21 01:01:14 +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
1f6d9f0211 Merge pull request #76 from sismics/master
Push to production
2016-03-02 00:43:37 +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
Benjamin Gamard
3248637e8c Merge pull request #64 from sismics/master
Push to production
2016-02-11 23:41:47 +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
Benjamin Gamard
bf8e0827e2 Merge pull request #60 from sismics/master
Push to production
2016-01-24 16:30:50 +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
Benjamin Gamard
ad1e57316f Merge pull request #56 from sismics/master
Push to production
2016-01-01 01:58:32 +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
Benjamin Gamard
737b3299ff Merge pull request #54 from sismics/master
Push to production
2015-12-11 22:22:44 +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
Benjamin Gamard
1934bb71f0 Merge pull request #52 from sismics/master
Push to production
2015-12-01 01:17:36 +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
Benjamin Gamard
b3ef9b0476 Merge pull request #50 from sismics/master
Push to production
2015-11-30 00:29:31 +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
Benjamin Gamard
8477920475 Merge pull request #47 from sismics/master
Push to production
2015-11-30 00:10:26 +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
Benjamin Gamard
f98a12b96f Merge pull request #46 from sismics/master
Push to production
2015-11-21 20:32:16 +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
Benjamin Gamard
bee8a4fcdc Merge pull request #43 from sismics/master
Push to production
2015-11-18 23:11:02 +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
Benjamin Gamard
f984595b97 Merge pull request #40 from sismics/master
Push to production
2015-11-02 23:55:16 +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
Benjamin Gamard
f01d78a9ea Merge pull request #39 from sismics/master
Push to production
2015-11-02 21:36:14 +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
Benjamin Gamard
97f25de0dc Merge pull request #31 from sismics/master
Push to production
2015-09-08 21:50:12 +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
Benjamin Gamard
3d1b5a7394 Merge pull request #28 from sismics/master
#4: Upgrade to unrelease PDFBox 2
2015-09-05 23:12:29 +02:00
jendib
e72fe3683c #4: Upgrade to unrelease PDFBox 2 2015-09-05 23:12:01 +02:00
Benjamin Gamard
df1eaf54c8 Merge pull request #27 from sismics/master
Push to production
2015-09-05 21:41:43 +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
Benjamin Gamard
3851408100 Merge pull request #25 from sismics/master
Push to production
2015-08-28 01:16:33 +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
Benjamin Gamard
794c5012ad Merge pull request #19 from sismics/master
Fix post-ACL system
2015-05-10 14:03:44 +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
Benjamin Gamard
1ba9f7d7d9 Merge pull request #17 from sismics/master
#13: Fix performance issue
2015-05-09 18:00:33 +02:00
jendib
42320dc9b9 #13: Fix performance issue 2015-05-09 18:00:03 +02:00
Benjamin Gamard
9b4330d618 Merge pull request #16 from sismics/master
#13: Disable shared status in GET /document/list (too slow)
2015-05-09 17:32:04 +02:00
jendib
ff994ce63b #13: Disable shared status in GET /document/list (too slow) 2015-05-09 16:48:01 +02:00
Benjamin Gamard
3a32b742e8 Merge pull request #15 from sismics/master
ACL system
2015-05-09 16:37:13 +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
Benjamin Gamard
87b3d25c0f Merge pull request #12 from sismics/master
Push to production
2015-05-02 17:19:30 +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
Benjamin Gamard
9f28649a3a Merge pull request #9 from sismics/master
Upload drag & dropped files sequentially
2015-03-29 16:07:07 +02:00
jendib
1c4161981b Upload drag & dropped files sequentially 2015-03-29 16:05:42 +02:00
Benjamin Gamard
8f9df8961b Merge pull request #8 from sismics/master
Attach orphan files to a new document
2015-03-28 18:05:01 +01: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
Benjamin Gamard
9e9217bfcb Merge pull request #7 from sismics/master
Drag & drop
2015-03-27 23:11:33 +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
Benjamin Gamard
ffdd5a3631 hook me 2015-03-23 17:30:15 +01:00
Benjamin Gamard
db4bf8d35a Merge pull request #5 from sismics/master
chmod +x
2015-03-17 00:03:00 +01:00
Jean-Marc Tremeaux
bad42d96f3 chmod +x 2015-03-13 15:58:32 +01:00
Jean-Marc Tremeaux
7b2859f96e Deploy script 2015-03-11 22:56:56 +01:00
Jean-Marc Tremeaux
03c5c33ea7 Deploy script 2015-03-11 22:54:42 +01:00
Jean-Marc Tremeaux
6ff1570b3c Deploy script 2015-03-11 22:51:24 +01:00
Jean-Marc Tremeaux
24345bc176 Deploy script 2015-03-11 22:37:35 +01:00
Walter
75cde7e9d6 Docker: Using sismics/data for data image 2015-03-11 14:31:10 +01:00
Walter
192c2030d3 Dockerization + Fix for Tesseract 3.03 2015-03-11 00:35:42 +01:00
jendib
18cedaef2c Orphan files are linked to a specific user 2015-03-06 22:40:33 +01:00
jendib
d0c259ead2 List orphan files 2015-03-06 21:23:50 +01:00
jendib
2347483676 Order of files attached to document 2015-03-06 21:13:09 +01:00
jendib
6c976087de Missing file + TODO 2015-03-03 00:26:40 +01:00
jendib
c36014b46f Ability to upload files without document (no OCR, no Lucene)
+ New resource to attach a document to a file and OCR/Lucene it
2015-03-03 00:23:30 +01:00
jendib
5cf0532db7 Page size selector 2015-03-02 00:07:42 +01:00
jendib
6edae27d26 Fixes #3: Race condition on settings screen 2015-01-25 19:20:49 +01:00
jendib
9ae8303b18 Android: sort tags by count desc 2015-01-15 01:57:29 +01:00
jendib
a0356845b1 Android: event listeners on main thread, files UI polish 2015-01-15 01:49:11 +01:00
jendib
6fa0b8494e Android: file delete 2015-01-14 23:31:53 +01:00
jendib
c9210c39c4 Android: file upload 2015-01-11 01:53:40 +01:00
jendib
790453047d Android: file upload background service 2015-01-08 00:34:57 +01:00
jendib
5befef2992 Android: Document deleting 2014-12-13 16:27:27 +01:00
jendib
dd59172a19 Android: Document form validation 2014-12-11 22:29:04 +01:00
jendib
17e5c65d04 Android: Document adding/editing 2014-12-11 22:13:06 +01:00
jendib
a762ce4715 Android: Update view after an editing 2014-12-05 00:12:42 +01:00
jendib
89d66eca4a Android: Document edit activity filling 2014-12-04 22:32:53 +01:00
jendib
323b95ad7a Android: language selection 2014-12-04 03:41:35 +01:00
jendib
b42b195245 Android: tags autocompletion 2014-12-04 02:28:44 +01:00
jendib
a7987386e1 Android: tags autocompletion (in progress) 2014-12-03 00:28:26 +01:00
jendib
a181eac9a5 Android: settings activity 2014-12-01 22:20:23 +01:00
jendib
5662c080d6 Merge branch 'master' of https://github.com/sismics/docs.git 2014-12-01 01:21:48 +01:00
jendib
745766a2c3 Fix error handling on DELETE /share/id 2014-12-01 01:21:38 +01:00
Benjamin Gamard
f44d30d890 Update README.md 2014-11-30 23:13:08 +01:00
Benjamin Gamard
ea0c7982ac Update README.md 2014-11-30 23:12:28 +01:00
jendib
2abf0c6eab Android: drawer background 2014-11-30 22:49:35 +01:00
jendib
13e8b828ac Android: filtering intents with an URL to Docs is not possible
The Android platform forces to specify a full hostname which is variable in our case
2014-11-30 22:02:07 +01:00
jendib
551c10e7a3 Android: file upload (in progress), title marquee 2014-11-30 16:11:35 +01:00
jendib
e17abfe411 Android: share editing 2014-11-29 23:50:56 +01:00
jendib
3330acfc75 Android: tags caching 2014-11-28 01:40:54 +01:00
jendib
2837b21a86 Android: init document edit activity 2014-11-26 23:30:25 +01:00
jendib
5666d9b8b5 Android: floating action button 2014-11-26 02:08:20 +01:00
jendib
824e37b8ea Android: swipe refresh on documents list 2014-11-26 01:01:36 +01:00
jendib
1d08508e51 Android: empty lists and loading error feedback 2014-11-25 23:53:52 +01:00
jendib
a84748f075 Android: better flags, tags loading feedback 2014-11-25 00:09:12 +01:00
jendib
a6c123ad03 Android: display shared status 2014-11-24 20:07:06 +01:00
jendib
407564f28c Android: show all documents from drawer 2014-11-23 22:10:24 +01:00
jendib
6a9a166670 Android: tags in drawer 2014-11-23 19:55:08 +01:00
jendib
1773998ca0 Android: Searching 2014-11-23 00:49:56 +01:00
jendib
c610364ef7 Android: Infinite scrolling 2014-11-22 22:43:31 +01:00
jendib
bc719b3165 Android: File(s) download 2014-11-22 22:18:21 +01:00
jendib
92b2219bed Android: Display tags, language and shared status on document 2014-11-22 14:06:08 +01:00
jendib
8c5c54125f Android: document details 2014-11-22 01:09:12 +01:00
jendib
2ce5749226 Android: documents listing: tags and date 2014-11-21 03:18:20 +01:00
jendib
76d195a344 Android: documents listing from API 2014-11-20 02:11:18 +01:00
jendib
04752cab0c Android: material design, API 21 2014-11-19 01:18:42 +01:00
jendib
ffa7d796b5 Upgrade Android build tools 2014-05-27 16:53:15 +02:00
jendib
5119d32714 Init login 2014-05-26 22:22:13 +02:00
jendib
c0a963e845 Button to clear the search field, bug fix on share modal 2014-05-12 21:39:20 +02:00
jendib
20c2b0460b TODO 2014-05-11 15:05:06 +02:00
jendib
2ee979c012 TODO 2014-05-11 15:02:02 +02:00
jendib
bc3683990f Merge branch 'master' of https://github.com/sismics/docs 2014-05-05 23:14:02 +02:00
jendib
c06e2971bc AS/Gradle upgrade 2014-05-05 23:13:23 +02:00
Benjamin Gamard
b3aeac8bf4 Update README.md 2014-02-23 14:30:54 +01:00
jendib
34e3ac5478 Download all files from a document as ZIP 2014-02-23 14:09:41 +01:00
jendib
ae566018d6 Gruntification 2014-02-23 02:28:44 +01:00
jendib
12c3c4750f Print a file 2014-02-02 01:19:17 +01:00
jendib
42f23ed0d7 Git ignore 2014-01-28 14:41:04 +01:00
jendib
d76b8e32c8 File upload progress 2014-01-19 18:46:07 +01:00
jendib
6aaecb473f New logo 2014-01-13 20:05:17 +01:00
jendib
03976160bf Migration of /share.html 2014-01-11 21:39:26 +01:00
Benjamin Gamard
438a38985a Merge pull request #1 from sismics/bs3
Merge BS3 branch
2014-01-11 11:40:03 -08:00
jendib
9802beaf7b Migration of /tags and /settings views 2014-01-11 20:39:00 +01:00
jendib
6963fd9770 Migration of /document views 2014-01-11 16:38:58 +01:00
jendib
85aa16afba Angular UI Bootstrap migration 2014-01-11 00:56:36 +01:00
jendib
0e99f06310 Upgrade libs 2014-01-10 23:54:12 +01:00
Ben
60beb0e2f5 Init Android project 2013-12-27 13:27:58 +01:00
jendib
77f0368ba5 Hardwire TIFFImageReaderSpi to avoid registering bug (again) 2013-11-07 01:12:11 +01:00
jendib
596dd4db13 Merge branch 'master' of https://github.com/sismics/docs.git 2013-10-01 22:24:23 +02:00
jendib
d2ba291287 Change package for tess4j 2013-10-01 22:24:12 +02:00
Benjamin Gamard
01cb3e611c Update README.md 2013-09-23 13:36:58 +02:00
jendib
bd9e918a62 Merge branch 'master' of https://github.com/sismics/docs.git 2013-09-07 00:27:22 +02:00
jendib
56f6038c09 Vertical scroll on file preview for small screens 2013-09-07 00:27:13 +02:00
Benjamin Gamard
f29ed3e671 Update README.md 2013-09-06 11:18:30 +02:00
jendib
726121d8c8 Fix Hibernate entity 2013-09-05 21:43:45 +02:00
jendib
c40e7e1cc9 File upload feedback in title and form state, new favicon 2013-09-05 19:14:21 +02:00
jendib
b1f9b072f3 TODO 2013-09-05 17:41:33 +02:00
jendib
22cea20a90 Hard coupling between tess4j and imageIO to avoid service registering 2013-09-05 16:10:26 +02:00
jendib
8eb5b8066e Merge branch 'master' of https://github.com/sismics/docs.git 2013-09-05 11:35:00 +02:00
jendib
e37eab3b7a All users can access logs 2013-09-05 11:34:53 +02:00
jendib
1db54174d4 Remove TODO 2013-09-05 00:50:23 +02:00
jendib
d5fa3a4e3a TODO 2013-09-04 18:04:31 +02:00
jendib
fc53758eb7 Typo, TODO 2013-09-03 18:03:44 +02:00
jendib
f5079e83cb Typo 2013-09-03 09:19:47 +02:00
jendib
b399e4081f Typo 2013-09-02 23:28:38 +02:00
jendib
a80bc27582 TODO 2013-09-02 22:25:58 +02:00
jendib
eb57af4029 Fix multi-platform jUnit 2013-08-26 22:35:30 +02:00
jendib
ac3580fb4a Fix leak 2013-08-26 22:03:14 +02:00
jendib
6e3d2ea972 Show file count on documents list (client) 2013-08-22 22:24:14 +02:00
jendib
870a44da0d Return file count on GET /document/list 2013-08-22 17:59:24 +02:00
jendib
62a5840777 Alert in navigation bar if there is a new error in logs 2013-08-22 17:41:47 +02:00
jendib
a858699391 Git ignore 2013-08-22 14:44:58 +02:00
jendib
fb0aec5fee Fix document scope reset after add, fix wrong web.xml configuration
(it broke TIFF image encoding on OCR)
2013-08-22 01:44:51 +02:00
jendib
1a495ac948 Fix jUnit on PDF rendering 2013-08-21 01:30:37 +02:00
jendib
ba083fb57a Disable verbose logging 2013-08-21 01:15:52 +02:00
jendib
4e22111f38 Fix default thumbnail 2013-08-21 01:05:59 +02:00
jendib
db7a9f0e4a Encrypt stored files in SHA 256 2013-08-20 21:51:07 +02:00
jendib
906de329ae DB update script 6 2013-08-20 18:55:49 +02:00
jendib
00b00f0d0c File encryption utilities 2013-08-20 18:06:08 +02:00
jendib
0bc658a396 More loading feedback (client) 2013-08-20 00:57:22 +02:00
jendib
464d43194b File encryption (in progress) 2013-08-19 23:57:50 +02:00
jendib
1c606ebf25 Delete files when a document is deleted, fix search by date 2013-08-19 15:27:34 +02:00
jendib
504c4dd815 Merge branch 'master' of https://github.com/sismics/docs.git 2013-08-19 01:30:27 +02:00
jendib
ecb5a7abf6 TODO 2013-08-19 01:29:59 +02:00
Benjamin Gamard
cc6d599293 Update README.md 2013-08-18 16:31:41 +02:00
687 changed files with 148566 additions and 74598 deletions

8
.gitignore vendored
View File

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

24
.travis.yml Normal file
View File

@@ -0,0 +1,24 @@
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-ita tesseract-ocr-kor tesseract-ocr-rus tesseract-ocr-ukr tesseract-ocr-spa tesseract-ocr-ara tesseract-ocr-hin tesseract-ocr-deu tesseract-ocr-pol tesseract-ocr-jpn tesseract-ocr-por tesseract-ocr-tha tesseract-ocr-jpn tesseract-ocr-chi-sim tesseract-ocr-chi-tra
- sudo apt-get -y -q install haveged && sudo service haveged start
after_success:
- mvn -Pprod -DskipTests clean install
- docker login -u $DOCKER_USER -p $DOCKER_PASS
- export REPO=sismics/docs
- export TAG=`if [ "$TRAVIS_BRANCH" == "master" ]; then echo "latest"; else echo $TRAVIS_BRANCH ; fi`
- docker build -f Dockerfile -t $REPO:$COMMIT .
- docker tag $REPO:$COMMIT $REPO:$TAG
- docker tag $REPO:$COMMIT $REPO:travis-$TRAVIS_BUILD_NUMBER
- docker push $REPO
env:
global:
- TESSDATA_PREFIX=/usr/share/tesseract-ocr
- LC_NUMERIC=C
- secure: LRGpjWORb0qy6VuypZjTAfA8uRHlFUMTwb77cenS9PPRBxuSnctC531asS9Xg3DqC5nsRxBBprgfCKotn5S8nBSD1ceHh84NASyzLSBft3xSMbg7f/2i7MQ+pGVwLncusBU6E/drnMFwZBleo+9M8Tf96axY5zuUp90MUTpSgt0=
- secure: bCDDR6+I7PmSkuTYZv1HF/z98ANX/SFEESUCqxVmV5Gs0zFC0vQXaPJQ2xaJNRop1HZBFMZLeMMPleb0iOs985smpvK2F6Rbop9Tu+Vyo0uKqv9tbZ7F8Nfgnv9suHKZlL84FNeUQZJX6vsFIYPEJ/r7K5P/M0PdUy++fEwxEhU=
- secure: ewXnzbkgCIHpDWtaWGMa1OYZJ/ki99zcIl4jcDPIC0eB3njX/WgfcC6i0Ke9mLqDqwXarWJ6helm22sNh+xtQiz6isfBtBX+novfRt9AANrBe3koCMUemMDy7oh5VflBaFNP0DVb8LSCnwf6dx6ZB5E9EB8knvk40quc/cXpGjY=
- COMMIT=${TRAVIS_COMMIT::8}

11
Dockerfile Normal file
View File

@@ -0,0 +1,11 @@
FROM sismics/jetty:9.2.20-jdk7
MAINTAINER b.gamard@sismics.com
RUN apt-get update && apt-get -y -q install tesseract-ocr tesseract-ocr-fra tesseract-ocr-ita tesseract-ocr-kor tesseract-ocr-rus tesseract-ocr-ukr tesseract-ocr-spa tesseract-ocr-ara tesseract-ocr-hin tesseract-ocr-deu tesseract-ocr-pol tesseract-ocr-jpn tesseract-ocr-por tesseract-ocr-tha tesseract-ocr-jpn tesseract-ocr-chi-sim tesseract-ocr-chi-tra && \
apt-get clean && rm -rf /var/lib/apt/lists/*
ENV TESSDATA_PREFIX /usr/share/tesseract-ocr
ENV LC_NUMERIC C
ADD docs.xml /opt/jetty/webapps/docs.xml
ADD docs-web/target/docs-web-*.war /opt/jetty/webapps/docs.war

View File

@@ -1,42 +1,77 @@
Sismics Docs
============
<h3 align="center">
<img src="https://www.sismicsdocs.com/img/github-title.png" alt="Sismics Docs" width=500 />
</h3>
![](http://www.sismics.com/docs/img/docs.jpg)
[![Twitter: @sismicsdocs](https://img.shields.io/badge/contact-@sismicsdocs-blue.svg?style=flat)](https://twitter.com/sismicsdocs)
[![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-blue.svg)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)
[![Build Status](https://secure.travis-ci.org/sismics/docs.png)](http://travis-ci.org/sismics/docs)
What is Docs?
---------------
Docs is an open source, lightweight document management system for individuals and businesses.
Docs is an open source, lightweight document management system.
<hr />
<h2 align="center">
✨ We just launched a Cloud version of Sismics Docs! Head to <a href="https://www.sismicsdocs.com/">sismicsdocs.com</a> for more informations ✨
</h2>
<hr />
Docs is written in Java, and may be run on any operating system with Java support.
![New!](https://www.sismicsdocs.com/img/laptop-demo.png)
Demo
----
A demo is available at [demo.sismicsdocs.com](https://demo.sismicsdocs.com)
- Guest login is enabled with read access on all documents
- "admin" login with "admin" password
- "demo" login with "password" password
Features
--------
- Responsive user interface
- Optical characted recognition
- Support image and PDF files
- Optical character recognition
- Support image, PDF, ODT and DOCX files
- Flexible search engine
- Full text search in image and PDF
- Tag system
- Multi-users
- Document sharing
- Full text search in all supported files
- All [Dublin Core](http://dublincore.org/) metadata
- Workflow system ![New!](https://www.sismics.com/public/img/new.png)
- 256-bit AES encryption of stored files
- Tag system with nesting
- Import document from email (EML format) ![New!](https://www.sismics.com/public/img/new.png)
- Automatic inbox scanning and importing ![New!](https://www.sismics.com/public/img/new.png)
- User/group permission system
- 2-factor authentication
- Hierarchical groups
- Audit log
- Comments
- Storage quota per user
- Document sharing by URL
- RESTful Web API
- Fully featured Android client
- [Mass files importer](https://github.com/sismics/docs/tree/master/docs-importer) (single or scan mode) ![New!](https://www.sismics.com/public/img/new.png)
- Tested to 100k documents
License
-------
Download
--------
Reader 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/).
The default admin password is "admin". Don't forget to change it before going to production.
Install with Docker
-------------------
From a Docker host, run this command to download and install Sismics Docs. The server will run on <http://[your-docker-host-ip]:8100>.
The default admin password is "admin". Don't forget to change it before going to production.
docker run --rm --name sismics_docs_latest -d -p 8100:8080 -v sismics_docs_latest:/data sismics/docs:latest
How to build Docs from the sources
------------------------------------
----------------------------------
Prerequisites: JDK 7, Maven 3
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
@@ -46,7 +81,7 @@ or download the sources from GitHub.
#### Launch the build
From the `docs-parent` directory:
From the root directory:
mvn clean -DskipTests install
@@ -62,4 +97,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>.

4
docs-android/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.gradle
/local.properties
/.idea
.DS_Store

1
docs-android/app/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

View File

@@ -0,0 +1,43 @@
buildscript {
repositories {
jcenter()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
}
}
apply plugin: 'com.android.application'
repositories {
jcenter()
google()
}
android {
compileSdkVersion 26
defaultConfig {
minSdkVersion 14
targetSdkVersion 26
versionCode 1
versionName '1.0'
}
lintOptions {
abortOnError false
}
}
dependencies {
compile fileTree(dir: 'libs', include: '*.jar')
compile 'com.android.support:appcompat-v7:26.1.0'
compile 'com.android.support:recyclerview-v7:26.1.0'
compile 'com.android.support:design:26.1.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.7.0'
compile 'com.squareup.okhttp3:okhttp-urlconnection:3.4.0'
compile 'com.jakewharton.picasso:picasso2-okhttp3-downloader:1.1.0'
}

Binary file not shown.

View File

@@ -0,0 +1,17 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /opt/android-studio/sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the ProGuard
# include property in project.properties.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
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="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
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=".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

@@ -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("tags");
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,853 @@
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.text.method.LinkMovementMethod;
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.SpannableUtil;
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;
/**
* 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
// Create date
TextView createdDateTextView = (TextView) findViewById(R.id.createdDateTextView);
createdDateTextView.setText(date);
// Description
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);
}
// Tags
TextView tagTextView = (TextView) findViewById(R.id.tagTextView);
if (tags.length() == 0) {
tagTextView.setVisibility(View.GONE);
} else {
tagTextView.setVisibility(View.VISIBLE);
tagTextView.setText(SpannableUtil.buildSpannableTags(tags));
}
// Language
ImageView languageImageView = (ImageView) findViewById(R.id.languageImageView);
languageImageView.setImageResource(getResources().getIdentifier(language, "drawable", getPackageName()));
// Shared status
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());
startActivity(intent);
}
});
// 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.GONE);
findViewById(R.id.actionUploadFile).setVisibility(writable ? View.VISIBLE : View.GONE);
findViewById(R.id.actionSharing).setVisibility(writable ? View.VISIBLE : View.GONE);
findViewById(R.id.actionDelete).setVisibility(writable ? View.VISIBLE : View.GONE);
// 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);
}
});
// Contributors
TextView contributorsTextView = (TextView) findViewById(R.id.contributorsTextView);
contributorsTextView.setText(SpannableUtil.buildSpannableContributors(document.optJSONArray("contributors")));
// Relations
JSONArray relations = document.optJSONArray("relations");
if (relations.length() > 0) {
TextView relationsTextView = (TextView) findViewById(R.id.relationsTextView);
relationsTextView.setMovementMethod(LinkMovementMethod.getInstance());
relationsTextView.setText(SpannableUtil.buildSpannableRelations(relations));
} else {
findViewById(R.id.relationsLayout).setVisibility(View.GONE);
}
// Additional dublincore metadata
displayDublincoreMetadata(R.id.subjectTextView, R.id.subjectLayout, "subject");
displayDublincoreMetadata(R.id.identifierTextView, R.id.identifierLayout, "identifier");
displayDublincoreMetadata(R.id.publisherTextView, R.id.publisherLayout, "publisher");
displayDublincoreMetadata(R.id.formatTextView, R.id.formatLayout, "format");
displayDublincoreMetadata(R.id.sourceTextView, R.id.sourceLayout, "source");
displayDublincoreMetadata(R.id.typeTextView, R.id.typeLayout, "type");
displayDublincoreMetadata(R.id.coverageTextView, R.id.coverageLayout, "coverage");
displayDublincoreMetadata(R.id.rightsTextView, R.id.rightsLayout, "rights");
}
});
}
/**
* Display a dublincore metadata.
*
* @param textViewId TextView ID
* @param blockViewId View ID
* @param name Name
*/
private void displayDublincoreMetadata(int textViewId, int blockViewId, String name) {
if (document == null) return;
String value = document.optString(name);
if (document.isNull(name) || value.isEmpty()) {
findViewById(blockViewId).setVisibility(View.GONE);
return;
}
findViewById(blockViewId).setVisibility(View.VISIBLE);
TextView textView = (TextView) findViewById(textViewId);
textView.setText(value);
}
@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,288 @@
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("tags")));
}
TagResource.list(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("tags")));
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
PreferenceUtil.clearAuthToken(MainActivity.this);
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.SpannableUtil;
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(SpannableUtil.buildSpannableTags(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,113 @@
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));
}
@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,132 @@
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, "", 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 (parentId.equals(tagParentId)) {
TagItem tagItem = new TagItem();
tagItem.id = tag.optString("id");
tagItem.name = tag.optString("name");
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);
// 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 String color;
private int depth;
public String getName() {
return name;
}
}
}

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("tags");
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/list.
*
* @param context Context
* @param callback Callback
*/
public static void list(Context context, HttpCallback callback) {
Request request = new Request.Builder()
.url(HttpUrl.parse(getApiUrl(context) + "/tag/list"))
.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,33 @@
package com.sismics.docs.ui.view;
import android.content.Context;
import android.util.AttributeSet;
import android.view.ViewGroup;
import android.widget.ListView;
/**
* Non-scrollable ListView.
* All items are visible from the start.
*
* @author http://stackoverflow.com/questions/18813296/non-scrollable-listview-inside-scrollview/24629341#24629341
*/
public class NonScrollListView extends ListView {
public NonScrollListView(Context context) {
super(context);
}
public NonScrollListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public NonScrollListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int heightMeasureSpec_custom = MeasureSpec.makeMeasureSpec(
Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, heightMeasureSpec_custom);
ViewGroup.LayoutParams params = getLayoutParams();
params.height = getMeasuredHeight();
}
}

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,43 @@
package com.sismics.docs.util;
import android.Manifest;
import android.app.Activity;
import android.app.DownloadManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Environment;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
/**
* 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(Activity activity, String url, String fileName, String title, String description) {
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(activity, new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, 0);
return;
}
String authToken = PreferenceUtil.getAuthToken(activity);
DownloadManager downloadManager = (DownloadManager) activity.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,186 @@
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.
*
* @param context Context
* @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;
}
/**
* Clear all auth tokens.
*
* @param context Context
*/
public static void clearAuthToken(Context context) {
PersistentCookieStore cookieStore = new PersistentCookieStore(context);
cookieStore.removeAll();
}
/**
* 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,85 @@
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 spannable.
*
* @author bgamard.
*/
public class SpannableUtil {
/**
* Create a colored spannable from tags.
*
* @param tags Tags
* @return Colored spannable
*/
public static Spannable buildSpannableTags(JSONArray tags) {
return buildSpannable(tags, "name", "color");
}
/**
* Create a spannable for contributors.
*
* @param contributors Contributors
* @return Spannable
*/
public static Spannable buildSpannableContributors(JSONArray contributors) {
return buildSpannable(contributors, "username", null);
}
/**
* Create a spannable for relations.
*
* @param relations Relations
* @return Spannable
*/
public static Spannable buildSpannableRelations(JSONArray relations) {
return buildSpannable(relations, "title", null);
}
/**
* Create a spannable from a JSONArray.
*
* @param array JSONArray
* @param valueName Name of the value part
* @param colorName Name of the color part (optional)
* @return Spannable
*/
private static Spannable buildSpannable(JSONArray array, String valueName, String colorName) {
SpannableStringBuilder builder = new SpannableStringBuilder();
for (int i = 0; i < array.length(); i++) {
final JSONObject tag = array.optJSONObject(i);
int start = builder.length();
builder.append(" ").append(tag.optString(valueName)).append(" ");
builder.setSpan(new ForegroundColorSpan(Color.WHITE), start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
builder.setSpan(new BackgroundColorSpan(Color.parseColor(tag.optString(colorName, "#5bc0de"))), start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
/*
TODO : Make tags, relations and contributors clickable
builder.setSpan(new ClickableSpan() {
@Override
public void onClick(View widget) {
}
@Override
public void updateDrawState(TextPaint ds) {
super.updateDrawState(ds);
ds.setColor(Color.WHITE);
ds.setUnderlineText(false);
}
}, start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);*/
builder.append(" ");
}
return builder;
}
}

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 B

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