View Javadoc
1   /*
2    * Copyright (C) 2003-2015 eXo Platform SAS.
3    *
4    * This is free software; you can redistribute it and/or modify it
5    * under the terms of the GNU Lesser General Public License as
6    * published by the Free Software Foundation; either version 3 of
7    * the License, or (at your option) any later version.
8    *
9    * This software is distributed in the hope that it will be useful,
10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12   * Lesser General Public License for more details.
13   *
14   * You should have received a copy of the GNU Lesser General Public
15   * License along with this software; if not, write to the Free
16   * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
17   * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
18   */
19  package org.exoplatform.shareextension.service;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.net.MalformedURLException;
24  import java.net.URL;
25  import java.util.ArrayList;
26  import java.util.HashMap;
27  import java.util.List;
28  import java.util.Locale;
29  import java.util.Map;
30  
31  import org.exoplatform.R;
32  import org.exoplatform.model.SocialPostInfo;
33  import org.exoplatform.shareextension.service.Action.ActionListener;
34  import org.exoplatform.shareextension.service.PostAction.PostActionListener;
35  import org.exoplatform.singleton.DocumentHelper;
36  import org.exoplatform.singleton.SocialServiceHelper;
37  import org.exoplatform.social.client.api.SocialClientLibException;
38  import org.exoplatform.social.client.api.model.RestActivity;
39  import org.exoplatform.social.client.api.model.RestComment;
40  import org.exoplatform.social.client.api.service.ActivityService;
41  import org.exoplatform.utils.ExoConnectionUtils;
42  import org.exoplatform.utils.ExoConstants;
43  import org.exoplatform.utils.ExoDocumentUtils;
44  import org.exoplatform.utils.ExoDocumentUtils.DocumentInfo;
45  import org.exoplatform.utils.Log;
46  import org.exoplatform.utils.TitleExtractor;
47  
48  import android.app.IntentService;
49  import android.app.NotificationManager;
50  import android.content.Context;
51  import android.content.Intent;
52  import android.net.Uri;
53  import android.support.v4.app.NotificationCompat;
54  
55  /**
56   * Created by The eXo Platform SAS.<br/>
57   * IntentService that publishes a post with optional attachments. If multiple
58   * files are attached, the first one is part of the activity, the other are
59   * added in comments on the activity.
60   * 
61   * @author Philippe Aristote paristote@exoplatform.com
62   * @since Jun 4, 2015
63   */
64  public class ShareService extends IntentService {
65  
66    public static final String LOG_TAG     = "____eXo____ShareService____";
67  
68    public static final String POST_INFO   = "postInfo";
69  
70    private int                notifId     = 1;
71  
72    private SocialPostInfo     postInfo;
73  
74    // key is uri in device, value is url on server
75    private List<UploadInfo>   uploadedMap = new ArrayList<UploadInfo>();
76  
77    private enum ShareResult {
78      SUCCESS, ERROR_INCORRECT_CONTENT_URI, ERROR_INCORRECT_ACCOUNT, ERROR_CREATE_FOLDER, ERROR_UPLOAD_FAILED, ERROR_POST_FAILED, ERROR_COMMENT_FAILED
79    }
80  
81    public ShareService() {
82      super("eXo_Share_Service");
83    }
84  
85    /*
86     * We start here when the service is called
87     */
88    @Override
89    protected void onHandleIntent(Intent intent) {
90      // Retrieve the content of the post from the intent
91      postInfo = (SocialPostInfo) intent.getParcelableExtra(POST_INFO);
92  
93      // Notify the user that the share has started
94      notifyBegin();
95  
96      if (postInfo.ownerAccount == null) {
97        notifyResult(ShareResult.ERROR_INCORRECT_ACCOUNT);
98        return;
99      }
100 
101     if (postInfo.hasAttachment()) {
102 
103       UploadInfo initUploadInfo = initUpload();
104       boolean uploadStarted = startUpload(initUploadInfo);
105       if (uploadStarted) {
106         boolean uploadedAll = doUpload(initUploadInfo);
107         if (uploadedAll) {
108           // already set templateParam when first doc upload completed
109           doPost();
110         }
111       }
112       ;
113     } else {
114       // We don't have an attachment, maybe a link
115       // TODO move as a separate Action - MOB-1866
116       String link = null;
117       link = extractLinkFromText();
118       if (link != null) {
119         postInfo.activityType = SocialPostInfo.TYPE_LINK;
120         postInfo.postMessage = postInfo.postMessage.replace(link, String.format(Locale.US, "<a href=\"%s\">%s</a>", link, link));
121       }
122       postInfo.templateParams = linkParams(link);
123       doPost();
124     }
125 
126   }
127 
128   /**
129    * Create the resources needed to create the upload destination folder and
130    * upload the file
131    */
132   private UploadInfo initUpload() {
133     postInfo.activityType = SocialPostInfo.TYPE_DOC;
134     UploadInfo uploadInfo = new UploadInfo();
135     uploadInfo.init(postInfo);
136 
137     return uploadInfo;
138 
139   }
140 
141   /**
142    * Create the directory where the files are stored on the server, if it does
143    * not already exist.
144    */
145   private boolean startUpload(UploadInfo uploadInfo) {
146     return CreateFolderAction.execute(postInfo, uploadInfo, new ActionListener() {
147 
148       @Override
149       public boolean onSuccess(String message) {
150         return true;
151       }
152 
153       @Override
154       public boolean onError(String error) {
155         notifyResult(ShareResult.ERROR_CREATE_FOLDER);
156         return false;
157       }
158     });
159   }
160 
161   /**
162    * Upload the file
163    */
164   private boolean doUpload(UploadInfo initUploadInfo) {
165     boolean uploadedAll = false;
166     uploadedMap.clear();
167     UploadInfo uploadInfo = initUploadInfo;
168     final int numberOfFiles = postInfo.postAttachedFiles.size();
169     for (int i = 0; i < numberOfFiles; i++) {
170       // notify the start of the upload i / total
171       notifyProgress(i + 1, numberOfFiles);
172       // close the current open input stream
173       if (uploadInfo != null && uploadInfo.fileToUpload != null)
174         uploadInfo.fileToUpload.closeDocStream();
175       // Retrieve details of the document to upload
176       if (i != 0) {
177         uploadInfo = new UploadInfo(uploadInfo);
178       }
179 
180       String fileUri = "file://" + postInfo.postAttachedFiles.get(i);
181       Uri uri = Uri.parse(fileUri);
182       uploadInfo.fileToUpload = ExoDocumentUtils.documentInfoFromUri(uri, getBaseContext());
183 
184       if (uploadInfo.fileToUpload == null) {
185         notifyResult(ShareResult.ERROR_INCORRECT_CONTENT_URI);
186         return false;
187       } else {
188         uploadInfo.fileToUpload.documentName = ExoDocumentUtils.cleanupFilename(uploadInfo.fileToUpload.documentName);
189       }
190       uploadedAll = UploadAction.execute(postInfo, uploadInfo, new ActionListener() {
191 
192         @Override
193         public boolean onSuccess(String message) {
194           return true;
195         }
196 
197         @Override
198         public boolean onError(String error) {
199           notifyResult(ShareResult.ERROR_UPLOAD_FAILED);
200           return false;
201         }
202       });
203       if (uploadInfo != null && uploadInfo.fileToUpload != null)
204         uploadInfo.fileToUpload.closeDocStream();
205       if (!uploadedAll) {
206         if (Log.LOGD)
207           Log.e(LOG_TAG, String.format("Failed to upload file %d/%d : %s (doUpload)", i + 1, numberOfFiles, fileUri));
208         break;
209       }
210       if (uploadedAll) {
211         if (Log.LOGD)
212           Log.d(LOG_TAG, String.format("Uploaded file %d/%d OK %s (doUpload)", i + 1, numberOfFiles, fileUri));
213         if (i == 0)
214           postInfo.buildTemplateParams(uploadInfo);
215         else {
216           uploadedMap.add(uploadInfo);
217         }
218       }
219       // Delete file after upload
220       File f = new File(postInfo.postAttachedFiles.get(i));
221       if (Log.LOGD)
222         Log.d(LOG_TAG, "File " + f.getName() + " deleted: " + (f.delete() ? "YES" : "NO"));
223     }
224     return uploadedAll;
225 
226   }
227 
228   /**
229    * Post the message
230    */
231   private boolean doPost() {
232     RestActivity createdAct = PostAction.execute(postInfo, new PostActionListener());
233     boolean ret = createdAct != null;
234     if (ret) {
235       if (Log.LOGD)
236         Log.d(LOG_TAG, "Post activity done");
237       for (UploadInfo commentInfo : uploadedMap) {
238         ret = doComment(createdAct, commentInfo);
239         if (!ret)
240           break;
241         if (Log.LOGD)
242           Log.d(LOG_TAG, "Comment activity done");
243       }
244       // Share finished successfully
245       // Needed to avoid some problems when reopening the app
246       if (ret) {
247         ExoConnectionUtils.loggingOut();
248         // Notify
249         notifyResult(ShareResult.SUCCESS);
250       } else
251         notifyResult(ShareResult.ERROR_COMMENT_FAILED);
252     } else
253       notifyResult(ShareResult.ERROR_POST_FAILED);
254     return ret;
255   }
256 
257   /**
258    * Post a comment on an activity
259    * 
260    * @param restAct the activity to comment
261    * @param commentInfo the info to put in the comment
262    * @return
263    */
264   private boolean doComment(RestActivity restAct, UploadInfo commentInfo) {
265     // TODO create a Comment Action to delegate the operation
266     boolean ret = false;
267     String mimeType = (commentInfo == null ? null
268                                            : (commentInfo.fileToUpload == null ? null
269                                                                                : commentInfo.fileToUpload.documentMimeType));
270     String urlWithoutServer = null;
271     try {
272       URL url = new URL(commentInfo.getUploadedUrl());
273       urlWithoutServer = url.getPath();
274       if (urlWithoutServer != null && !urlWithoutServer.startsWith("/"))
275         urlWithoutServer = "/" + urlWithoutServer;
276     } catch (MalformedURLException e) {
277       if (Log.LOGW)
278         Log.w(LOG_TAG, e.getMessage());
279       return false;
280     }
281     StringBuilder bld = new StringBuilder();
282     // append link
283     bld.append("<a href=\"")
284        .append(urlWithoutServer)
285        .append("\">")
286        .append(commentInfo.fileToUpload.documentName)
287        .append("</a>");
288     // add image in the comment's body
289     if (mimeType != null && mimeType.startsWith("image/")) {
290       String thumbnailUrl = urlWithoutServer.replace("/jcr/", "/thumbnailImage/large/");
291       bld.append("<br/><a href=\"").append(urlWithoutServer).append("\"><img src=\"").append(thumbnailUrl).append("\" /></a>");
292     }
293 
294     ActivityService<RestActivity> activityService = SocialServiceHelper.getInstance().activityService;
295     RestComment restComment = new RestComment();
296     restComment.setText(bld.toString());
297     try {
298       ret = activityService.createComment(restAct, restComment) != null;
299     } catch (SocialClientLibException e) {
300       if (Log.LOGD)
301         Log.e(LOG_TAG, Log.getStackTraceString(e));
302     }
303     return ret;
304   }
305 
306   private Map<String, String> linkParams(String link) {
307     // Create and return TemplateParams for a LINK_ACTIVITY
308     // Return null if there is no link
309     if (link == null)
310       return null;
311     Map<String, String> templateParams = new HashMap<String, String>();
312     templateParams.put("comment", postInfo.postMessage);
313     templateParams.put("link", link);
314     templateParams.put("description", "");
315     templateParams.put("image", "");
316     try {
317       templateParams.put("title", TitleExtractor.getPageTitle(link));
318     } catch (IOException e) {
319       Log.e(LOG_TAG, "Cannot retrieve link title", e);
320       templateParams.put("title", link);
321     }
322     return templateParams;
323   }
324 
325   private String extractLinkFromText() {
326     String text = postInfo.postMessage;
327     // Find an occurrence of http:// or https://
328     // And return the corresponding URL if any
329     int posHttp = text.indexOf("http://");
330     int posHttps = text.indexOf("https://");
331     int startOfLink = -1;
332     if (posHttps > -1)
333       startOfLink = posHttps;
334     else if (posHttp > -1)
335       startOfLink = posHttp;
336     if (startOfLink > -1) {
337       int endOfLink = text.indexOf(' ', startOfLink);
338       if (endOfLink == -1)
339         return text.substring(startOfLink);
340       else
341         return text.substring(startOfLink, endOfLink);
342     } else {
343       return null;
344     }
345   }
346 
347   /**
348    * Send a local notification to inform that the share has started
349    */
350   private void notifyBegin() {
351     notifId = (int) System.currentTimeMillis();
352     String title = postInfo.hasAttachment() ? getString(R.string.ShareDocumentTitle) : getString(R.string.ShareMessageTitle);
353     String text = postInfo.hasAttachment() ? getString(R.string.ShareDocumentText) : getString(R.string.ShareMessageText);
354     NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext());
355     builder.setSmallIcon(R.drawable.icon_share_notif);
356     builder.setContentTitle(title);
357     builder.setContentText(text);
358     builder.setAutoCancel(true);
359     builder.setProgress(0, 0, true);
360     NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
361     manager.notify(notifId, builder.build());
362   }
363 
364   /**
365    * Send a local notification to inform of the progress. Only called if the
366    * share contains 1 or more attachments.
367    * 
368    * @param current the index of the current file being uploaded
369    * @param total the total number of files to upload
370    */
371   private void notifyProgress(int current, int total) {
372     String text = String.format(Locale.US, "%s (%d/%d)", getString(R.string.ShareDocumentText), current, total);
373     NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext());
374     builder.setSmallIcon(R.drawable.icon_share_notif);
375     builder.setContentTitle(getString(R.string.ShareDocumentTitle));
376     builder.setContentText(text);
377     builder.setAutoCancel(true);
378     builder.setProgress(0, 0, true);
379     NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
380     manager.notify(notifId, builder.build());
381   }
382 
383   /**
384    * Notify the end of the sharing. The message depends on the given result.
385    * 
386    * @param result one of {@link ShareResult} values
387    */
388   private void notifyResult(ShareResult result) {
389     String text = "";
390     switch (result) {
391     case ERROR_CREATE_FOLDER:
392       text = getString(R.string.ShareErrorUploadFolderFailed);
393       break;
394     case ERROR_INCORRECT_ACCOUNT:
395       text = getString(R.string.ShareErrorIncorrectAccount);
396       break;
397     case ERROR_INCORRECT_CONTENT_URI:
398       text = getString(R.string.ShareErrorCannotReadDoc);
399       break;
400     case ERROR_POST_FAILED:
401       text = getString(R.string.ShareErrorPostFailed);
402       break;
403     case ERROR_COMMENT_FAILED:
404       text = getString(R.string.ShareErrorCommentFailed);
405       break;
406     case ERROR_UPLOAD_FAILED:
407       text = getString(R.string.ShareErrorUploadFailed);
408       break;
409     case SUCCESS:
410       text = getString(R.string.ShareOperationSuccess);
411       break;
412     default:
413       break;
414     }
415     String title = postInfo.hasAttachment() ? getString(R.string.ShareDocumentTitle) : getString(R.string.ShareMessageTitle);
416     NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext());
417     builder.setSmallIcon(R.drawable.icon_share_notif);
418     builder.setContentTitle(title);
419     builder.setContentText(text);
420     builder.setAutoCancel(true);
421     builder.setProgress(0, 0, false);
422     NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
423     manager.notify(notifId, builder.build());
424   }
425 
426   public static class UploadInfo {
427 
428     public String       uploadId;
429 
430     public DocumentInfo fileToUpload;
431 
432     public String       repository;
433 
434     public String       workspace;
435 
436     public String       drive;
437 
438     public String       folder;
439 
440     public String       jcrUrl;
441 
442     public UploadInfo() {
443       super();
444     }
445 
446     public UploadInfo(UploadInfo another) {
447       uploadId = Long.toHexString(System.currentTimeMillis());
448       this.repository = another.repository;
449       this.workspace = another.workspace;
450       this.drive = another.drive;
451       this.folder = another.folder;
452       this.jcrUrl = another.jcrUrl;
453     }
454 
455     public void init(SocialPostInfo postInfo) {
456 
457       uploadId = Long.toHexString(System.currentTimeMillis());
458       repository = DocumentHelper.getInstance().repository;
459       workspace = DocumentHelper.getInstance().workspace;
460 
461       if (postInfo.isPublic()) {
462         // File will be uploaded in the Public folder of the user's drive
463         // e.g. /Users/u___/us___/use___/user/Public/Mobile
464         drive = ExoConstants.DOCUMENT_PERSONAL_DRIVE_NAME;
465         folder = "Public/Mobile";
466         jcrUrl = DocumentHelper.getInstance().getRepositoryHomeUrl();
467       } else {
468         // File will be uploaded in the Documents folder of the space's drive
469         // e.g. /Groups/spaces/the_space/Documents/Mobile
470         drive = ".spaces." + postInfo.destinationSpace.getOriginalName();
471         folder = "Mobile";
472         StringBuffer url = new StringBuffer(postInfo.ownerAccount.serverUrl).append(ExoConstants.DOCUMENT_JCR_PATH)
473                                                                             .append("/")
474                                                                             .append(repository)
475                                                                             .append("/")
476                                                                             .append(workspace)
477                                                                             .append("/Groups/spaces/")
478                                                                             .append(postInfo.destinationSpace.getOriginalName())
479                                                                             .append("/Documents");
480         jcrUrl = url.toString();
481       }
482     }
483     
484     public String getUploadedUrl() {
485       return new StringBuffer(jcrUrl).append("/").append(folder).append("/").append(fileToUpload.documentName).toString();
486     }
487   }
488 }