1 package org.libreoffice;
2 
3 import android.graphics.Bitmap;
4 import android.graphics.PointF;
5 import android.graphics.RectF;
6 import android.util.Log;
7 import android.view.KeyEvent;
8 
9 import org.libreoffice.canvas.SelectionHandle;
10 import org.mozilla.gecko.ZoomConstraints;
11 import org.mozilla.gecko.gfx.CairoImage;
12 import org.mozilla.gecko.gfx.ComposedTileLayer;
13 import org.mozilla.gecko.gfx.GeckoLayerClient;
14 import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
15 import org.mozilla.gecko.gfx.SubTile;
16 
17 import java.util.ArrayList;
18 import java.util.List;
19 import java.util.concurrent.LinkedBlockingQueue;
20 
21 /*
22  * Thread that communicates with LibreOffice through LibreOfficeKit JNI interface. The thread
23  * consumes events from other threads (mainly the UI thread) and acts accordingly.
24  */
25 class LOKitThread extends Thread {
26     private static final String LOGTAG = LOKitThread.class.getSimpleName();
27 
28     private final LinkedBlockingQueue<LOEvent> mEventQueue = new LinkedBlockingQueue<LOEvent>();
29 
30     private TileProvider mTileProvider;
31     private InvalidationHandler mInvalidationHandler;
32     private ImmutableViewportMetrics mViewportMetrics;
33     private GeckoLayerClient mLayerClient;
34     private final LibreOfficeMainActivity mContext;
35 
LOKitThread(LibreOfficeMainActivity context)36     LOKitThread(LibreOfficeMainActivity context) {
37         mContext = context;
38         mInvalidationHandler = null;
39         TileProviderFactory.initialize();
40     }
41 
42     /**
43      * Starting point of the thread. Processes events that gather in the queue.
44      */
45     @Override
run()46     public void run() {
47         while (true) {
48             LOEvent event;
49             try {
50                 event = mEventQueue.take();
51                 processEvent(event);
52             } catch (InterruptedException exception) {
53                 throw new RuntimeException(exception);
54             }
55         }
56     }
57 
58     /**
59      * Viewport changed, Recheck if tiles need to be added / removed.
60      */
tileReevaluationRequest(ComposedTileLayer composedTileLayer)61     private void tileReevaluationRequest(ComposedTileLayer composedTileLayer) {
62         if (mTileProvider == null) {
63             return;
64         }
65         List<SubTile> tiles = new ArrayList<SubTile>();
66 
67         mLayerClient.beginDrawing();
68         composedTileLayer.addNewTiles(tiles);
69         mLayerClient.endDrawing();
70 
71         for (SubTile tile : tiles) {
72             TileIdentifier tileId = tile.id;
73             CairoImage image = mTileProvider.createTile(tileId.x, tileId.y, tileId.size, tileId.zoom);
74             mLayerClient.beginDrawing();
75             if (image != null) {
76                 tile.setImage(image);
77             }
78             mLayerClient.endDrawing();
79             mLayerClient.forceRender();
80         }
81 
82         mLayerClient.beginDrawing();
83         composedTileLayer.markTiles();
84         composedTileLayer.clearMarkedTiles();
85         mLayerClient.endDrawing();
86         mLayerClient.forceRender();
87     }
88 
89     /**
90      * Invalidate tiles that intersect the input rect.
91      */
tileInvalidation(RectF rect)92     private void tileInvalidation(RectF rect) {
93         if (mLayerClient == null || mTileProvider == null) {
94             return;
95         }
96 
97         mLayerClient.beginDrawing();
98 
99         List<SubTile> tiles = new ArrayList<SubTile>();
100         mLayerClient.invalidateTiles(tiles, rect);
101 
102         for (SubTile tile : tiles) {
103             CairoImage image = mTileProvider.createTile(tile.id.x, tile.id.y, tile.id.size, tile.id.zoom);
104             tile.setImage(image);
105             tile.invalidate();
106         }
107         mLayerClient.endDrawing();
108         mLayerClient.forceRender();
109     }
110 
111     /**
112      * Handle the geometry change + draw.
113      */
redraw(boolean resetZoomAndPosition)114     private void redraw(boolean resetZoomAndPosition) {
115         if (mLayerClient == null || mTileProvider == null) {
116             // called too early...
117             return;
118         }
119 
120         mLayerClient.setPageRect(0, 0, mTileProvider.getPageWidth(), mTileProvider.getPageHeight());
121         mViewportMetrics = mLayerClient.getViewportMetrics();
122         mLayerClient.setViewportMetrics(mViewportMetrics);
123 
124         if (resetZoomAndPosition) {
125             zoomAndRepositionTheDocument();
126         }
127 
128         mLayerClient.forceRedraw();
129         mLayerClient.forceRender();
130     }
131 
132     /**
133      * Reposition the view (zoom and position) when the document is firstly shown. This is document type dependent.
134      */
zoomAndRepositionTheDocument()135     private void zoomAndRepositionTheDocument() {
136         if (mTileProvider.isSpreadsheet()) {
137             // Don't do anything for spreadsheets - show at 100%
138         } else if (mTileProvider.isTextDocument()) {
139             // Always zoom text document to the beginning of the document and centered by width
140             float centerY = mViewportMetrics.getCssViewport().centerY();
141             mLayerClient.zoomTo(new RectF(0, centerY, mTileProvider.getPageWidth(), centerY));
142         } else {
143             // Other documents - always show the whole document on the screen,
144             // regardless of document shape and orientation.
145             if (mViewportMetrics.getViewport().width() < mViewportMetrics.getViewport().height()) {
146                 mLayerClient.zoomTo(mTileProvider.getPageWidth(), 0);
147             } else {
148                 mLayerClient.zoomTo(0, mTileProvider.getPageHeight());
149             }
150         }
151     }
152 
153     /**
154      * Invalidate everything + handle the geometry change
155      */
refresh(boolean resetZoomAndPosition)156     private void refresh(boolean resetZoomAndPosition) {
157         mLayerClient.clearAndResetlayers();
158         redraw(resetZoomAndPosition);
159         updatePartPageRectangles();
160         if (mTileProvider != null && mTileProvider.isSpreadsheet()) {
161             updateCalcHeaders();
162         }
163     }
164 
165     /**
166      * Update part page rectangles which hold positions of each document page.
167      * Result is stored in DocumentOverlayView class.
168      */
updatePartPageRectangles()169     private void updatePartPageRectangles() {
170         if (mTileProvider == null) {
171             Log.d(LOGTAG, "mTileProvider==null when calling updatePartPageRectangles");
172             return;
173         }
174         String partPageRectString = ((LOKitTileProvider) mTileProvider).getPartPageRectangles();
175         List<RectF> partPageRectangles = mInvalidationHandler.convertPayloadToRectangles(partPageRectString);
176         mContext.getDocumentOverlay().setPartPageRectangles(partPageRectangles);
177     }
178 
updatePageSize(int pageWidth, int pageHeight)179     private void updatePageSize(int pageWidth, int pageHeight){
180         mTileProvider.setDocumentSize(pageWidth, pageHeight);
181         redraw(true);
182     }
183 
updateZoomConstraints()184     private void updateZoomConstraints() {
185         if (mTileProvider == null) return;
186         mLayerClient = mContext.getLayerClient();
187         // Set min zoom to the page width so that you cannot zoom below page width
188         final float minZoom = mLayerClient.getViewportMetrics().getWidth()/mTileProvider.getPageWidth();
189         mLayerClient.setZoomConstraints(new ZoomConstraints(true, 1f, minZoom, 0f));
190     }
191 
192     /**
193      * Change part of the document.
194      */
changePart(int partIndex)195     private void changePart(int partIndex) {
196         LOKitShell.showProgressSpinner(mContext);
197         mTileProvider.changePart(partIndex);
198         mViewportMetrics = mLayerClient.getViewportMetrics();
199         // mLayerClient.setViewportMetrics(mViewportMetrics.scaleTo(0.9f, new PointF()));
200         refresh(true);
201         LOKitShell.hideProgressSpinner(mContext);
202     }
203 
204     /**
205      * Handle load document event.
206      * @param filePath - filePath to where the document is located
207      */
loadDocument(String filePath)208     private void loadDocument(String filePath) {
209         mLayerClient = mContext.getLayerClient();
210 
211         mInvalidationHandler = new InvalidationHandler(mContext);
212         mTileProvider = TileProviderFactory.create(mContext, mInvalidationHandler, filePath);
213 
214         if (mTileProvider.isReady()) {
215             LOKitShell.showProgressSpinner(mContext);
216             updateZoomConstraints();
217             LOKitShell.getMainHandler().post(new Runnable() {
218                 @Override
219                 public void run() {
220                     // synchronize to avoid deletion while loading
221                     synchronized (LOKitThread.this) {
222                         refresh(true);
223                     }
224                 }
225             });
226             LOKitShell.hideProgressSpinner(mContext);
227         } else {
228             closeDocument();
229         }
230     }
231 
232     /**
233      * Handle load new document event.
234      * @param filePath - filePath to where new document is to be created
235      * @param fileType - fileType what type of new document is to be loaded
236      */
loadNewDocument(String filePath, String fileType)237     private void loadNewDocument(String filePath, String fileType) {
238         mLayerClient = mContext.getLayerClient();
239 
240         mInvalidationHandler = new InvalidationHandler(mContext);
241         mTileProvider = TileProviderFactory.create(mContext, mInvalidationHandler, fileType);
242 
243         if (mTileProvider.isReady()) {
244             LOKitShell.showProgressSpinner(mContext);
245             updateZoomConstraints();
246             refresh(true);
247             LOKitShell.hideProgressSpinner(mContext);
248 
249             mTileProvider.saveDocumentAs(filePath, true);
250         } else {
251             closeDocument();
252         }
253     }
254 
255     /**
256      * Save the currently loaded document.
257      */
saveDocumentAs(String filePath, String fileType, boolean bTakeOwnership)258     private void saveDocumentAs(String filePath, String fileType, boolean bTakeOwnership) {
259        if (mTileProvider == null) {
260            Log.e(LOGTAG, "Error in saving, Tile Provider instance is null");
261        } else {
262            mTileProvider.saveDocumentAs(filePath, fileType, bTakeOwnership);
263        }
264     }
265 
266     /**
267      * Close the currently loaded document.
268      */
269     // needs to be synchronized to not destroy doc while it's loaded
closeDocument()270     private synchronized void closeDocument() {
271         if (mTileProvider != null) {
272             mTileProvider.close();
273             mTileProvider = null;
274         }
275     }
276 
277     /**
278      * Process the input event.
279      */
processEvent(LOEvent event)280     private void processEvent(LOEvent event) {
281         switch (event.mType) {
282             case LOEvent.LOAD:
283                 loadDocument(event.filePath);
284                 break;
285             case LOEvent.LOAD_NEW:
286                 loadNewDocument(event.filePath, event.fileType);
287                 break;
288             case LOEvent.SAVE_AS:
289                 saveDocumentAs(event.filePath, event.fileType, true);
290                 break;
291             case LOEvent.SAVE_COPY_AS:
292                 saveDocumentAs(event.filePath, event.fileType, false);
293                 break;
294             case LOEvent.CLOSE:
295                 closeDocument();
296                 break;
297             case LOEvent.SIZE_CHANGED:
298                 redraw(false);
299                 break;
300             case LOEvent.CHANGE_PART:
301                 changePart(event.mPartIndex);
302                 break;
303             case LOEvent.TILE_INVALIDATION:
304                 tileInvalidation(event.mInvalidationRect);
305                 break;
306             case LOEvent.THUMBNAIL:
307                 createThumbnail(event.mTask);
308                 break;
309             case LOEvent.TOUCH:
310                 touch(event.mTouchType, event.mDocumentCoordinate);
311                 break;
312             case LOEvent.KEY_EVENT:
313                 keyEvent(event.mKeyEvent);
314                 break;
315             case LOEvent.TILE_REEVALUATION_REQUEST:
316                 tileReevaluationRequest(event.mComposedTileLayer);
317                 break;
318             case LOEvent.CHANGE_HANDLE_POSITION:
319                 changeHandlePosition(event.mHandleType, event.mDocumentCoordinate);
320                 break;
321             case LOEvent.SWIPE_LEFT:
322                 if (null != mTileProvider) onSwipeLeft();
323                 break;
324             case LOEvent.SWIPE_RIGHT:
325                 if (null != mTileProvider) onSwipeRight();
326                 break;
327             case LOEvent.NAVIGATION_CLICK:
328                 mInvalidationHandler.changeStateTo(InvalidationHandler.OverlayState.NONE);
329                 break;
330             case LOEvent.UNO_COMMAND:
331                 if (null == mTileProvider)
332                     Log.e(LOGTAG, "no mTileProvider when trying to process "+event.mValue+" from UNO_COMMAND "+event.mString);
333                 else
334                     mTileProvider.postUnoCommand(event.mString, event.mValue);
335                 break;
336             case LOEvent.UPDATE_PART_PAGE_RECT:
337                 updatePartPageRectangles();
338                 break;
339             case LOEvent.UPDATE_ZOOM_CONSTRAINTS:
340                 updateZoomConstraints();
341                 break;
342             case LOEvent.UPDATE_CALC_HEADERS:
343                 updateCalcHeaders();
344                 break;
345             case LOEvent.UNO_COMMAND_NOTIFY:
346                 if (null == mTileProvider)
347                     Log.e(LOGTAG, "no mTileProvider when trying to process "+event.mValue+" from UNO_COMMAND "+event.mString);
348                 else
349                     mTileProvider.postUnoCommand(event.mString, event.mValue, event.mNotify);
350                 break;
351             case LOEvent.REFRESH:
352                 refresh(false);
353                 break;
354             case LOEvent.PAGE_SIZE_CHANGED:
355                 updatePageSize(event.mPageWidth, event.mPageHeight);
356                 break;
357         }
358     }
359 
updateCalcHeaders()360     private void updateCalcHeaders() {
361         if (null == mTileProvider) return;
362         LOKitTileProvider tileProvider = (LOKitTileProvider)mTileProvider;
363         String values = tileProvider.getCalcHeaders();
364         mContext.getCalcHeadersController().setHeaders(values);
365     }
366 
367     /**
368      * Request a change of the handle position.
369      */
changeHandlePosition(SelectionHandle.HandleType handleType, PointF documentCoordinate)370     private void changeHandlePosition(SelectionHandle.HandleType handleType, PointF documentCoordinate) {
371         switch (handleType) {
372             case MIDDLE:
373                 mTileProvider.setTextSelectionReset(documentCoordinate);
374                 break;
375             case START:
376                 mTileProvider.setTextSelectionStart(documentCoordinate);
377                 break;
378             case END:
379                 mTileProvider.setTextSelectionEnd(documentCoordinate);
380                 break;
381         }
382     }
383 
384     /**
385      * Processes key events.
386      */
keyEvent(KeyEvent keyEvent)387     private void keyEvent(KeyEvent keyEvent) {
388         if (!LOKitShell.isEditingEnabled()) {
389             return;
390         }
391         if (mTileProvider == null) {
392             return;
393         }
394         mInvalidationHandler.keyEvent();
395         mTileProvider.sendKeyEvent(keyEvent);
396     }
397 
398     /**
399      * Process swipe left event.
400      */
onSwipeLeft()401     private void onSwipeLeft() {
402         mTileProvider.onSwipeLeft();
403     }
404 
405     /**
406      * Process swipe right event.
407      */
onSwipeRight()408     private void onSwipeRight() {
409         mTileProvider.onSwipeRight();
410     }
411 
412     /**
413      * Processes touch events.
414      */
touch(String touchType, PointF documentCoordinate)415     private void touch(String touchType, PointF documentCoordinate) {
416         if (mTileProvider == null || mViewportMetrics == null) {
417             return;
418         }
419 
420         // to handle hyperlinks, enable single tap even in the Viewer
421         boolean editing = LOKitShell.isEditingEnabled();
422         float zoomFactor = mViewportMetrics.getZoomFactor();
423 
424         if (touchType.equals("LongPress")) {
425             mInvalidationHandler.changeStateTo(InvalidationHandler.OverlayState.TRANSITION);
426             mTileProvider.mouseButtonDown(documentCoordinate, 1, zoomFactor);
427             mTileProvider.mouseButtonUp(documentCoordinate, 1, zoomFactor);
428             mTileProvider.mouseButtonDown(documentCoordinate, 2, zoomFactor);
429             mTileProvider.mouseButtonUp(documentCoordinate, 2, zoomFactor);
430         } else if (touchType.equals("SingleTap")) {
431             mInvalidationHandler.changeStateTo(InvalidationHandler.OverlayState.TRANSITION);
432             mTileProvider.mouseButtonDown(documentCoordinate, 1, zoomFactor);
433             mTileProvider.mouseButtonUp(documentCoordinate, 1, zoomFactor);
434         } else if (touchType.equals("GraphicSelectionStart") && editing) {
435             mTileProvider.setGraphicSelectionStart(documentCoordinate);
436         } else if (touchType.equals("GraphicSelectionEnd") && editing) {
437             mTileProvider.setGraphicSelectionEnd(documentCoordinate);
438         }
439     }
440 
441     /**
442      * Create thumbnail for the requested document task.
443      */
createThumbnail(final ThumbnailCreator.ThumbnailCreationTask task)444     private void createThumbnail(final ThumbnailCreator.ThumbnailCreationTask task) {
445         final Bitmap bitmap = task.getThumbnail(mTileProvider);
446         task.applyBitmap(bitmap);
447     }
448 
449     /**
450      * Queue an event.
451      */
queueEvent(LOEvent event)452     public void queueEvent(LOEvent event) {
453         mEventQueue.add(event);
454     }
455 
456     /**
457      * Clear all events in the queue (used when document is closed).
458      */
clearQueue()459     public void clearQueue() {
460         mEventQueue.clear();
461     }
462 }
463 
464 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
465