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