Avoid potential negative array index access to cached text.
[LibreOffice.git] / android / source / src / java / org / libreoffice / LOKitThread.java
blobfd40c30891028f9fd0e57f4eba68931a0d2fb7c9
1 package org.libreoffice;
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;
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;
17 import java.util.ArrayList;
18 import java.util.List;
19 import java.util.concurrent.LinkedBlockingQueue;
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.
25 class LOKitThread extends Thread {
26 private static final String LOGTAG = LOKitThread.class.getSimpleName();
28 private final LinkedBlockingQueue<LOEvent> mEventQueue = new LinkedBlockingQueue<LOEvent>();
30 private TileProvider mTileProvider;
31 private InvalidationHandler mInvalidationHandler;
32 private ImmutableViewportMetrics mViewportMetrics;
33 private GeckoLayerClient mLayerClient;
34 private final LibreOfficeMainActivity mContext;
36 LOKitThread(LibreOfficeMainActivity context) {
37 mContext = context;
38 mInvalidationHandler = null;
39 TileProviderFactory.initialize();
42 /**
43 * Starting point of the thread. Processes events that gather in the queue.
45 @Override
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);
58 /**
59 * Viewport changed, Recheck if tiles need to be added / removed.
61 private void tileReevaluationRequest(ComposedTileLayer composedTileLayer) {
62 if (mTileProvider == null) {
63 return;
65 List<SubTile> tiles = new ArrayList<SubTile>();
67 mLayerClient.beginDrawing();
68 composedTileLayer.addNewTiles(tiles);
69 mLayerClient.endDrawing();
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);
78 mLayerClient.endDrawing();
79 mLayerClient.forceRender();
82 mLayerClient.beginDrawing();
83 composedTileLayer.markTiles();
84 composedTileLayer.clearMarkedTiles();
85 mLayerClient.endDrawing();
86 mLayerClient.forceRender();
89 /**
90 * Invalidate tiles that intersect the input rect.
92 private void tileInvalidation(RectF rect) {
93 if (mLayerClient == null || mTileProvider == null) {
94 return;
97 mLayerClient.beginDrawing();
99 List<SubTile> tiles = new ArrayList<SubTile>();
100 mLayerClient.invalidateTiles(tiles, rect);
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();
107 mLayerClient.endDrawing();
108 mLayerClient.forceRender();
112 * Handle the geometry change + draw.
114 private void redraw(boolean resetZoomAndPosition) {
115 if (mLayerClient == null || mTileProvider == null) {
116 // called too early...
117 return;
120 mLayerClient.setPageRect(0, 0, mTileProvider.getPageWidth(), mTileProvider.getPageHeight());
121 mViewportMetrics = mLayerClient.getViewportMetrics();
122 mLayerClient.setViewportMetrics(mViewportMetrics);
124 if (resetZoomAndPosition) {
125 zoomAndRepositionTheDocument();
128 mLayerClient.forceRedraw();
129 mLayerClient.forceRender();
133 * Reposition the view (zoom and position) when the document is firstly shown. This is document type dependent.
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());
154 * Invalidate everything + handle the geometry change
156 private void refresh(boolean resetZoomAndPosition) {
157 mLayerClient.clearAndResetlayers();
158 redraw(resetZoomAndPosition);
159 updatePartPageRectangles();
160 if (mTileProvider != null && mTileProvider.isSpreadsheet()) {
161 updateCalcHeaders();
166 * Update part page rectangles which hold positions of each document page.
167 * Result is stored in DocumentOverlayView class.
169 private void updatePartPageRectangles() {
170 if (mTileProvider == null) {
171 Log.d(LOGTAG, "mTileProvider==null when calling updatePartPageRectangles");
172 return;
174 String partPageRectString = ((LOKitTileProvider) mTileProvider).getPartPageRectangles();
175 List<RectF> partPageRectangles = mInvalidationHandler.convertPayloadToRectangles(partPageRectString);
176 mContext.getDocumentOverlay().setPartPageRectangles(partPageRectangles);
179 private void updatePageSize(int pageWidth, int pageHeight){
180 mTileProvider.setDocumentSize(pageWidth, pageHeight);
181 redraw(true);
184 private void updateZoomConstraints() {
185 if (mTileProvider == null) return;
186 mLayerClient = mContext.getLayerClient();
187 // Set default zoom to the page width and min zoom so that the whole page is visible
188 final float pageHeightZoom = mLayerClient.getViewportMetrics().getHeight() / mTileProvider.getPageHeight();
189 final float pageWidthZoom = mLayerClient.getViewportMetrics().getWidth() / mTileProvider.getPageWidth();
190 final float minZoom = Math.min(pageWidthZoom, pageHeightZoom);
191 mLayerClient.setZoomConstraints(new ZoomConstraints(pageWidthZoom, minZoom, 0f));
195 * Change part of the document.
197 private void changePart(int partIndex) {
198 LOKitShell.showProgressSpinner(mContext);
199 mTileProvider.changePart(partIndex);
200 mViewportMetrics = mLayerClient.getViewportMetrics();
201 // mLayerClient.setViewportMetrics(mViewportMetrics.scaleTo(0.9f, new PointF()));
202 refresh(true);
203 LOKitShell.hideProgressSpinner(mContext);
207 * Handle load document event.
208 * @param filePath - filePath to where the document is located
209 * @return Whether the document has been loaded successfully.
211 private boolean loadDocument(String filePath) {
212 mLayerClient = mContext.getLayerClient();
214 mInvalidationHandler = new InvalidationHandler(mContext);
215 mTileProvider = TileProviderFactory.create(mContext, mInvalidationHandler, filePath);
217 if (mTileProvider.isReady()) {
218 LOKitShell.showProgressSpinner(mContext);
219 updateZoomConstraints();
220 refresh(true);
221 LOKitShell.hideProgressSpinner(mContext);
222 return true;
223 } else {
224 closeDocument();
225 return false;
230 * Handle load new document event.
231 * @param filePath - filePath to where new document is to be created
232 * @param fileType - fileType what type of new document is to be loaded
234 private void loadNewDocument(String filePath, String fileType) {
235 boolean ok = loadDocument(fileType);
236 if (ok) {
237 mTileProvider.saveDocumentAs(filePath, true);
242 * Save the currently loaded document.
244 private void saveDocumentAs(String filePath, String fileType, boolean bTakeOwnership) {
245 if (mTileProvider == null) {
246 Log.e(LOGTAG, "Error in saving, Tile Provider instance is null");
247 } else {
248 mTileProvider.saveDocumentAs(filePath, fileType, bTakeOwnership);
253 * Close the currently loaded document.
255 private void closeDocument() {
256 if (mTileProvider != null) {
257 mTileProvider.close();
258 mTileProvider = null;
263 * Process the input event.
265 private void processEvent(LOEvent event) {
266 switch (event.mType) {
267 case LOEvent.LOAD:
268 loadDocument(event.filePath);
269 break;
270 case LOEvent.LOAD_NEW:
271 loadNewDocument(event.filePath, event.fileType);
272 break;
273 case LOEvent.SAVE_AS:
274 saveDocumentAs(event.filePath, event.fileType, true);
275 break;
276 case LOEvent.SAVE_COPY_AS:
277 saveDocumentAs(event.filePath, event.fileType, false);
278 break;
279 case LOEvent.CLOSE:
280 closeDocument();
281 break;
282 case LOEvent.SIZE_CHANGED:
283 redraw(false);
284 break;
285 case LOEvent.CHANGE_PART:
286 changePart(event.mPartIndex);
287 break;
288 case LOEvent.TILE_INVALIDATION:
289 tileInvalidation(event.mInvalidationRect);
290 break;
291 case LOEvent.THUMBNAIL:
292 createThumbnail(event.mTask);
293 break;
294 case LOEvent.TOUCH:
295 touch(event.mTouchType, event.mDocumentCoordinate);
296 break;
297 case LOEvent.KEY_EVENT:
298 keyEvent(event.mKeyEvent);
299 break;
300 case LOEvent.TILE_REEVALUATION_REQUEST:
301 tileReevaluationRequest(event.mComposedTileLayer);
302 break;
303 case LOEvent.CHANGE_HANDLE_POSITION:
304 changeHandlePosition(event.mHandleType, event.mDocumentCoordinate);
305 break;
306 case LOEvent.SWIPE_LEFT:
307 if (null != mTileProvider) onSwipeLeft();
308 break;
309 case LOEvent.SWIPE_RIGHT:
310 if (null != mTileProvider) onSwipeRight();
311 break;
312 case LOEvent.NAVIGATION_CLICK:
313 mInvalidationHandler.changeStateTo(InvalidationHandler.OverlayState.NONE);
314 break;
315 case LOEvent.UNO_COMMAND:
316 if (null == mTileProvider)
317 Log.e(LOGTAG, "no mTileProvider when trying to process "+event.mValue+" from UNO_COMMAND "+event.mString);
318 else
319 mTileProvider.postUnoCommand(event.mString, event.mValue);
320 break;
321 case LOEvent.UPDATE_PART_PAGE_RECT:
322 updatePartPageRectangles();
323 break;
324 case LOEvent.UPDATE_ZOOM_CONSTRAINTS:
325 updateZoomConstraints();
326 break;
327 case LOEvent.UPDATE_CALC_HEADERS:
328 updateCalcHeaders();
329 break;
330 case LOEvent.UNO_COMMAND_NOTIFY:
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, event.mNotify);
335 break;
336 case LOEvent.REFRESH:
337 refresh(false);
338 break;
339 case LOEvent.PAGE_SIZE_CHANGED:
340 updatePageSize(event.mPageWidth, event.mPageHeight);
341 break;
345 private void updateCalcHeaders() {
346 if (null == mTileProvider) return;
347 LOKitTileProvider tileProvider = (LOKitTileProvider)mTileProvider;
348 String values = tileProvider.getCalcHeaders();
349 mContext.getCalcHeadersController().setHeaders(values);
353 * Request a change of the handle position.
355 private void changeHandlePosition(SelectionHandle.HandleType handleType, PointF documentCoordinate) {
356 switch (handleType) {
357 case MIDDLE:
358 mTileProvider.setTextSelectionReset(documentCoordinate);
359 break;
360 case START:
361 mTileProvider.setTextSelectionStart(documentCoordinate);
362 break;
363 case END:
364 mTileProvider.setTextSelectionEnd(documentCoordinate);
365 break;
370 * Processes key events.
372 private void keyEvent(KeyEvent keyEvent) {
373 if (!LOKitShell.isEditingEnabled()) {
374 return;
376 if (mTileProvider == null) {
377 return;
379 mInvalidationHandler.keyEvent();
380 mTileProvider.sendKeyEvent(keyEvent);
384 * Process swipe left event.
386 private void onSwipeLeft() {
387 mTileProvider.onSwipeLeft();
391 * Process swipe right event.
393 private void onSwipeRight() {
394 mTileProvider.onSwipeRight();
398 * Processes touch events.
400 private void touch(String touchType, PointF documentCoordinate) {
401 if (mTileProvider == null || mViewportMetrics == null) {
402 return;
405 // to handle hyperlinks, enable single tap even in the Viewer
406 boolean editing = LOKitShell.isEditingEnabled();
407 float zoomFactor = mViewportMetrics.getZoomFactor();
409 if (touchType.equals("LongPress")) {
410 mInvalidationHandler.changeStateTo(InvalidationHandler.OverlayState.TRANSITION);
411 mTileProvider.mouseButtonDown(documentCoordinate, 1, zoomFactor);
412 mTileProvider.mouseButtonUp(documentCoordinate, 1, zoomFactor);
413 mTileProvider.mouseButtonDown(documentCoordinate, 2, zoomFactor);
414 mTileProvider.mouseButtonUp(documentCoordinate, 2, zoomFactor);
415 } else if (touchType.equals("SingleTap")) {
416 mInvalidationHandler.changeStateTo(InvalidationHandler.OverlayState.TRANSITION);
417 mTileProvider.mouseButtonDown(documentCoordinate, 1, zoomFactor);
418 mTileProvider.mouseButtonUp(documentCoordinate, 1, zoomFactor);
419 } else if (touchType.equals("GraphicSelectionStart") && editing) {
420 mTileProvider.setGraphicSelectionStart(documentCoordinate);
421 } else if (touchType.equals("GraphicSelectionEnd") && editing) {
422 mTileProvider.setGraphicSelectionEnd(documentCoordinate);
427 * Create thumbnail for the requested document task.
429 private void createThumbnail(final ThumbnailCreator.ThumbnailCreationTask task) {
430 final Bitmap bitmap = task.getThumbnail(mTileProvider);
431 task.applyBitmap(bitmap);
435 * Queue an event.
437 public void queueEvent(LOEvent event) {
438 mEventQueue.add(event);
442 * Clear all events in the queue (used when document is closed).
444 public void clearQueue() {
445 mEventQueue.clear();
449 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */