Version 5.2.6.1, tag libreoffice-5.2.6.1
[LibreOffice.git] / android / source / src / java / org / libreoffice / LibreOfficeMainActivity.java
blob1cbdabb32edf5f36f8616c8211f97ab2593a9347
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;
34 import java.io.File;
35 import java.io.FileNotFoundException;
36 import java.io.FileOutputStream;
37 import java.io.IOException;
38 import java.net.URI;
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;
46 /**
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() {
88 return mLayerClient;
91 public static boolean isExperimentalMode() {
92 return mIsExperimentalMode;
95 public boolean usesTemporaryFile() {
96 return mTempFile != null;
99 @Override
100 public void onCreate(Bundle savedInstanceState) {
101 Log.w(LOGTAG, "onCreate..");
102 mAppContext = this;
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);
120 hideBottomToolbar();
122 mToolbarController = new ToolbarController(this, getSupportActionBar(), toolbarTop);
123 mFormattingController = new FormattingController(this, toolbarBottom);
124 toolbarTop.setNavigationOnClickListener(new View.OnClickListener() {
125 @Override
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());
139 } else {
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");
153 } else {
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();
170 } else {
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))
200 suffix = ".csv";
202 try {
203 try {
204 AssetFileDescriptor assetFD = contentResolver.openAssetFileDescriptor(getIntent().getData(), "r");
205 if (assetFD == null) {
206 Log.e(LOGTAG, "couldn't create assetfiledescriptor from " + getIntent().getDataString());
207 return false;
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");
219 return true;
220 } finally {
221 if (inputChannel != null) inputChannel.close();
222 if (outputChannel != null) outputChannel.close();
224 } catch (FileNotFoundException e) {
225 return false;
226 } catch (IOException e) {
227 return false;
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();
239 // local save
240 LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Save"));
242 final AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
243 @Override
244 protected Void doInBackground(Void... params) {
245 try {
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() {
254 @Override
255 public void run() {
256 Toast.makeText(activity, e.getMessage(),
257 Toast.LENGTH_SHORT).show();
260 Log.e(LOGTAG, e.getMessage(), e.getCause());
262 return null;
265 @Override
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;
277 @Override
278 public void run() {
279 if (lastModified < mInputFile.lastModified()) {
280 // we are sure local save is complete, push changes to cloud
281 task.execute();
283 else {
284 timesRun++;
285 if(timesRun < 4) {
286 new Handler().postDelayed(this, 5000);
288 else {
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);
299 @Override
300 protected void onResume() {
301 super.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;
310 @Override
311 protected void onPause() {
312 Log.i(LOGTAG, "onPause..");
313 super.onPause();
316 @Override
317 protected void onStart() {
318 Log.i(LOGTAG, "onStart..");
319 super.onStart();
320 LOKitShell.sendLoadEvent(mInputFile.getPath());
323 @Override
324 protected void onStop() {
325 Log.i(LOGTAG, "onStop..");
326 hideSoftKeyboardDirect();
327 LOKitShell.sendCloseEvent();
328 super.onStop();
331 @Override
332 protected void onDestroy() {
333 Log.i(LOGTAG, "onDestroy..");
334 mLayerClient.destroy();
335 super.onDestroy();
337 if (isFinishing()) { // Not an orientation change
338 if (mTempFile != null) {
339 // noinspection ResultOfMethodCallIgnored
340 mTempFile.delete();
345 public LOKitThread getLOKitThread() {
346 return sLOKitThread;
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() {
356 @Override
357 public void run() {
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() {
373 @Override
374 public void run() {
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);
388 hideBottomToolbar();
391 public void showSoftKeyboardOrFormattingToolbar() {
392 LOKitShell.getMainHandler().post(new Runnable() {
393 @Override
394 public void run() {
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() {
408 @Override
409 public void run() {
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() {
427 @Override
428 public void run() {
429 findViewById(R.id.toolbar_bottom).setVisibility(View.VISIBLE);
434 public void hideBottomToolbar() {
435 LOKitShell.getMainHandler().post(new Runnable() {
436 @Override
437 public void run() {
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() {
447 @Override
448 public void run() {
449 showBottomToolbar();
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() {
459 @Override
460 public void run() {
461 hideBottomToolbar();
462 findViewById(R.id.formatting_toolbar).setVisibility(View.GONE);
467 public void showSearchToolbar() {
468 LOKitShell.getMainHandler().post(new Runnable() {
469 @Override
470 public void run() {
471 showBottomToolbar();
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() {
481 @Override
482 public void run() {
483 hideBottomToolbar();
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) {
505 finish();
509 AlertDialog alertDialog = alertDialogBuilder.create();
510 alertDialog.show();
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() {
534 mAbout.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 {
548 @Override
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) {
558 try {
559 String[] files = assetManager.list(fromAssetPath);
561 boolean res = true;
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);
570 } else
571 res &= copyFromAssets(assetManager,
572 fromAssetPath + "/" + file,
573 targetDir + "/" + file);
575 return res;
576 } catch (Exception e) {
577 e.printStackTrace();
578 Log.e(LOGTAG, "copyFromAssets failed: " + e.getMessage());
579 return false;
583 private static boolean copyAsset(AssetManager assetManager, String fromAssetPath, String toPath) {
584 ReadableByteChannel source = null;
585 FileChannel dest = null;
586 try {
587 try {
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) {
594 buffer.flip();
595 bytesTransferred += dest.write(buffer);
596 buffer.clear();
598 Log.v(LOGTAG, "Success copying " + fromAssetPath + " to " + toPath + " bytes: " + bytesTransferred);
599 return true;
600 } finally {
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());
606 return false;
607 } catch (IOException e) {
608 Log.e(LOGTAG, "failed to copy file " + fromAssetPath + " from assets to " + toPath + " - " + e.getMessage());
609 return false;
614 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */