1 package org
.libreoffice
;
3 import android
.app
.Activity
;
4 import android
.app
.AlertDialog
;
5 import android
.content
.ContentResolver
;
6 import android
.content
.Context
;
7 import android
.content
.DialogInterface
;
8 import android
.content
.Intent
;
9 import android
.content
.SharedPreferences
;
10 import android
.content
.res
.AssetFileDescriptor
;
11 import android
.content
.res
.AssetManager
;
12 import android
.graphics
.RectF
;
13 import android
.os
.AsyncTask
;
14 import android
.os
.Bundle
;
15 import android
.os
.Handler
;
16 import android
.preference
.PreferenceManager
;
17 import android
.support
.v4
.widget
.DrawerLayout
;
18 import android
.support
.v7
.app
.AppCompatActivity
;
19 import android
.support
.v7
.widget
.Toolbar
;
20 import android
.util
.Log
;
21 import android
.view
.View
;
22 import android
.view
.inputmethod
.InputMethodManager
;
23 import android
.widget
.AdapterView
;
24 import android
.widget
.ListView
;
25 import android
.widget
.Toast
;
27 import org
.libreoffice
.overlay
.DocumentOverlay
;
28 import org
.libreoffice
.storage
.DocumentProviderFactory
;
29 import org
.libreoffice
.storage
.IFile
;
30 import org
.mozilla
.gecko
.ZoomConstraints
;
31 import org
.mozilla
.gecko
.gfx
.GeckoLayerClient
;
32 import org
.mozilla
.gecko
.gfx
.LayerView
;
35 import java
.io
.FileNotFoundException
;
36 import java
.io
.FileOutputStream
;
37 import java
.io
.IOException
;
39 import java
.nio
.ByteBuffer
;
40 import java
.nio
.channels
.Channels
;
41 import java
.nio
.channels
.FileChannel
;
42 import java
.nio
.channels
.ReadableByteChannel
;
43 import java
.util
.ArrayList
;
44 import java
.util
.List
;
47 * Main activity of the LibreOffice App. It is started in the UI thread.
49 public class LibreOfficeMainActivity
extends AppCompatActivity
{
51 private static final String LOGTAG
= "LibreOfficeMainActivity";
52 private static final String DEFAULT_DOC_PATH
= "/assets/example.odt";
53 private static final String ENABLE_EXPERIMENTAL_PREFS_KEY
= "ENABLE_EXPERIMENTAL";
54 private static final String ASSETS_EXTRACTED_PREFS_KEY
= "ASSETS_EXTRACTED";
56 public static LibreOfficeMainActivity mAppContext
;
58 private static GeckoLayerClient mLayerClient
;
59 private static LOKitThread sLOKitThread
;
61 private static boolean mIsExperimentalMode
;
63 private int providerId
;
64 private URI documentUri
;
66 public Handler mMainHandler
;
68 private DrawerLayout mDrawerLayout
;
69 private LOAbout mAbout
;
71 private ListView mDrawerList
;
72 private List
<DocumentPartView
> mDocumentPartView
= new ArrayList
<DocumentPartView
>();
73 private DocumentPartViewListAdapter mDocumentPartViewListAdapter
;
74 private File mInputFile
;
75 private DocumentOverlay mDocumentOverlay
;
76 private File mTempFile
= null;
78 private FormattingController mFormattingController
;
79 private ToolbarController mToolbarController
;
80 private FontController mFontController
;
81 private SearchController mSearchController
;
83 public LibreOfficeMainActivity() {
84 mAbout
= new LOAbout(this, false);
87 public static GeckoLayerClient
getLayerClient() {
91 public static boolean isExperimentalMode() {
92 return mIsExperimentalMode
;
95 public boolean usesTemporaryFile() {
96 return mTempFile
!= null;
100 public void onCreate(Bundle savedInstanceState
) {
101 Log
.w(LOGTAG
, "onCreate..");
103 super.onCreate(savedInstanceState
);
105 SharedPreferences sPrefs
= PreferenceManager
.getDefaultSharedPreferences(getApplicationContext());
106 mIsExperimentalMode
= sPrefs
.getBoolean(ENABLE_EXPERIMENTAL_PREFS_KEY
, false);
108 if (sPrefs
.getInt(ASSETS_EXTRACTED_PREFS_KEY
, 0) != BuildConfig
.VERSION_CODE
) {
109 if(copyFromAssets(getAssets(), "unpack", getApplicationInfo().dataDir
)) {
110 sPrefs
.edit().putInt(ASSETS_EXTRACTED_PREFS_KEY
, BuildConfig
.VERSION_CODE
).apply();
113 mMainHandler
= new Handler();
115 setContentView(R
.layout
.activity_main
);
117 Toolbar toolbarTop
= (Toolbar
) findViewById(R
.id
.toolbar
);
118 Toolbar toolbarBottom
= (Toolbar
) findViewById(R
.id
.toolbar_bottom
);
122 mToolbarController
= new ToolbarController(this, getSupportActionBar(), toolbarTop
);
123 mFormattingController
= new FormattingController(this, toolbarBottom
);
124 toolbarTop
.setNavigationOnClickListener(new View
.OnClickListener() {
126 public void onClick(View view
) {
127 LOKitShell
.sendNavigationClickEvent();
131 mFontController
= new FontController(this);
132 mSearchController
= new SearchController(this);
134 if (getIntent().getData() != null) {
135 if (getIntent().getData().getScheme().equals(ContentResolver
.SCHEME_CONTENT
)) {
136 if (copyFileToTemp() && mTempFile
!= null) {
137 mInputFile
= mTempFile
;
138 Log
.d(LOGTAG
, "SCHEME_CONTENT: getPath(): " + getIntent().getData().getPath());
140 // TODO: can't open the file
141 Log
.e(LOGTAG
, "couldn't create temporary file from " + getIntent().getData());
143 } else if (getIntent().getData().getScheme().equals(ContentResolver
.SCHEME_FILE
)) {
144 mInputFile
= new File(getIntent().getData().getPath());
145 Log
.d(LOGTAG
, "SCHEME_FILE: getPath(): " + getIntent().getData().getPath());
147 // Gather data to rebuild IFile object later
148 providerId
= getIntent().getIntExtra(
149 "org.libreoffice.document_provider_id", 0);
150 documentUri
= (URI
) getIntent().getSerializableExtra(
151 "org.libreoffice.document_uri");
154 mInputFile
= new File(DEFAULT_DOC_PATH
);
157 mDrawerLayout
= (DrawerLayout
) findViewById(R
.id
.drawer_layout
);
159 if (mDocumentPartViewListAdapter
== null) {
160 mDrawerList
= (ListView
) findViewById(R
.id
.left_drawer
);
162 mDocumentPartViewListAdapter
= new DocumentPartViewListAdapter(this, R
.layout
.document_part_list_layout
, mDocumentPartView
);
163 mDrawerList
.setAdapter(mDocumentPartViewListAdapter
);
164 mDrawerList
.setOnItemClickListener(new DocumentPartClickListener());
167 if (sLOKitThread
== null) {
168 sLOKitThread
= new LOKitThread();
169 sLOKitThread
.start();
171 sLOKitThread
.clearQueue();
174 mLayerClient
= new GeckoLayerClient(this);
175 mLayerClient
.setZoomConstraints(new ZoomConstraints(true));
176 LayerView layerView
= (LayerView
) findViewById(R
.id
.layer_view
);
177 mLayerClient
.setView(layerView
);
178 layerView
.setInputConnectionHandler(new LOKitInputConnectionHandler());
179 mLayerClient
.notifyReady();
181 // create TextCursorLayer
182 mDocumentOverlay
= new DocumentOverlay(mAppContext
, layerView
);
184 mToolbarController
.setupToolbars();
187 public RectF
getCurrentCursorPosition() {
188 return mDocumentOverlay
.getCurrentCursorPosition();
191 private boolean copyFileToTemp() {
192 ContentResolver contentResolver
= getContentResolver();
193 FileChannel inputChannel
= null;
194 FileChannel outputChannel
= null;
195 // CSV files need a .csv suffix to be opened in Calc.
196 String suffix
= null;
197 String intentType
= getIntent().getType();
198 // K-9 mail uses the first, GMail uses the second variant.
199 if ("text/comma-separated-values".equals(intentType
) || "text/csv".equals(intentType
))
204 AssetFileDescriptor assetFD
= contentResolver
.openAssetFileDescriptor(getIntent().getData(), "r");
205 if (assetFD
== null) {
206 Log
.e(LOGTAG
, "couldn't create assetfiledescriptor from " + getIntent().getDataString());
209 inputChannel
= assetFD
.createInputStream().getChannel();
210 mTempFile
= File
.createTempFile("LibreOffice", suffix
, this.getCacheDir());
212 outputChannel
= new FileOutputStream(mTempFile
).getChannel();
213 long bytesTransferred
= 0;
214 // might not copy all at once, so make sure everything gets copied....
215 while (bytesTransferred
< inputChannel
.size()) {
216 bytesTransferred
+= outputChannel
.transferFrom(inputChannel
, bytesTransferred
, inputChannel
.size());
218 Log
.e(LOGTAG
, "Success copying " + bytesTransferred
+ " bytes");
221 if (inputChannel
!= null) inputChannel
.close();
222 if (outputChannel
!= null) outputChannel
.close();
224 } catch (FileNotFoundException e
) {
226 } catch (IOException e
) {
232 * Save the document and invoke save on document provider to upload the file
233 * to the cloud if necessary.
235 public void saveDocument() {
236 final long lastModified
= mInputFile
.lastModified();
237 final Activity activity
= LibreOfficeMainActivity
.this;
238 Toast
.makeText(this, R
.string
.message_saving
, Toast
.LENGTH_SHORT
).show();
240 LOKitShell
.sendEvent(new LOEvent(LOEvent
.UNO_COMMAND
, ".uno:Save"));
242 final AsyncTask
<Void
, Void
, Void
> task
= new AsyncTask
<Void
, Void
, Void
>() {
244 protected Void
doInBackground(Void
... params
) {
246 // rebuild the IFile object from the data passed in the Intent
247 IFile mStorageFile
= DocumentProviderFactory
.getInstance()
248 .getProvider(providerId
).createFromUri(documentUri
);
249 // call document provider save operation
250 mStorageFile
.saveDocument(mInputFile
);
252 catch (final RuntimeException e
) {
253 activity
.runOnUiThread(new Runnable() {
256 Toast
.makeText(activity
, e
.getMessage(),
257 Toast
.LENGTH_SHORT
).show();
260 Log
.e(LOGTAG
, e
.getMessage(), e
.getCause());
266 protected void onPostExecute(Void param
) {
267 Toast
.makeText(activity
, R
.string
.message_saved
,
268 Toast
.LENGTH_SHORT
).show();
271 // Delay the call to document provider save operation and check the
272 // modification time periodically to ensure the local file has been saved.
273 // TODO: ideally the save operation should have a callback
274 Runnable runTask
= new Runnable() {
275 private int timesRun
= 0;
279 if (lastModified
< mInputFile
.lastModified()) {
280 // we are sure local save is complete, push changes to cloud
286 new Handler().postDelayed(this, 5000);
289 // 20 seconds later, the local file has not changed,
290 // maybe there were no changes at all
291 Toast
.makeText(activity
, R
.string
.message_save_incomplete
, Toast
.LENGTH_LONG
).show();
296 new Handler().postDelayed(runTask
, 5000);
300 protected void onResume() {
302 Log
.i(LOGTAG
, "onResume..");
303 // check for config change
304 boolean bEnableExperimental
= PreferenceManager
.getDefaultSharedPreferences(getApplicationContext()).getBoolean(ENABLE_EXPERIMENTAL_PREFS_KEY
, false);
305 if (bEnableExperimental
!= mIsExperimentalMode
) {
306 mIsExperimentalMode
= bEnableExperimental
;
311 protected void onPause() {
312 Log
.i(LOGTAG
, "onPause..");
317 protected void onStart() {
318 Log
.i(LOGTAG
, "onStart..");
320 LOKitShell
.sendLoadEvent(mInputFile
.getPath());
324 protected void onStop() {
325 Log
.i(LOGTAG
, "onStop..");
326 hideSoftKeyboardDirect();
327 LOKitShell
.sendCloseEvent();
332 protected void onDestroy() {
333 Log
.i(LOGTAG
, "onDestroy..");
334 mLayerClient
.destroy();
337 if (isFinishing()) { // Not an orientation change
338 if (mTempFile
!= null) {
339 // noinspection ResultOfMethodCallIgnored
345 public LOKitThread
getLOKitThread() {
349 public List
<DocumentPartView
> getDocumentPartView() {
350 return mDocumentPartView
;
353 public void disableNavigationDrawer() {
354 // Only the original thread that created mDrawerLayout should touch its views.
355 LOKitShell
.getMainHandler().post(new Runnable() {
358 mDrawerLayout
.setDrawerLockMode(DrawerLayout
.LOCK_MODE_LOCKED_CLOSED
, mDrawerList
);
363 public DocumentPartViewListAdapter
getDocumentPartViewListAdapter() {
364 return mDocumentPartViewListAdapter
;
368 * Show software keyboard.
369 * Force the request on main thread.
371 public void showSoftKeyboard() {
372 LOKitShell
.getMainHandler().post(new Runnable() {
375 showSoftKeyboardDirect();
380 private void showSoftKeyboardDirect() {
381 LayerView layerView
= (LayerView
) findViewById(R
.id
.layer_view
);
383 if (layerView
.requestFocus()) {
384 InputMethodManager inputMethodManager
= (InputMethodManager
) getApplicationContext().getSystemService(Context
.INPUT_METHOD_SERVICE
);
385 inputMethodManager
.showSoftInput(layerView
, InputMethodManager
.SHOW_FORCED
);
391 public void showSoftKeyboardOrFormattingToolbar() {
392 LOKitShell
.getMainHandler().post(new Runnable() {
395 Toolbar toolbarBottom
= (Toolbar
) findViewById(R
.id
.toolbar_bottom
);
396 if (toolbarBottom
.getVisibility() != View
.VISIBLE
) {
397 showSoftKeyboardDirect();
404 * Hides software keyboard on UI thread.
406 public void hideSoftKeyboard() {
407 LOKitShell
.getMainHandler().post(new Runnable() {
410 hideSoftKeyboardDirect();
416 * Hides software keyboard.
418 private void hideSoftKeyboardDirect() {
419 if (getCurrentFocus() != null) {
420 InputMethodManager inputMethodManager
= (InputMethodManager
) getApplicationContext().getSystemService(Context
.INPUT_METHOD_SERVICE
);
421 inputMethodManager
.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
425 public void showBottomToolbar() {
426 LOKitShell
.getMainHandler().post(new Runnable() {
429 findViewById(R
.id
.toolbar_bottom
).setVisibility(View
.VISIBLE
);
434 public void hideBottomToolbar() {
435 LOKitShell
.getMainHandler().post(new Runnable() {
438 findViewById(R
.id
.toolbar_bottom
).setVisibility(View
.GONE
);
439 findViewById(R
.id
.formatting_toolbar
).setVisibility(View
.GONE
);
440 findViewById(R
.id
.search_toolbar
).setVisibility(View
.GONE
);
445 public void showFormattingToolbar() {
446 LOKitShell
.getMainHandler().post(new Runnable() {
450 findViewById(R
.id
.formatting_toolbar
).setVisibility(View
.VISIBLE
);
451 findViewById(R
.id
.search_toolbar
).setVisibility(View
.GONE
);
452 hideSoftKeyboardDirect();
457 public void hideFormattingToolbar() {
458 LOKitShell
.getMainHandler().post(new Runnable() {
462 findViewById(R
.id
.formatting_toolbar
).setVisibility(View
.GONE
);
467 public void showSearchToolbar() {
468 LOKitShell
.getMainHandler().post(new Runnable() {
472 findViewById(R
.id
.formatting_toolbar
).setVisibility(View
.GONE
);
473 findViewById(R
.id
.search_toolbar
).setVisibility(View
.VISIBLE
);
474 hideSoftKeyboardDirect();
479 public void hideSearchToolbar() {
480 LOKitShell
.getMainHandler().post(new Runnable() {
484 findViewById(R
.id
.search_toolbar
).setVisibility(View
.GONE
);
489 public void showProgressSpinner() {
490 findViewById(R
.id
.loadingPanel
).setVisibility(View
.VISIBLE
);
493 public void hideProgressSpinner() {
494 findViewById(R
.id
.loadingPanel
).setVisibility(View
.GONE
);
497 public void showAlertDialog(String message
) {
499 AlertDialog
.Builder alertDialogBuilder
= new AlertDialog
.Builder(LibreOfficeMainActivity
.this);
501 alertDialogBuilder
.setTitle("Error");
502 alertDialogBuilder
.setMessage(message
);
503 alertDialogBuilder
.setNeutralButton("OK", new DialogInterface
.OnClickListener() {
504 public void onClick(DialogInterface dialog
, int id
) {
509 AlertDialog alertDialog
= alertDialogBuilder
.create();
513 public DocumentOverlay
getDocumentOverlay() {
514 return mDocumentOverlay
;
517 public ToolbarController
getToolbarController() {
518 return mToolbarController
;
521 public FontController
getFontController() {
522 return mFontController
;
525 public FormattingController
getFormattingController() {
526 return mFormattingController
;
529 public void openDrawer() {
530 mDrawerLayout
.openDrawer(mDrawerList
);
533 public void showAbout() {
537 public void showSettings() {
538 startActivity(new Intent(getApplicationContext(), SettingsActivity
.class));
541 public boolean isDrawerEnabled() {
542 boolean isDrawerOpen
= mDrawerLayout
.isDrawerOpen(mDrawerList
);
543 boolean isDrawerLocked
= mDrawerLayout
.getDrawerLockMode(mDrawerList
) != DrawerLayout
.LOCK_MODE_UNLOCKED
;
544 return !isDrawerOpen
&& !isDrawerLocked
;
547 private class DocumentPartClickListener
implements android
.widget
.AdapterView
.OnItemClickListener
{
549 public void onItemClick(AdapterView
<?
> parent
, View view
, int position
, long id
) {
550 DocumentPartView partView
= mDocumentPartViewListAdapter
.getItem(position
);
551 LOKitShell
.sendChangePartEvent(partView
.partIndex
);
552 mDrawerLayout
.closeDrawer(mDrawerList
);
556 private static boolean copyFromAssets(AssetManager assetManager
,
557 String fromAssetPath
, String targetDir
) {
559 String
[] files
= assetManager
.list(fromAssetPath
);
562 for (String file
: files
) {
563 String
[] dirOrFile
= assetManager
.list(fromAssetPath
+ "/" + file
);
564 if ( dirOrFile
.length
== 0) {
565 // noinspection ResultOfMethodCallIgnored
566 new File(targetDir
).mkdirs();
567 res
&= copyAsset(assetManager
,
568 fromAssetPath
+ "/" + file
,
569 targetDir
+ "/" + file
);
571 res
&= copyFromAssets(assetManager
,
572 fromAssetPath
+ "/" + file
,
573 targetDir
+ "/" + file
);
576 } catch (Exception e
) {
578 Log
.e(LOGTAG
, "copyFromAssets failed: " + e
.getMessage());
583 private static boolean copyAsset(AssetManager assetManager
, String fromAssetPath
, String toPath
) {
584 ReadableByteChannel source
= null;
585 FileChannel dest
= null;
588 source
= Channels
.newChannel(assetManager
.open(fromAssetPath
));
589 dest
= new FileOutputStream(toPath
).getChannel();
590 long bytesTransferred
= 0;
591 // might not copy all at once, so make sure everything gets copied....
592 ByteBuffer buffer
= ByteBuffer
.allocate(4096);
593 while (source
.read(buffer
) > 0) {
595 bytesTransferred
+= dest
.write(buffer
);
598 Log
.v(LOGTAG
, "Success copying " + fromAssetPath
+ " to " + toPath
+ " bytes: " + bytesTransferred
);
601 if (dest
!= null) dest
.close();
602 if (source
!= null) source
.close();
604 } catch (FileNotFoundException e
) {
605 Log
.e(LOGTAG
, "file " + fromAssetPath
+ " not found! " + e
.getMessage());
607 } catch (IOException e
) {
608 Log
.e(LOGTAG
, "failed to copy file " + fromAssetPath
+ " from assets to " + toPath
+ " - " + e
.getMessage());
614 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */