cid#1607171 Data race condition
[LibreOffice.git] / android / source / src / java / org / libreoffice / LibreOfficeMainActivity.java
blob505c886c4403c67bb0f171fb6adeb3740818c9f4
1 package org.libreoffice;
3 import android.app.AlertDialog;
4 import android.content.ContentResolver;
5 import android.content.Context;
6 import android.content.DialogInterface;
7 import android.content.Intent;
8 import android.content.SharedPreferences;
9 import android.content.res.AssetManager;
10 import android.graphics.RectF;
11 import android.net.Uri;
12 import android.os.Build;
13 import android.os.Bundle;
14 import android.provider.DocumentsContract;
15 import com.google.android.material.bottomsheet.BottomSheetBehavior;
16 import com.google.android.material.snackbar.Snackbar;
17 import androidx.drawerlayout.widget.DrawerLayout;
18 import androidx.appcompat.app.AppCompatActivity;
19 import androidx.appcompat.widget.Toolbar;
20 import androidx.preference.PreferenceManager;
21 import android.text.InputType;
22 import android.util.Log;
23 import android.view.KeyEvent;
24 import android.view.View;
25 import android.view.inputmethod.InputMethodManager;
26 import android.widget.AdapterView;
27 import android.widget.EditText;
28 import android.widget.LinearLayout;
29 import android.widget.ListView;
30 import android.widget.TabHost;
31 import android.widget.Toast;
33 import org.libreoffice.overlay.CalcHeadersController;
34 import org.libreoffice.overlay.DocumentOverlay;
35 import org.libreoffice.ui.FileUtilities;
36 import org.libreoffice.ui.LibreOfficeUIActivity;
37 import org.mozilla.gecko.gfx.GeckoLayerClient;
38 import org.mozilla.gecko.gfx.LayerView;
40 import java.io.File;
41 import java.io.FileInputStream;
42 import java.io.FileNotFoundException;
43 import java.io.FileOutputStream;
44 import java.io.IOException;
45 import java.io.InputStream;
46 import java.io.OutputStream;
47 import java.nio.ByteBuffer;
48 import java.nio.channels.Channels;
49 import java.nio.channels.FileChannel;
50 import java.nio.channels.ReadableByteChannel;
51 import java.util.ArrayList;
52 import java.util.List;
53 import java.util.UUID;
55 /**
56 * Main activity of the LibreOffice App. It is started in the UI thread.
58 public class LibreOfficeMainActivity extends AppCompatActivity implements SharedPreferences.OnSharedPreferenceChangeListener {
60 private static final String LOGTAG = "LibreOfficeMainActivity";
61 public static final String ENABLE_EXPERIMENTAL_PREFS_KEY = "ENABLE_EXPERIMENTAL";
62 private static final String ASSETS_EXTRACTED_PREFS_KEY = "ASSETS_EXTRACTED";
63 private static final String ENABLE_DEVELOPER_PREFS_KEY = "ENABLE_DEVELOPER";
64 private static final int REQUEST_CODE_SAVEAS = 12345;
65 private static final int REQUEST_CODE_EXPORT_TO_PDF = 12346;
67 //TODO "public static" is a temporary workaround
68 public static LOKitThread loKitThread;
70 private GeckoLayerClient mLayerClient;
72 private static boolean mIsExperimentalMode;
73 private static boolean mIsDeveloperMode;
74 private static boolean mbReadOnlyDoc;
76 private DrawerLayout mDrawerLayout;
77 Toolbar toolbarTop;
79 private ListView mDrawerList;
80 private final List<DocumentPartView> mDocumentPartView = new ArrayList<DocumentPartView>();
81 private DocumentPartViewListAdapter mDocumentPartViewListAdapter;
82 private DocumentOverlay mDocumentOverlay;
83 /** URI to save the document to. */
84 private Uri mDocumentUri;
85 /** Temporary local copy of the document. */
86 private File mTempFile = null;
87 private File mTempSlideShowFile = null;
89 BottomSheetBehavior bottomToolbarSheetBehavior;
90 BottomSheetBehavior toolbarColorPickerBottomSheetBehavior;
91 BottomSheetBehavior toolbarBackColorPickerBottomSheetBehavior;
92 private FormattingController mFormattingController;
93 private ToolbarController mToolbarController;
94 private FontController mFontController;
95 private SearchController mSearchController;
96 private UNOCommandsController mUNOCommandsController;
97 private CalcHeadersController mCalcHeadersController;
98 private LOKitTileProvider mTileProvider;
99 private String mPassword;
100 private boolean mPasswordProtected;
101 private boolean mbSkipNextRefresh;
103 public GeckoLayerClient getLayerClient() {
104 return mLayerClient;
107 public static boolean isExperimentalMode() {
108 return mIsExperimentalMode;
111 public static boolean isDeveloperMode() {
112 return mIsDeveloperMode;
115 private boolean isKeyboardOpen = false;
116 private boolean isFormattingToolbarOpen = false;
117 private boolean isSearchToolbarOpen = false;
118 private static boolean isDocumentChanged = false;
119 private boolean isUNOCommandsToolbarOpen = false;
121 @Override
122 public void onCreate(Bundle savedInstanceState) {
123 Log.w(LOGTAG, "onCreate..");
124 super.onCreate(savedInstanceState);
126 updatePreferences();
127 PreferenceManager.getDefaultSharedPreferences(getApplicationContext())
128 .registerOnSharedPreferenceChangeListener(this);
130 setContentView(R.layout.activity_main);
132 toolbarTop = findViewById(R.id.toolbar);
133 hideBottomToolbar();
135 mToolbarController = new ToolbarController(this, toolbarTop);
136 mFormattingController = new FormattingController(this);
137 toolbarTop.setNavigationOnClickListener(new View.OnClickListener() {
138 @Override
139 public void onClick(View view) {
140 LOKitShell.sendNavigationClickEvent();
144 mFontController = new FontController(this);
145 mSearchController = new SearchController(this);
146 mUNOCommandsController = new UNOCommandsController(this);
148 loKitThread = new LOKitThread(this);
149 loKitThread.start();
151 mLayerClient = new GeckoLayerClient(this);
152 LayerView layerView = findViewById(R.id.layer_view);
153 mLayerClient.setView(layerView);
154 layerView.setInputConnectionHandler(new LOKitInputConnectionHandler());
155 mLayerClient.notifyReady();
157 layerView.setOnKeyListener(new View.OnKeyListener() {
158 @Override
159 public boolean onKey(View view, int i, KeyEvent keyEvent) {
160 if(!isReadOnlyMode() && keyEvent.getKeyCode() != KeyEvent.KEYCODE_BACK){
161 setDocumentChanged(true);
163 return false;
167 // create TextCursorLayer
168 mDocumentOverlay = new DocumentOverlay(this, layerView);
170 mbReadOnlyDoc = false;
172 final Uri docUri = getIntent().getData();
173 if (docUri != null) {
174 if (docUri.getScheme().equals(ContentResolver.SCHEME_CONTENT)
175 || docUri.getScheme().equals(ContentResolver.SCHEME_ANDROID_RESOURCE)) {
176 mbReadOnlyDoc = (getIntent().getFlags() & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) == 0;
177 Log.d(LOGTAG, "SCHEME_CONTENT: getPath(): " + docUri.getPath());
179 String displayName = FileUtilities.retrieveDisplayNameForDocumentUri(getContentResolver(), docUri);
180 toolbarTop.setTitle(displayName);
182 } else if (docUri.getScheme().equals(ContentResolver.SCHEME_FILE)) {
183 mbReadOnlyDoc = true;
184 Log.d(LOGTAG, "SCHEME_FILE: getPath(): " + docUri.getPath());
185 toolbarTop.setTitle(docUri.getLastPathSegment());
187 // create a temporary local copy to work with
188 boolean copyOK = copyFileToTemp(docUri) && mTempFile != null;
189 if (!copyOK) {
190 // TODO: can't open the file
191 Log.e(LOGTAG, "couldn't create temporary file from " + docUri);
192 return;
195 // if input doc is a template, a new doc is created and a proper URI to save to
196 // will only be available after a "Save As"
197 if (isTemplate(docUri)) {
198 toolbarTop.setTitle(R.string.default_document_name);
199 } else {
200 mDocumentUri = docUri;
203 LOKitShell.sendLoadEvent(mTempFile.getPath());
204 } else if (getIntent().getStringExtra(LibreOfficeUIActivity.NEW_DOC_TYPE_KEY) != null) {
205 // New document type string is not null, meaning we want to open a new document
206 String newDocumentType = getIntent().getStringExtra(LibreOfficeUIActivity.NEW_DOC_TYPE_KEY);
207 // create a temporary local file, will be copied to the actual URI when saving
208 loadNewDocument(newDocumentType);
209 toolbarTop.setTitle(getString(R.string.default_document_name));
210 } else {
211 Log.e(LOGTAG, "No document specified. This should never happen.");
212 return;
214 // the loadDocument/loadNewDocument event already triggers a refresh as well,
215 // so there's no need to do another refresh in 'onStart'
216 mbSkipNextRefresh = true;
218 mDrawerLayout = findViewById(R.id.drawer_layout);
220 if (mDocumentPartViewListAdapter == null) {
221 mDrawerList = findViewById(R.id.left_drawer);
223 mDocumentPartViewListAdapter = new DocumentPartViewListAdapter(this, R.layout.document_part_list_layout, mDocumentPartView);
224 mDrawerList.setAdapter(mDocumentPartViewListAdapter);
225 mDrawerList.setOnItemClickListener(new DocumentPartClickListener());
228 mToolbarController.setupToolbars();
230 TabHost host = findViewById(R.id.toolbarTabHost);
231 host.setup();
233 TabHost.TabSpec spec = host.newTabSpec(getString(R.string.tabhost_character));
234 spec.setContent(R.id.tab_character);
235 spec.setIndicator(getString(R.string.tabhost_character));
236 host.addTab(spec);
238 spec = host.newTabSpec(getString(R.string.tabhost_paragraph));
239 spec.setContent(R.id.tab_paragraph);
240 spec.setIndicator(getString(R.string.tabhost_paragraph));
241 host.addTab(spec);
243 spec = host.newTabSpec(getString(R.string.tabhost_insert));
244 spec.setContent(R.id.tab_insert);
245 spec.setIndicator(getString(R.string.tabhost_insert));
246 host.addTab(spec);
248 spec = host.newTabSpec(getString(R.string.tabhost_style));
249 spec.setContent(R.id.tab_style);
250 spec.setIndicator(getString(R.string.tabhost_style));
251 host.addTab(spec);
253 LinearLayout bottomToolbarLayout = findViewById(R.id.toolbar_bottom);
254 LinearLayout toolbarColorPickerLayout = findViewById(R.id.toolbar_color_picker);
255 LinearLayout toolbarBackColorPickerLayout = findViewById(R.id.toolbar_back_color_picker);
256 bottomToolbarSheetBehavior = BottomSheetBehavior.from(bottomToolbarLayout);
257 toolbarColorPickerBottomSheetBehavior = BottomSheetBehavior.from(toolbarColorPickerLayout);
258 toolbarBackColorPickerBottomSheetBehavior = BottomSheetBehavior.from(toolbarBackColorPickerLayout);
259 bottomToolbarSheetBehavior.setHideable(true);
260 toolbarColorPickerBottomSheetBehavior.setHideable(true);
261 toolbarBackColorPickerBottomSheetBehavior.setHideable(true);
264 private void updatePreferences() {
265 SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
266 mIsExperimentalMode = BuildConfig.ALLOW_EDITING
267 && sPrefs.getBoolean(ENABLE_EXPERIMENTAL_PREFS_KEY, false);
268 mIsDeveloperMode = mIsExperimentalMode
269 && sPrefs.getBoolean(ENABLE_DEVELOPER_PREFS_KEY, false);
270 if (sPrefs.getInt(ASSETS_EXTRACTED_PREFS_KEY, 0) != BuildConfig.VERSION_CODE) {
271 if(copyFromAssets(getAssets(), "unpack", getApplicationInfo().dataDir)) {
272 sPrefs.edit().putInt(ASSETS_EXTRACTED_PREFS_KEY, BuildConfig.VERSION_CODE).apply();
277 // Loads a new Document and saves it to a temporary file
278 private void loadNewDocument(String newDocumentType) {
279 String tempFileName = "LibreOffice_" + UUID.randomUUID().toString();
280 mTempFile = new File(this.getCacheDir(), tempFileName);
281 LOKitShell.sendNewDocumentLoadEvent(mTempFile.getPath(), newDocumentType);
284 public RectF getCurrentCursorPosition() {
285 return mDocumentOverlay.getCurrentCursorPosition();
288 private boolean copyFileToTemp(Uri documentUri) {
289 // CSV files need a .csv suffix to be opened in Calc.
290 String suffix = null;
291 String intentType = getIntent().getType();
292 // K-9 mail uses the first, GMail uses the second variant.
293 if ("text/comma-separated-values".equals(intentType) || "text/csv".equals(intentType))
294 suffix = ".csv";
296 try {
297 mTempFile = File.createTempFile("LibreOffice", suffix, this.getCacheDir());
298 final FileOutputStream outputStream = new FileOutputStream(mTempFile);
299 return copyUriToStream(documentUri, outputStream);
300 } catch (FileNotFoundException e) {
301 return false;
302 } catch (IOException e) {
303 return false;
308 * Save the document.
310 public void saveDocument() {
311 Toast.makeText(this, R.string.message_saving, Toast.LENGTH_SHORT).show();
312 // local save
313 LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND_NOTIFY, ".uno:Save", true));
317 * Open file chooser and save the document to the URI
318 * selected there.
320 public void saveDocumentAs() {
321 Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
322 intent.addCategory(Intent.CATEGORY_OPENABLE);
323 String mimeType = getODFMimeTypeForDocument();
324 intent.setType(mimeType);
325 if (Build.VERSION.SDK_INT >= 26) {
326 intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, mDocumentUri);
329 startActivityForResult(intent, REQUEST_CODE_SAVEAS);
333 * Saves the document under the given URI using ODF format
334 * and uses that URI from now on for all operations.
335 * @param newUri URI to save the document and use from now on.
337 private void saveDocumentAs(Uri newUri) {
338 mDocumentUri = newUri;
339 // save in ODF format
340 mTileProvider.saveDocumentAs(mTempFile.getPath(), true);
341 saveFileToOriginalSource();
343 String displayName = FileUtilities.retrieveDisplayNameForDocumentUri(getContentResolver(), mDocumentUri);
344 toolbarTop.setTitle(displayName);
345 mbReadOnlyDoc = false;
346 getToolbarController().setupToolbars();
349 public void exportToPDF() {
350 Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
351 intent.addCategory(Intent.CATEGORY_OPENABLE);
352 intent.setType(FileUtilities.MIMETYPE_PDF);
353 // suggest directory and file name based on the doc
354 if (Build.VERSION.SDK_INT >= 26) {
355 intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, mDocumentUri);
357 final String displayName = toolbarTop.getTitle().toString();
358 final String suggestedFileName = FileUtilities.stripExtensionFromFileName(displayName) + ".pdf";
359 intent.putExtra(Intent.EXTRA_TITLE, suggestedFileName);
361 startActivityForResult(intent, REQUEST_CODE_EXPORT_TO_PDF);
364 private void exportToPDF(final Uri uri) {
365 boolean exportOK = false;
366 File tempFile = null;
367 try {
368 tempFile = File.createTempFile("LibreOffice_", ".pdf");
369 mTileProvider.saveDocumentAs(tempFile.getAbsolutePath(),"pdf", false);
371 try {
372 FileInputStream inputStream = new FileInputStream(tempFile);
373 exportOK = copyStreamToUri(inputStream, uri);
374 } catch (FileNotFoundException e) {
375 e.printStackTrace();
378 } catch (IOException e) {
379 e.printStackTrace();
380 } finally {
381 if (tempFile != null && tempFile.exists()) {
382 tempFile.delete();
386 final int msgId = exportOK ? R.string.pdf_export_finished : R.string.unable_to_export_pdf;
387 LOKitShell.getMainHandler().post(new Runnable() {
388 @Override
389 public void run() {
390 showCustomStatusMessage(getString(msgId));
396 * Returns the ODF MIME type that can be used for the current document,
397 * regardless of whether the document is an ODF Document or not
398 * (e.g. returns FileUtilities.MIMETYPE_OPENDOCUMENT_TEXT for a DOCX file).
399 * @return MIME type, or empty string, if no appropriate MIME type could be found.
401 private String getODFMimeTypeForDocument() {
402 if (mTileProvider.isTextDocument())
403 return FileUtilities.MIMETYPE_OPENDOCUMENT_TEXT;
404 else if (mTileProvider.isSpreadsheet())
405 return FileUtilities.MIMETYPE_OPENDOCUMENT_SPREADSHEET;
406 else if (mTileProvider.isPresentation())
407 return FileUtilities.MIMETYPE_OPENDOCUMENT_PRESENTATION;
408 else if (mTileProvider.isDrawing())
409 return FileUtilities.MIMETYPE_OPENDOCUMENT_GRAPHICS;
410 else {
411 Log.w(LOGTAG, "Cannot determine MIME type to use.");
412 return "";
417 * Returns whether the MIME type for the URI is considered one for a document template.
419 private boolean isTemplate(final Uri documentUri) {
420 final String mimeType = getContentResolver().getType(documentUri);
421 return FileUtilities.isTemplateMimeType(mimeType);
424 public void saveFileToOriginalSource() {
425 if (mTempFile == null || mDocumentUri == null || !mDocumentUri.getScheme().equals(ContentResolver.SCHEME_CONTENT))
426 return;
428 boolean copyOK = false;
429 try {
430 final FileInputStream inputStream = new FileInputStream(mTempFile);
431 copyOK = copyStreamToUri(inputStream, mDocumentUri);
432 } catch (FileNotFoundException e) {
433 e.printStackTrace();
435 if (copyOK) {
436 runOnUiThread(new Runnable() {
437 @Override
438 public void run() {
439 Toast.makeText(LibreOfficeMainActivity.this, R.string.message_saved,
440 Toast.LENGTH_SHORT).show();
443 setDocumentChanged(false);
444 } else {
445 runOnUiThread(new Runnable() {
446 @Override
447 public void run() {
448 Toast.makeText(LibreOfficeMainActivity.this, R.string.message_saving_failed,
449 Toast.LENGTH_SHORT).show();
455 @Override
456 protected void onResume() {
457 super.onResume();
458 Log.i(LOGTAG, "onResume..");
459 if (mToolbarController.getEditModeStatus() && isExperimentalMode()) {
460 mToolbarController.switchToEditMode();
461 } else {
462 mToolbarController.switchToViewMode();
466 @Override
467 protected void onPause() {
468 Log.i(LOGTAG, "onPause..");
469 super.onPause();
472 @Override
473 protected void onStart() {
474 Log.i(LOGTAG, "onStart..");
475 super.onStart();
476 if (!mbSkipNextRefresh) {
477 LOKitShell.sendEvent(new LOEvent(LOEvent.REFRESH));
479 mbSkipNextRefresh = false;
482 @Override
483 protected void onStop() {
484 Log.i(LOGTAG, "onStop..");
485 hideSoftKeyboardDirect();
486 super.onStop();
489 @Override
490 protected void onDestroy() {
491 Log.i(LOGTAG, "onDestroy..");
492 PreferenceManager.getDefaultSharedPreferences(getApplicationContext())
493 .unregisterOnSharedPreferenceChangeListener(this);
495 LOKitShell.sendCloseEvent();
496 mLayerClient.destroy();
497 super.onDestroy();
499 if (isFinishing()) { // Not an orientation change
500 if (mTempFile != null) {
501 // noinspection ResultOfMethodCallIgnored
502 mTempFile.delete();
504 if (mTempSlideShowFile != null && mTempSlideShowFile.exists()) {
505 // noinspection ResultOfMethodCallIgnored
506 mTempSlideShowFile.delete();
510 @Override
511 public void onBackPressed() {
512 if (!isDocumentChanged) {
513 super.onBackPressed();
514 return;
518 DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
519 @Override
520 public void onClick(DialogInterface dialog, int which) {
521 switch (which){
522 case DialogInterface.BUTTON_POSITIVE:
523 mTileProvider.saveDocument();
524 isDocumentChanged=false;
525 onBackPressed();
526 break;
527 case DialogInterface.BUTTON_NEGATIVE:
528 //CANCEL
529 break;
530 case DialogInterface.BUTTON_NEUTRAL:
531 //NO
532 isDocumentChanged=false;
533 onBackPressed();
534 break;
539 AlertDialog.Builder builder = new AlertDialog.Builder(this);
540 builder.setMessage(R.string.save_alert_dialog_title)
541 .setPositiveButton(R.string.save_document, dialogClickListener)
542 .setNegativeButton(R.string.action_cancel, dialogClickListener)
543 .setNeutralButton(R.string.no_save_document, dialogClickListener)
544 .show();
548 public List<DocumentPartView> getDocumentPartView() {
549 return mDocumentPartView;
552 public void disableNavigationDrawer() {
553 // Only the original thread that created mDrawerLayout should touch its views.
554 LOKitShell.getMainHandler().post(new Runnable() {
555 @Override
556 public void run() {
557 mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, mDrawerList);
562 public DocumentPartViewListAdapter getDocumentPartViewListAdapter() {
563 return mDocumentPartViewListAdapter;
567 * Show software keyboard.
568 * Force the request on main thread.
570 public void showSoftKeyboard() {
572 LOKitShell.getMainHandler().post(new Runnable() {
573 @Override
574 public void run() {
575 if(!isKeyboardOpen) showSoftKeyboardDirect();
576 else hideSoftKeyboardDirect();
582 private void showSoftKeyboardDirect() {
583 LayerView layerView = findViewById(R.id.layer_view);
585 if (layerView.requestFocus()) {
586 InputMethodManager inputMethodManager = (InputMethodManager) getApplicationContext().getSystemService(Context.INPUT_METHOD_SERVICE);
587 inputMethodManager.showSoftInput(layerView, InputMethodManager.SHOW_FORCED);
589 isKeyboardOpen=true;
590 isSearchToolbarOpen=false;
591 isFormattingToolbarOpen=false;
592 isUNOCommandsToolbarOpen=false;
593 hideBottomToolbar();
596 public void showSoftKeyboardOrFormattingToolbar() {
597 LOKitShell.getMainHandler().post(new Runnable() {
598 @Override
599 public void run() {
600 if (findViewById(R.id.toolbar_bottom).getVisibility() != View.VISIBLE
601 && findViewById(R.id.toolbar_color_picker).getVisibility() != View.VISIBLE) {
602 showSoftKeyboardDirect();
609 * Hides software keyboard on UI thread.
611 public void hideSoftKeyboard() {
612 LOKitShell.getMainHandler().post(new Runnable() {
613 @Override
614 public void run() {
615 hideSoftKeyboardDirect();
621 * Hides software keyboard.
623 private void hideSoftKeyboardDirect() {
624 if (getCurrentFocus() != null) {
625 InputMethodManager inputMethodManager = (InputMethodManager) getApplicationContext().getSystemService(Context.INPUT_METHOD_SERVICE);
626 inputMethodManager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
627 isKeyboardOpen=false;
631 public void showBottomToolbar() {
632 LOKitShell.getMainHandler().post(new Runnable() {
633 @Override
634 public void run() {
635 bottomToolbarSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
640 public void hideBottomToolbar() {
641 LOKitShell.getMainHandler().post(new Runnable() {
642 @Override
643 public void run() {
644 bottomToolbarSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
645 toolbarColorPickerBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
646 toolbarBackColorPickerBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
647 findViewById(R.id.search_toolbar).setVisibility(View.GONE);
648 findViewById(R.id.UNO_commands_toolbar).setVisibility(View.GONE);
649 isFormattingToolbarOpen=false;
650 isSearchToolbarOpen=false;
651 isUNOCommandsToolbarOpen=false;
656 public void showFormattingToolbar() {
657 LOKitShell.getMainHandler().post(new Runnable() {
658 @Override
659 public void run() {
660 if (isFormattingToolbarOpen) {
661 hideFormattingToolbar();
662 } else {
663 showBottomToolbar();
664 findViewById(R.id.search_toolbar).setVisibility(View.GONE);
665 findViewById(R.id.formatting_toolbar).setVisibility(View.VISIBLE);
666 findViewById(R.id.search_toolbar).setVisibility(View.GONE);
667 findViewById(R.id.UNO_commands_toolbar).setVisibility(View.GONE);
668 hideSoftKeyboardDirect();
669 isSearchToolbarOpen=false;
670 isFormattingToolbarOpen=true;
671 isUNOCommandsToolbarOpen=false;
678 public void hideFormattingToolbar() {
679 LOKitShell.getMainHandler().post(new Runnable() {
680 @Override
681 public void run() {
682 hideBottomToolbar();
687 public void showSearchToolbar() {
688 LOKitShell.getMainHandler().post(new Runnable() {
689 @Override
690 public void run() {
691 if (isSearchToolbarOpen) {
692 hideSearchToolbar();
693 } else {
694 showBottomToolbar();
695 findViewById(R.id.formatting_toolbar).setVisibility(View.GONE);
696 toolbarColorPickerBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
697 toolbarBackColorPickerBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
698 findViewById(R.id.search_toolbar).setVisibility(View.VISIBLE);
699 findViewById(R.id.UNO_commands_toolbar).setVisibility(View.GONE);
700 hideSoftKeyboardDirect();
701 isFormattingToolbarOpen=false;
702 isSearchToolbarOpen=true;
703 isUNOCommandsToolbarOpen=false;
709 public void hideSearchToolbar() {
710 LOKitShell.getMainHandler().post(new Runnable() {
711 @Override
712 public void run() {
713 hideBottomToolbar();
718 public void showUNOCommandsToolbar() {
719 LOKitShell.getMainHandler().post(new Runnable() {
720 @Override
721 public void run() {
722 if(isUNOCommandsToolbarOpen){
723 hideUNOCommandsToolbar();
724 }else{
725 showBottomToolbar();
726 findViewById(R.id.formatting_toolbar).setVisibility(View.GONE);
727 findViewById(R.id.search_toolbar).setVisibility(View.GONE);
728 findViewById(R.id.UNO_commands_toolbar).setVisibility(View.VISIBLE);
729 hideSoftKeyboardDirect();
730 isFormattingToolbarOpen=false;
731 isSearchToolbarOpen=false;
732 isUNOCommandsToolbarOpen=true;
738 public void hideUNOCommandsToolbar() {
739 LOKitShell.getMainHandler().post(new Runnable() {
740 @Override
741 public void run() {
742 hideBottomToolbar();
747 public void showProgressSpinner() {
748 findViewById(R.id.loadingPanel).setVisibility(View.VISIBLE);
751 public void hideProgressSpinner() {
752 findViewById(R.id.loadingPanel).setVisibility(View.GONE);
755 public void showAlertDialog(String message) {
757 AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(LibreOfficeMainActivity.this);
759 alertDialogBuilder.setTitle(R.string.error);
760 alertDialogBuilder.setMessage(message);
761 alertDialogBuilder.setNeutralButton(R.string.alert_ok, new DialogInterface.OnClickListener() {
762 public void onClick(DialogInterface dialog, int id) {
763 finish();
767 AlertDialog alertDialog = alertDialogBuilder.create();
768 alertDialog.show();
771 public DocumentOverlay getDocumentOverlay() {
772 return mDocumentOverlay;
775 public CalcHeadersController getCalcHeadersController() {
776 return mCalcHeadersController;
779 public ToolbarController getToolbarController() {
780 return mToolbarController;
783 public FontController getFontController() {
784 return mFontController;
787 public FormattingController getFormattingController() {
788 return mFormattingController;
791 public void openDrawer() {
792 mDrawerLayout.openDrawer(mDrawerList);
793 hideBottomToolbar();
796 public void showAbout() {
797 AboutDialogFragment aboutDialogFragment = new AboutDialogFragment();
798 aboutDialogFragment.show(getSupportFragmentManager(), "AboutDialogFragment");
801 public void addPart(){
802 mTileProvider.addPart();
803 mDocumentPartViewListAdapter.notifyDataSetChanged();
804 setDocumentChanged(true);
807 public void renamePart(){
808 AlertDialog.Builder builder = new AlertDialog.Builder(this);
809 builder.setTitle(R.string.enter_part_name);
810 final EditText input = new EditText(this);
811 input.setInputType(InputType.TYPE_CLASS_TEXT);
812 builder.setView(input);
814 builder.setPositiveButton(R.string.alert_ok, new DialogInterface.OnClickListener() {
815 @Override
816 public void onClick(DialogInterface dialog, int which) {
817 mTileProvider.renamePart( input.getText().toString());
820 builder.setNegativeButton(R.string.alert_cancel, new DialogInterface.OnClickListener() {
821 @Override
822 public void onClick(DialogInterface dialog, int which) {
823 dialog.cancel();
827 builder.show();
830 public void deletePart() {
831 mTileProvider.removePart();
834 public void showSettings() {
835 startActivity(new Intent(getApplicationContext(), SettingsActivity.class));
838 public boolean isDrawerEnabled() {
839 boolean isDrawerOpen = mDrawerLayout.isDrawerOpen(mDrawerList);
840 boolean isDrawerLocked = mDrawerLayout.getDrawerLockMode(mDrawerList) != DrawerLayout.LOCK_MODE_UNLOCKED;
841 return !isDrawerOpen && !isDrawerLocked;
844 @Override
845 public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
846 updatePreferences();
849 public void promptForPassword() {
850 PasswordDialogFragment passwordDialogFragment = new PasswordDialogFragment();
851 passwordDialogFragment.setLOMainActivity(this);
852 passwordDialogFragment.show(getSupportFragmentManager(), "PasswordDialogFragment");
855 // this function can only be called in InvalidationHandler.java
856 public void setPassword() {
857 mTileProvider.setDocumentPassword("file://" + mTempFile.getPath(), mPassword);
860 // setTileProvider is meant to let main activity have a handle of LOKit when dealing with password
861 public void setTileProvider(LOKitTileProvider loKitTileProvider) {
862 mTileProvider = loKitTileProvider;
865 public LOKitTileProvider getTileProvider() {
866 return mTileProvider;
869 public void savePassword(String pwd) {
870 mPassword = pwd;
871 synchronized (mTileProvider.getMessageCallback()) {
872 mTileProvider.getMessageCallback().notifyAll();
876 public void setPasswordProtected(boolean b) {
877 mPasswordProtected = b;
880 public boolean isPasswordProtected() {
881 return mPasswordProtected;
884 public void initializeCalcHeaders() {
885 mCalcHeadersController = new CalcHeadersController(this, mLayerClient.getView());
886 mCalcHeadersController.setupHeaderPopupView();
887 LOKitShell.getMainHandler().post(new Runnable() {
888 @Override
889 public void run() {
890 findViewById(R.id.calc_header_top_left).setVisibility(View.VISIBLE);
891 findViewById(R.id.calc_header_row).setVisibility(View.VISIBLE);
892 findViewById(R.id.calc_header_column).setVisibility(View.VISIBLE);
893 findViewById(R.id.calc_address).setVisibility(View.VISIBLE);
894 findViewById(R.id.calc_formula).setVisibility(View.VISIBLE);
899 public static boolean isReadOnlyMode() {
900 return !isExperimentalMode() || mbReadOnlyDoc;
903 public boolean hasLocationForSave() {
904 return mDocumentUri != null;
907 public static void setDocumentChanged (boolean changed) {
908 isDocumentChanged = changed;
911 private class DocumentPartClickListener implements android.widget.AdapterView.OnItemClickListener {
912 @Override
913 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
914 DocumentPartView partView = mDocumentPartViewListAdapter.getItem(position);
915 LOKitShell.sendChangePartEvent(partView.partIndex);
916 mDrawerLayout.closeDrawer(mDrawerList);
920 private static boolean copyFromAssets(AssetManager assetManager,
921 String fromAssetPath, String targetDir) {
922 try {
923 String[] files = assetManager.list(fromAssetPath);
925 boolean res = true;
926 for (String file : files) {
927 String[] dirOrFile = assetManager.list(fromAssetPath + "/" + file);
928 if ( dirOrFile.length == 0) {
929 // noinspection ResultOfMethodCallIgnored
930 new File(targetDir).mkdirs();
931 res &= copyAsset(assetManager,
932 fromAssetPath + "/" + file,
933 targetDir + "/" + file);
934 } else
935 res &= copyFromAssets(assetManager,
936 fromAssetPath + "/" + file,
937 targetDir + "/" + file);
939 return res;
940 } catch (Exception e) {
941 e.printStackTrace();
942 Log.e(LOGTAG, "copyFromAssets failed: " + e.getMessage());
943 return false;
947 private static boolean copyAsset(AssetManager assetManager, String fromAssetPath, String toPath) {
948 ReadableByteChannel source = null;
949 FileChannel dest = null;
950 try {
951 try {
952 source = Channels.newChannel(assetManager.open(fromAssetPath));
953 dest = new FileOutputStream(toPath).getChannel();
954 long bytesTransferred = 0;
955 // might not copy all at once, so make sure everything gets copied...
956 ByteBuffer buffer = ByteBuffer.allocate(4096);
957 while (source.read(buffer) > 0) {
958 buffer.flip();
959 bytesTransferred += dest.write(buffer);
960 buffer.clear();
962 Log.v(LOGTAG, "Success copying " + fromAssetPath + " to " + toPath + " bytes: " + bytesTransferred);
963 return true;
964 } finally {
965 if (dest != null) dest.close();
966 if (source != null) source.close();
968 } catch (FileNotFoundException e) {
969 Log.e(LOGTAG, "file " + fromAssetPath + " not found! " + e.getMessage());
970 return false;
971 } catch (IOException e) {
972 Log.e(LOGTAG, "failed to copy file " + fromAssetPath + " from assets to " + toPath + " - " + e.getMessage());
973 return false;
978 * Copies everything from the given input stream to the given output stream
979 * and closes both streams in the end.
980 * @return Whether copy operation was successful.
982 private boolean copyStream(InputStream inputStream, OutputStream outputStream) {
983 try {
984 byte[] buffer = new byte[4096];
985 int readBytes = inputStream.read(buffer);
986 while (readBytes != -1) {
987 outputStream.write(buffer, 0, readBytes);
988 readBytes = inputStream.read(buffer);
990 return true;
991 } catch (IOException e) {
992 e.printStackTrace();
993 return false;
994 } finally {
995 try {
996 inputStream.close();
997 outputStream.close();
998 } catch (IOException e) {
999 e.printStackTrace();
1005 * Copies everything from the given Uri to the given OutputStream
1006 * and closes the OutputStream in the end.
1007 * The copy operation runs in a separate thread, but the method only returns
1008 * after the thread has finished its execution.
1009 * This can be used to copy in a blocking way when network access is involved,
1010 * which is not allowed from the main thread, but that may happen when an underlying
1011 * DocumentsProvider (like the NextCloud one) does network access.
1013 private boolean copyUriToStream(final Uri inputUri, final OutputStream outputStream) {
1014 class CopyThread extends Thread {
1015 /** Whether copy operation was successful. */
1016 private boolean result = false;
1018 @Override
1019 public void run() {
1020 final ContentResolver contentResolver = getContentResolver();
1021 try {
1022 InputStream inputStream = contentResolver.openInputStream(inputUri);
1023 result = copyStream(inputStream, outputStream);
1024 } catch (FileNotFoundException e) {
1025 e.printStackTrace();
1029 CopyThread copyThread = new CopyThread();
1030 copyThread.start();
1031 try {
1032 // wait for copy operation to finish
1033 // NOTE: might be useful to add some indicator in UI for long copy operations involving network...
1034 copyThread.join();
1035 } catch(InterruptedException e) {
1036 e.printStackTrace();
1038 return copyThread.result;
1042 * Copies everything from the given InputStream to the given URI and closes the
1043 * InputStream in the end.
1044 * @see LibreOfficeMainActivity#copyUriToStream(Uri, OutputStream)
1045 * which does the same thing the other way around.
1047 private boolean copyStreamToUri(final InputStream inputStream, final Uri outputUri) {
1048 class CopyThread extends Thread {
1049 /** Whether copy operation was successful. */
1050 private boolean result = false;
1052 @Override
1053 public void run() {
1054 final ContentResolver contentResolver = getContentResolver();
1055 try {
1056 OutputStream outputStream = contentResolver.openOutputStream(outputUri);
1057 result = copyStream(inputStream, outputStream);
1058 } catch (FileNotFoundException e) {
1059 e.printStackTrace();
1063 CopyThread copyThread = new CopyThread();
1064 copyThread.start();
1065 try {
1066 // wait for copy operation to finish
1067 // NOTE: might be useful to add some indicator in UI for long copy operations involving network...
1068 copyThread.join();
1069 } catch(InterruptedException e) {
1070 e.printStackTrace();
1072 return copyThread.result;
1075 public void showCustomStatusMessage(String message){
1076 Snackbar.make(mDrawerLayout, message, Snackbar.LENGTH_LONG).show();
1079 public void preparePresentation() {
1080 if (getExternalCacheDir() != null) {
1081 String tempPath = getExternalCacheDir().getPath() + "/" + mTempFile.getName() + ".svg";
1082 mTempSlideShowFile = new File(tempPath);
1083 if (mTempSlideShowFile.exists() && !isDocumentChanged) {
1084 startPresentation("file://" + tempPath);
1085 } else {
1086 LOKitShell.sendSaveCopyAsEvent(tempPath, "svg");
1091 public void startPresentation(String tempPath) {
1092 Intent intent = new Intent(this, PresentationActivity.class);
1093 intent.setData(Uri.parse(tempPath));
1094 startActivity(intent);
1097 @Override
1098 protected void onActivityResult(int requestCode, int resultCode, Intent data) {
1099 super.onActivityResult(requestCode, resultCode, data);
1100 if (requestCode == REQUEST_CODE_SAVEAS && resultCode == RESULT_OK) {
1101 final Uri fileUri = data.getData();
1102 saveDocumentAs(fileUri);
1103 } else if (requestCode == REQUEST_CODE_EXPORT_TO_PDF && resultCode == RESULT_OK) {
1104 final Uri fileUri = data.getData();
1105 exportToPDF(fileUri);
1106 } else {
1107 mFormattingController.handleActivityResult(requestCode, resultCode, data);
1108 hideBottomToolbar();
1113 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */