Avoid potential negative array index access to cached text.
[LibreOffice.git] / android / source / src / java / org / libreoffice / LibreOfficeMainActivity.java
blob23bf8d27b6c3c0d793f50c426cc3c624ec636f4e
1 package org.libreoffice;
3 import android.app.AlertDialog;
4 import android.content.ClipData;
5 import android.content.ClipboardManager;
6 import android.content.ContentResolver;
7 import android.content.Context;
8 import android.content.DialogInterface;
9 import android.content.Intent;
10 import android.content.SharedPreferences;
11 import android.content.res.AssetManager;
12 import android.graphics.RectF;
13 import android.net.Uri;
14 import android.os.Build;
15 import android.os.Bundle;
16 import android.preference.PreferenceManager;
17 import android.provider.DocumentsContract;
18 import com.google.android.material.bottomsheet.BottomSheetBehavior;
19 import com.google.android.material.snackbar.Snackbar;
20 import androidx.drawerlayout.widget.DrawerLayout;
21 import androidx.appcompat.app.AppCompatActivity;
22 import androidx.appcompat.widget.Toolbar;
23 import android.text.InputType;
24 import android.util.Log;
25 import android.view.KeyEvent;
26 import android.view.View;
27 import android.view.inputmethod.InputMethodManager;
28 import android.widget.AdapterView;
29 import android.widget.EditText;
30 import android.widget.LinearLayout;
31 import android.widget.ListView;
32 import android.widget.TabHost;
33 import android.widget.Toast;
35 import org.libreoffice.overlay.CalcHeadersController;
36 import org.libreoffice.overlay.DocumentOverlay;
37 import org.libreoffice.ui.FileUtilities;
38 import org.libreoffice.ui.LibreOfficeUIActivity;
39 import org.mozilla.gecko.gfx.GeckoLayerClient;
40 import org.mozilla.gecko.gfx.LayerView;
42 import java.io.File;
43 import java.io.FileInputStream;
44 import java.io.FileNotFoundException;
45 import java.io.FileOutputStream;
46 import java.io.IOException;
47 import java.io.InputStream;
48 import java.io.OutputStream;
49 import java.nio.ByteBuffer;
50 import java.nio.channels.Channels;
51 import java.nio.channels.FileChannel;
52 import java.nio.channels.ReadableByteChannel;
53 import java.util.ArrayList;
54 import java.util.List;
55 import java.util.UUID;
57 /**
58 * Main activity of the LibreOffice App. It is started in the UI thread.
60 public class LibreOfficeMainActivity extends AppCompatActivity implements SettingsListenerModel.OnSettingsPreferenceChangedListener {
62 private static final String LOGTAG = "LibreOfficeMainActivity";
63 public static final String ENABLE_EXPERIMENTAL_PREFS_KEY = "ENABLE_EXPERIMENTAL";
64 private static final String ASSETS_EXTRACTED_PREFS_KEY = "ASSETS_EXTRACTED";
65 private static final String ENABLE_DEVELOPER_PREFS_KEY = "ENABLE_DEVELOPER";
66 private static final int REQUEST_CODE_SAVEAS = 12345;
67 private static final int REQUEST_CODE_EXPORT_TO_PDF = 12346;
69 //TODO "public static" is a temporary workaround
70 public static LOKitThread loKitThread;
72 private GeckoLayerClient mLayerClient;
74 private static boolean mIsExperimentalMode;
75 private static boolean mIsDeveloperMode;
76 private static boolean mbISReadOnlyMode;
78 private DrawerLayout mDrawerLayout;
79 Toolbar toolbarTop;
81 private ListView mDrawerList;
82 private final List<DocumentPartView> mDocumentPartView = new ArrayList<DocumentPartView>();
83 private DocumentPartViewListAdapter mDocumentPartViewListAdapter;
84 private DocumentOverlay mDocumentOverlay;
85 /** URI to save the document to. */
86 private Uri mDocumentUri;
87 /** Temporary local copy of the document. */
88 private File mTempFile = null;
89 private File mTempSlideShowFile = null;
91 BottomSheetBehavior bottomToolbarSheetBehavior;
92 BottomSheetBehavior toolbarColorPickerBottomSheetBehavior;
93 BottomSheetBehavior toolbarBackColorPickerBottomSheetBehavior;
94 private FormattingController mFormattingController;
95 private ToolbarController mToolbarController;
96 private FontController mFontController;
97 private SearchController mSearchController;
98 private UNOCommandsController mUNOCommandsController;
99 private CalcHeadersController mCalcHeadersController;
100 private LOKitTileProvider mTileProvider;
101 private String mPassword;
102 private boolean mPasswordProtected;
103 private boolean mbSkipNextRefresh;
105 public GeckoLayerClient getLayerClient() {
106 return mLayerClient;
109 public static boolean isExperimentalMode() {
110 return mIsExperimentalMode;
113 public static boolean isDeveloperMode() {
114 return mIsDeveloperMode;
117 private boolean isKeyboardOpen = false;
118 private boolean isFormattingToolbarOpen = false;
119 private boolean isSearchToolbarOpen = false;
120 private static boolean isDocumentChanged = false;
121 private boolean isUNOCommandsToolbarOpen = false;
123 @Override
124 public void onCreate(Bundle savedInstanceState) {
125 Log.w(LOGTAG, "onCreate..");
126 super.onCreate(savedInstanceState);
128 SettingsListenerModel.getInstance().setListener(this);
129 updatePreferences();
131 setContentView(R.layout.activity_main);
133 toolbarTop = findViewById(R.id.toolbar);
134 hideBottomToolbar();
136 mToolbarController = new ToolbarController(this, toolbarTop);
137 mFormattingController = new FormattingController(this);
138 toolbarTop.setNavigationOnClickListener(new View.OnClickListener() {
139 @Override
140 public void onClick(View view) {
141 LOKitShell.sendNavigationClickEvent();
145 mFontController = new FontController(this);
146 mSearchController = new SearchController(this);
147 mUNOCommandsController = new UNOCommandsController(this);
149 loKitThread = new LOKitThread(this);
150 loKitThread.start();
152 mLayerClient = new GeckoLayerClient(this);
153 LayerView layerView = findViewById(R.id.layer_view);
154 mLayerClient.setView(layerView);
155 layerView.setInputConnectionHandler(new LOKitInputConnectionHandler());
156 mLayerClient.notifyReady();
158 layerView.setOnKeyListener(new View.OnKeyListener() {
159 @Override
160 public boolean onKey(View view, int i, KeyEvent keyEvent) {
161 if(!isReadOnlyMode() && keyEvent.getKeyCode() != KeyEvent.KEYCODE_BACK){
162 setDocumentChanged(true);
164 return false;
168 // create TextCursorLayer
169 mDocumentOverlay = new DocumentOverlay(this, layerView);
171 mbISReadOnlyMode = !isExperimentalMode();
173 final Uri docUri = getIntent().getData();
174 if (docUri != null) {
175 if (docUri.getScheme().equals(ContentResolver.SCHEME_CONTENT)
176 || docUri.getScheme().equals(ContentResolver.SCHEME_ANDROID_RESOURCE)) {
177 final boolean isReadOnlyDoc = (getIntent().getFlags() & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) == 0;
178 mbISReadOnlyMode = !isExperimentalMode() || isReadOnlyDoc;
179 Log.d(LOGTAG, "SCHEME_CONTENT: getPath(): " + docUri.getPath());
181 String displayName = FileUtilities.retrieveDisplayNameForDocumentUri(getContentResolver(), docUri);
182 toolbarTop.setTitle(displayName);
184 } else if (docUri.getScheme().equals(ContentResolver.SCHEME_FILE)) {
185 mbISReadOnlyMode = true;
186 Log.d(LOGTAG, "SCHEME_FILE: getPath(): " + docUri.getPath());
187 toolbarTop.setTitle(docUri.getLastPathSegment());
189 // create a temporary local copy to work with
190 boolean copyOK = copyFileToTemp(docUri) && mTempFile != null;
191 if (!copyOK) {
192 // TODO: can't open the file
193 Log.e(LOGTAG, "couldn't create temporary file from " + docUri);
194 return;
197 // if input doc is a template, a new doc is created and a proper URI to save to
198 // will only be available after a "Save As"
199 if (isTemplate(docUri)) {
200 toolbarTop.setTitle(R.string.default_document_name);
201 } else {
202 mDocumentUri = docUri;
205 LOKitShell.sendLoadEvent(mTempFile.getPath());
206 } else if (getIntent().getStringExtra(LibreOfficeUIActivity.NEW_DOC_TYPE_KEY) != null) {
207 // New document type string is not null, meaning we want to open a new document
208 String newDocumentType = getIntent().getStringExtra(LibreOfficeUIActivity.NEW_DOC_TYPE_KEY);
209 // create a temporary local file, will be copied to the actual URI when saving
210 loadNewDocument(newDocumentType);
211 toolbarTop.setTitle(getString(R.string.default_document_name));
212 } else {
213 Log.e(LOGTAG, "No document specified. This should never happen.");
214 return;
216 // the loadDocument/loadNewDocument event already triggers a refresh as well,
217 // so there's no need to do another refresh in 'onStart'
218 mbSkipNextRefresh = true;
220 mDrawerLayout = findViewById(R.id.drawer_layout);
222 if (mDocumentPartViewListAdapter == null) {
223 mDrawerList = findViewById(R.id.left_drawer);
225 mDocumentPartViewListAdapter = new DocumentPartViewListAdapter(this, R.layout.document_part_list_layout, mDocumentPartView);
226 mDrawerList.setAdapter(mDocumentPartViewListAdapter);
227 mDrawerList.setOnItemClickListener(new DocumentPartClickListener());
230 mToolbarController.setupToolbars();
232 TabHost host = findViewById(R.id.toolbarTabHost);
233 host.setup();
235 TabHost.TabSpec spec = host.newTabSpec(getString(R.string.tabhost_character));
236 spec.setContent(R.id.tab_character);
237 spec.setIndicator(getString(R.string.tabhost_character));
238 host.addTab(spec);
240 spec = host.newTabSpec(getString(R.string.tabhost_paragraph));
241 spec.setContent(R.id.tab_paragraph);
242 spec.setIndicator(getString(R.string.tabhost_paragraph));
243 host.addTab(spec);
245 spec = host.newTabSpec(getString(R.string.tabhost_insert));
246 spec.setContent(R.id.tab_insert);
247 spec.setIndicator(getString(R.string.tabhost_insert));
248 host.addTab(spec);
250 spec = host.newTabSpec(getString(R.string.tabhost_style));
251 spec.setContent(R.id.tab_style);
252 spec.setIndicator(getString(R.string.tabhost_style));
253 host.addTab(spec);
255 LinearLayout bottomToolbarLayout = findViewById(R.id.toolbar_bottom);
256 LinearLayout toolbarColorPickerLayout = findViewById(R.id.toolbar_color_picker);
257 LinearLayout toolbarBackColorPickerLayout = findViewById(R.id.toolbar_back_color_picker);
258 bottomToolbarSheetBehavior = BottomSheetBehavior.from(bottomToolbarLayout);
259 toolbarColorPickerBottomSheetBehavior = BottomSheetBehavior.from(toolbarColorPickerLayout);
260 toolbarBackColorPickerBottomSheetBehavior = BottomSheetBehavior.from(toolbarBackColorPickerLayout);
261 bottomToolbarSheetBehavior.setHideable(true);
262 toolbarColorPickerBottomSheetBehavior.setHideable(true);
263 toolbarBackColorPickerBottomSheetBehavior.setHideable(true);
266 private void updatePreferences() {
267 SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
268 mIsExperimentalMode = BuildConfig.ALLOW_EDITING
269 && sPrefs.getBoolean(ENABLE_EXPERIMENTAL_PREFS_KEY, false);
270 mIsDeveloperMode = mIsExperimentalMode
271 && sPrefs.getBoolean(ENABLE_DEVELOPER_PREFS_KEY, false);
272 if (sPrefs.getInt(ASSETS_EXTRACTED_PREFS_KEY, 0) != BuildConfig.VERSION_CODE) {
273 if(copyFromAssets(getAssets(), "unpack", getApplicationInfo().dataDir)) {
274 sPrefs.edit().putInt(ASSETS_EXTRACTED_PREFS_KEY, BuildConfig.VERSION_CODE).apply();
279 // Loads a new Document and saves it to a temporary file
280 private void loadNewDocument(String newDocumentType) {
281 String tempFileName = "LibreOffice_" + UUID.randomUUID().toString();
282 mTempFile = new File(this.getCacheDir(), tempFileName);
283 LOKitShell.sendNewDocumentLoadEvent(mTempFile.getPath(), newDocumentType);
286 public RectF getCurrentCursorPosition() {
287 return mDocumentOverlay.getCurrentCursorPosition();
290 private boolean copyFileToTemp(Uri documentUri) {
291 // CSV files need a .csv suffix to be opened in Calc.
292 String suffix = null;
293 String intentType = getIntent().getType();
294 // K-9 mail uses the first, GMail uses the second variant.
295 if ("text/comma-separated-values".equals(intentType) || "text/csv".equals(intentType))
296 suffix = ".csv";
298 try {
299 mTempFile = File.createTempFile("LibreOffice", suffix, this.getCacheDir());
300 final FileOutputStream outputStream = new FileOutputStream(mTempFile);
301 return copyUriToStream(documentUri, outputStream);
302 } catch (FileNotFoundException e) {
303 return false;
304 } catch (IOException e) {
305 return false;
310 * Save the document.
312 public void saveDocument() {
313 Toast.makeText(this, R.string.message_saving, Toast.LENGTH_SHORT).show();
314 // local save
315 LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND_NOTIFY, ".uno:Save", true));
319 * Open file chooser and save the document to the URI
320 * selected there.
322 public void saveDocumentAs() {
323 Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
324 intent.addCategory(Intent.CATEGORY_OPENABLE);
325 String mimeType = getODFMimeTypeForDocument();
326 intent.setType(mimeType);
327 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 mbISReadOnlyMode = !isExperimentalMode();
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 intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, mDocumentUri);
355 final String displayName = toolbarTop.getTitle().toString();
356 final String suggestedFileName = FileUtilities.stripExtensionFromFileName(displayName) + ".pdf";
357 intent.putExtra(Intent.EXTRA_TITLE, suggestedFileName);
359 startActivityForResult(intent, REQUEST_CODE_EXPORT_TO_PDF);
362 private void exportToPDF(final Uri uri) {
363 boolean exportOK = false;
364 File tempFile = null;
365 try {
366 tempFile = File.createTempFile("LibreOffice_", ".pdf");
367 mTileProvider.saveDocumentAs(tempFile.getAbsolutePath(),"pdf", false);
369 try {
370 FileInputStream inputStream = new FileInputStream(tempFile);
371 exportOK = copyStreamToUri(inputStream, uri);
372 } catch (FileNotFoundException e) {
373 e.printStackTrace();
376 } catch (IOException e) {
377 e.printStackTrace();
378 } finally {
379 if (tempFile != null && tempFile.exists()) {
380 tempFile.delete();
384 final int msgId = exportOK ? R.string.pdf_export_finished : R.string.unable_to_export_pdf;
385 LOKitShell.getMainHandler().post(new Runnable() {
386 @Override
387 public void run() {
388 showCustomStatusMessage(getString(msgId));
394 * Returns the ODF MIME type that can be used for the current document,
395 * regardless of whether the document is an ODF Document or not
396 * (e.g. returns FileUtilities.MIMETYPE_OPENDOCUMENT_TEXT for a DOCX file).
397 * @return MIME type, or empty string, if no appropriate MIME type could be found.
399 private String getODFMimeTypeForDocument() {
400 if (mTileProvider.isTextDocument())
401 return FileUtilities.MIMETYPE_OPENDOCUMENT_TEXT;
402 else if (mTileProvider.isSpreadsheet())
403 return FileUtilities.MIMETYPE_OPENDOCUMENT_SPREADSHEET;
404 else if (mTileProvider.isPresentation())
405 return FileUtilities.MIMETYPE_OPENDOCUMENT_PRESENTATION;
406 else if (mTileProvider.isDrawing())
407 return FileUtilities.MIMETYPE_OPENDOCUMENT_GRAPHICS;
408 else {
409 Log.w(LOGTAG, "Cannot determine MIME type to use.");
410 return "";
415 * Returns whether the MIME type for the URI is considered one for a document template.
417 private boolean isTemplate(final Uri documentUri) {
418 final String mimeType = getContentResolver().getType(documentUri);
419 return FileUtilities.isTemplateMimeType(mimeType);
422 public void saveFileToOriginalSource() {
423 if (mTempFile == null || mDocumentUri == null || !mDocumentUri.getScheme().equals(ContentResolver.SCHEME_CONTENT))
424 return;
426 boolean copyOK = false;
427 try {
428 final FileInputStream inputStream = new FileInputStream(mTempFile);
429 copyOK = copyStreamToUri(inputStream, mDocumentUri);
430 } catch (FileNotFoundException e) {
431 e.printStackTrace();
433 if (copyOK) {
434 runOnUiThread(new Runnable() {
435 @Override
436 public void run() {
437 Toast.makeText(LibreOfficeMainActivity.this, R.string.message_saved,
438 Toast.LENGTH_SHORT).show();
441 setDocumentChanged(false);
442 } else {
443 runOnUiThread(new Runnable() {
444 @Override
445 public void run() {
446 Toast.makeText(LibreOfficeMainActivity.this, R.string.message_saving_failed,
447 Toast.LENGTH_SHORT).show();
453 @Override
454 protected void onResume() {
455 super.onResume();
456 Log.i(LOGTAG, "onResume..");
457 // check for config change
458 updatePreferences();
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 LOKitShell.sendCloseEvent();
493 mLayerClient.destroy();
494 super.onDestroy();
496 if (isFinishing()) { // Not an orientation change
497 if (mTempFile != null) {
498 // noinspection ResultOfMethodCallIgnored
499 mTempFile.delete();
501 if (mTempSlideShowFile != null && mTempSlideShowFile.exists()) {
502 // noinspection ResultOfMethodCallIgnored
503 mTempSlideShowFile.delete();
507 @Override
508 public void onBackPressed() {
509 if (!isDocumentChanged) {
510 super.onBackPressed();
511 return;
515 DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
516 @Override
517 public void onClick(DialogInterface dialog, int which) {
518 switch (which){
519 case DialogInterface.BUTTON_POSITIVE:
520 mTileProvider.saveDocument();
521 isDocumentChanged=false;
522 onBackPressed();
523 break;
524 case DialogInterface.BUTTON_NEGATIVE:
525 //CANCEL
526 break;
527 case DialogInterface.BUTTON_NEUTRAL:
528 //NO
529 isDocumentChanged=false;
530 onBackPressed();
531 break;
536 AlertDialog.Builder builder = new AlertDialog.Builder(this);
537 builder.setMessage(R.string.save_alert_dialog_title)
538 .setPositiveButton(R.string.save_document, dialogClickListener)
539 .setNegativeButton(R.string.action_cancel, dialogClickListener)
540 .setNeutralButton(R.string.no_save_document, dialogClickListener)
541 .show();
545 public List<DocumentPartView> getDocumentPartView() {
546 return mDocumentPartView;
549 public void disableNavigationDrawer() {
550 // Only the original thread that created mDrawerLayout should touch its views.
551 LOKitShell.getMainHandler().post(new Runnable() {
552 @Override
553 public void run() {
554 mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, mDrawerList);
559 public DocumentPartViewListAdapter getDocumentPartViewListAdapter() {
560 return mDocumentPartViewListAdapter;
564 * Show software keyboard.
565 * Force the request on main thread.
567 public void showSoftKeyboard() {
569 LOKitShell.getMainHandler().post(new Runnable() {
570 @Override
571 public void run() {
572 if(!isKeyboardOpen) showSoftKeyboardDirect();
573 else hideSoftKeyboardDirect();
579 private void showSoftKeyboardDirect() {
580 LayerView layerView = findViewById(R.id.layer_view);
582 if (layerView.requestFocus()) {
583 InputMethodManager inputMethodManager = (InputMethodManager) getApplicationContext().getSystemService(Context.INPUT_METHOD_SERVICE);
584 inputMethodManager.showSoftInput(layerView, InputMethodManager.SHOW_FORCED);
586 isKeyboardOpen=true;
587 isSearchToolbarOpen=false;
588 isFormattingToolbarOpen=false;
589 isUNOCommandsToolbarOpen=false;
590 hideBottomToolbar();
593 public void showSoftKeyboardOrFormattingToolbar() {
594 LOKitShell.getMainHandler().post(new Runnable() {
595 @Override
596 public void run() {
597 if (findViewById(R.id.toolbar_bottom).getVisibility() != View.VISIBLE
598 && findViewById(R.id.toolbar_color_picker).getVisibility() != View.VISIBLE) {
599 showSoftKeyboardDirect();
606 * Hides software keyboard on UI thread.
608 public void hideSoftKeyboard() {
609 LOKitShell.getMainHandler().post(new Runnable() {
610 @Override
611 public void run() {
612 hideSoftKeyboardDirect();
618 * Hides software keyboard.
620 private void hideSoftKeyboardDirect() {
621 if (getCurrentFocus() != null) {
622 InputMethodManager inputMethodManager = (InputMethodManager) getApplicationContext().getSystemService(Context.INPUT_METHOD_SERVICE);
623 inputMethodManager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
624 isKeyboardOpen=false;
628 public void showBottomToolbar() {
629 LOKitShell.getMainHandler().post(new Runnable() {
630 @Override
631 public void run() {
632 bottomToolbarSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
637 public void hideBottomToolbar() {
638 LOKitShell.getMainHandler().post(new Runnable() {
639 @Override
640 public void run() {
641 bottomToolbarSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
642 toolbarColorPickerBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
643 toolbarBackColorPickerBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
644 findViewById(R.id.search_toolbar).setVisibility(View.GONE);
645 findViewById(R.id.UNO_commands_toolbar).setVisibility(View.GONE);
646 isFormattingToolbarOpen=false;
647 isSearchToolbarOpen=false;
648 isUNOCommandsToolbarOpen=false;
653 public void showFormattingToolbar() {
654 LOKitShell.getMainHandler().post(new Runnable() {
655 @Override
656 public void run() {
657 if (isFormattingToolbarOpen) {
658 hideFormattingToolbar();
659 } else {
660 showBottomToolbar();
661 findViewById(R.id.search_toolbar).setVisibility(View.GONE);
662 findViewById(R.id.formatting_toolbar).setVisibility(View.VISIBLE);
663 findViewById(R.id.search_toolbar).setVisibility(View.GONE);
664 findViewById(R.id.UNO_commands_toolbar).setVisibility(View.GONE);
665 hideSoftKeyboardDirect();
666 isSearchToolbarOpen=false;
667 isFormattingToolbarOpen=true;
668 isUNOCommandsToolbarOpen=false;
675 public void hideFormattingToolbar() {
676 LOKitShell.getMainHandler().post(new Runnable() {
677 @Override
678 public void run() {
679 hideBottomToolbar();
684 public void showSearchToolbar() {
685 LOKitShell.getMainHandler().post(new Runnable() {
686 @Override
687 public void run() {
688 if (isSearchToolbarOpen) {
689 hideSearchToolbar();
690 } else {
691 showBottomToolbar();
692 findViewById(R.id.formatting_toolbar).setVisibility(View.GONE);
693 toolbarColorPickerBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
694 toolbarBackColorPickerBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
695 findViewById(R.id.search_toolbar).setVisibility(View.VISIBLE);
696 findViewById(R.id.UNO_commands_toolbar).setVisibility(View.GONE);
697 hideSoftKeyboardDirect();
698 isFormattingToolbarOpen=false;
699 isSearchToolbarOpen=true;
700 isUNOCommandsToolbarOpen=false;
706 public void hideSearchToolbar() {
707 LOKitShell.getMainHandler().post(new Runnable() {
708 @Override
709 public void run() {
710 hideBottomToolbar();
715 public void showUNOCommandsToolbar() {
716 LOKitShell.getMainHandler().post(new Runnable() {
717 @Override
718 public void run() {
719 if(isUNOCommandsToolbarOpen){
720 hideUNOCommandsToolbar();
721 }else{
722 showBottomToolbar();
723 findViewById(R.id.formatting_toolbar).setVisibility(View.GONE);
724 findViewById(R.id.search_toolbar).setVisibility(View.GONE);
725 findViewById(R.id.UNO_commands_toolbar).setVisibility(View.VISIBLE);
726 hideSoftKeyboardDirect();
727 isFormattingToolbarOpen=false;
728 isSearchToolbarOpen=false;
729 isUNOCommandsToolbarOpen=true;
735 public void hideUNOCommandsToolbar() {
736 LOKitShell.getMainHandler().post(new Runnable() {
737 @Override
738 public void run() {
739 hideBottomToolbar();
744 public void showProgressSpinner() {
745 findViewById(R.id.loadingPanel).setVisibility(View.VISIBLE);
748 public void hideProgressSpinner() {
749 findViewById(R.id.loadingPanel).setVisibility(View.GONE);
752 public void showAlertDialog(String message) {
754 AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(LibreOfficeMainActivity.this);
756 alertDialogBuilder.setTitle(R.string.error);
757 alertDialogBuilder.setMessage(message);
758 alertDialogBuilder.setNeutralButton(R.string.alert_ok, new DialogInterface.OnClickListener() {
759 public void onClick(DialogInterface dialog, int id) {
760 finish();
764 AlertDialog alertDialog = alertDialogBuilder.create();
765 alertDialog.show();
768 public DocumentOverlay getDocumentOverlay() {
769 return mDocumentOverlay;
772 public CalcHeadersController getCalcHeadersController() {
773 return mCalcHeadersController;
776 public ToolbarController getToolbarController() {
777 return mToolbarController;
780 public FontController getFontController() {
781 return mFontController;
784 public FormattingController getFormattingController() {
785 return mFormattingController;
788 public void openDrawer() {
789 mDrawerLayout.openDrawer(mDrawerList);
790 hideBottomToolbar();
793 public void showAbout() {
794 AboutDialogFragment aboutDialogFragment = new AboutDialogFragment();
795 aboutDialogFragment.show(getSupportFragmentManager(), "AboutDialogFragment");
798 public void addPart(){
799 mTileProvider.addPart();
800 mDocumentPartViewListAdapter.notifyDataSetChanged();
801 setDocumentChanged(true);
804 public void renamePart(){
805 AlertDialog.Builder builder = new AlertDialog.Builder(this);
806 builder.setTitle(R.string.enter_part_name);
807 final EditText input = new EditText(this);
808 input.setInputType(InputType.TYPE_CLASS_TEXT);
809 builder.setView(input);
811 builder.setPositiveButton(R.string.alert_ok, new DialogInterface.OnClickListener() {
812 @Override
813 public void onClick(DialogInterface dialog, int which) {
814 mTileProvider.renamePart( input.getText().toString());
817 builder.setNegativeButton(R.string.alert_cancel, new DialogInterface.OnClickListener() {
818 @Override
819 public void onClick(DialogInterface dialog, int which) {
820 dialog.cancel();
824 builder.show();
827 public void deletePart() {
828 mTileProvider.removePart();
831 public void showSettings() {
832 startActivity(new Intent(getApplicationContext(), SettingsActivity.class));
835 public boolean isDrawerEnabled() {
836 boolean isDrawerOpen = mDrawerLayout.isDrawerOpen(mDrawerList);
837 boolean isDrawerLocked = mDrawerLayout.getDrawerLockMode(mDrawerList) != DrawerLayout.LOCK_MODE_UNLOCKED;
838 return !isDrawerOpen && !isDrawerLocked;
841 @Override
842 public void settingsPreferenceChanged(SharedPreferences sharedPreferences, String key) {
843 if (key.matches(ENABLE_EXPERIMENTAL_PREFS_KEY)) {
844 Log.d(LOGTAG, "Editing Preference Changed");
845 mIsExperimentalMode = sharedPreferences.getBoolean(ENABLE_EXPERIMENTAL_PREFS_KEY, false);
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 mbISReadOnlyMode;
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: */