collabora-online/android/lib/src/main/java/org/libreoffice/androidlib/LOActivity.java

1170 lines
46 KiB
Java

/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.libreoffice.androidlib;
import android.Manifest;
import android.app.AlertDialog;
import android.content.ClipData;
import android.content.ClipDescription;
import android.content.ClipboardManager;
import android.content.ActivityNotFoundException;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.preference.PreferenceManager;
import android.print.PrintAttributes;
import android.print.PrintDocumentAdapter;
import android.print.PrintManager;
import android.provider.DocumentsContract;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.webkit.JavascriptInterface;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.widget.RatingBar;
import android.widget.TextView;
import android.widget.Toast;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.BufferedWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.libreoffice.androidlib.lok.LokClipboardData;
import org.libreoffice.androidlib.lok.LokClipboardEntry;
public class LOActivity extends AppCompatActivity {
final static String TAG = "LOActivity";
private static final String ASSETS_EXTRACTED_GIT_COMMIT = "ASSETS_EXTRACTED_GIT_COMMIT";
private static final int PERMISSION_WRITE_EXTERNAL_STORAGE = 777;
private static final String KEY_ENABLE_SHOW_DEBUG_INFO = "ENABLE_SHOW_DEBUG_INFO";
private static final String KEY_PROVIDER_ID = "providerID";
private static final String KEY_DOCUMENT_URI = "documentUri";
private static final String KEY_IS_EDITABLE = "isEditable";
private static final String KEY_INTENT_URI = "intentUri";
private static final String CLIPBOARD_FILE_PATH = "LibreofficeClipboardFile.data";
private static final String CLIPBOARD_LOOL_SIGNATURE = "lool-clip-magic-4a22437e49a8-";
private File mTempFile = null;
private int providerId;
/// Unique number identifying this app + document.
private long loadDocumentMillis = 0;
@Nullable
private URI documentUri;
private String urlToLoad;
private WebView mWebView = null;
private SharedPreferences sPrefs;
private Handler mMainHandler = null;
private RateAppController rateAppController;
private boolean isDocEditable = false;
private boolean isDocDebuggable = BuildConfig.DEBUG;
private boolean documentLoaded = false;
private ClipboardManager clipboardManager;
private ClipData clipData;
private Thread nativeMsgThread;
private Handler nativeHandler;
private Looper nativeLooper;
private Bundle savedInstanceState;
private ProgressDialog mProgressDialog = null;
/** In case the mobile-wizard is visible, we have to intercept the Android's Back button. */
private boolean mMobileWizardVisible = false;
private boolean mIsEditModeActive = false;
private ValueCallback<Uri[]> valueCallback;
public static final int REQUEST_SELECT_IMAGE_FILE = 500;
public static final int REQUEST_SAVEAS_PDF = 501;
public static final int REQUEST_SAVEAS_RTF = 502;
public static final int REQUEST_SAVEAS_ODT = 503;
public static final int REQUEST_SAVEAS_ODP = 504;
public static final int REQUEST_SAVEAS_ODS = 505;
public static final int REQUEST_SAVEAS_DOCX = 506;
public static final int REQUEST_SAVEAS_PPTX = 507;
public static final int REQUEST_SAVEAS_XLSX = 508;
public static final int REQUEST_SAVEAS_DOC = 509;
public static final int REQUEST_SAVEAS_PPT = 510;
public static final int REQUEST_SAVEAS_XLS = 511;
public static final int REQUEST_SAVEAS_EPUB = 512;
/** Broadcasting event for passing info back to the shell. */
public static final String LO_ACTIVITY_BROADCAST = "LOActivityBroadcast";
/** Event description for passing info back to the shell. */
public static final String LO_ACTION_EVENT = "LOEvent";
/** Data description for passing info back to the shell. */
public static final String LO_ACTION_DATA = "LOData";
private static boolean copyFromAssets(AssetManager assetManager,
String fromAssetPath, String targetDir) {
try {
String[] files = assetManager.list(fromAssetPath);
boolean res = true;
for (String file : files) {
String[] dirOrFile = assetManager.list(fromAssetPath + "/" + file);
if (dirOrFile.length == 0) {
// noinspection ResultOfMethodCallIgnored
new File(targetDir).mkdirs();
res &= copyAsset(assetManager,
fromAssetPath + "/" + file,
targetDir + "/" + file);
} else
res &= copyFromAssets(assetManager,
fromAssetPath + "/" + file,
targetDir + "/" + file);
}
return res;
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "copyFromAssets failed: " + e.getMessage());
return false;
}
}
private static boolean copyAsset(AssetManager assetManager, String fromAssetPath, String toPath) {
ReadableByteChannel source = null;
FileChannel dest = null;
try {
try {
source = Channels.newChannel(assetManager.open(fromAssetPath));
dest = new FileOutputStream(toPath).getChannel();
long bytesTransferred = 0;
// might not copy all at once, so make sure everything gets copied....
ByteBuffer buffer = ByteBuffer.allocate(4096);
while (source.read(buffer) > 0) {
buffer.flip();
bytesTransferred += dest.write(buffer);
buffer.clear();
}
Log.v(TAG, "Success copying " + fromAssetPath + " to " + toPath + " bytes: " + bytesTransferred);
return true;
} finally {
if (dest != null) dest.close();
if (source != null) source.close();
}
} catch (FileNotFoundException e) {
Log.e(TAG, "file " + fromAssetPath + " not found! " + e.getMessage());
return false;
} catch (IOException e) {
Log.e(TAG, "failed to copy file " + fromAssetPath + " from assets to " + toPath + " - " + e.getMessage());
return false;
}
}
private Handler getMainHandler() {
if (mMainHandler == null) {
mMainHandler = new Handler(getMainLooper());
}
return mMainHandler;
}
/** True if the App is running under ChromeOS. */
public static boolean isChromeOS(Context context) {
return context.getPackageManager().hasSystemFeature("org.chromium.arc.device_management");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.savedInstanceState = savedInstanceState;
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
sPrefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
setContentView(R.layout.lolib_activity_main);
mProgressDialog = new ProgressDialog(this);
if (BuildConfig.GOOGLE_PLAY_ENABLED)
this.rateAppController = new RateAppController(this);
else
this.rateAppController = null;
init();
}
/** Initialize the app - copy the assets and create the UI. */
private void init() {
if (sPrefs.getString(ASSETS_EXTRACTED_GIT_COMMIT, "").equals(BuildConfig.GIT_COMMIT)) {
// all is fine, we have already copied the assets
initUI();
return;
}
mProgressDialog.indeterminate(R.string.preparing_for_the_first_start_after_an_update);
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... voids) {
// copy the new assets
if (copyFromAssets(getAssets(), "unpack", getApplicationInfo().dataDir)) {
sPrefs.edit().putString(ASSETS_EXTRACTED_GIT_COMMIT, BuildConfig.GIT_COMMIT).apply();
}
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
initUI();
}
}.execute();
}
/** Actual initialization of the UI. */
private void initUI() {
isDocDebuggable = sPrefs.getBoolean(KEY_ENABLE_SHOW_DEBUG_INFO, false) && BuildConfig.DEBUG;
if (getIntent().getData() != null) {
if (getIntent().getData().getScheme().equals(ContentResolver.SCHEME_CONTENT)) {
isDocEditable = true;
// is it read-only?
if ((getIntent().getFlags() & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) == 0) {
isDocEditable = false;
Log.d(TAG, "Disabled editing: Read-only");
Toast.makeText(this, getResources().getString(R.string.temp_file_saving_disabled), Toast.LENGTH_SHORT).show();
}
// turns out that on ChromeOS, it is not possible to save back
// to Google Drive; detect it already here to avoid disappointment
// also the volumeprovider does not work for saving back,
// which is much more serious :-(
if (isDocEditable && (getIntent().getData().toString().startsWith("content://org.chromium.arc.chromecontentprovider/externalfile") ||
getIntent().getData().toString().startsWith("content://org.chromium.arc.volumeprovider/"))) {
isDocEditable = false;
Log.d(TAG, "Disabled editing: Chrome OS unsupported content providers");
Toast.makeText(this, getResources().getString(R.string.file_chromeos_read_only), Toast.LENGTH_LONG).show();
}
if (copyFileToTemp() && mTempFile != null) {
documentUri = mTempFile.toURI();
urlToLoad = documentUri.toString();
Log.d(TAG, "SCHEME_CONTENT: getPath(): " + getIntent().getData().getPath());
} else {
// TODO: can't open the file
Log.e(TAG, "couldn't create temporary file from " + getIntent().getData());
}
} else if (getIntent().getData().getScheme().equals(ContentResolver.SCHEME_FILE)) {
isDocEditable = true;
urlToLoad = getIntent().getData().toString();
Log.d(TAG, "SCHEME_FILE: getPath(): " + getIntent().getData().getPath());
// Gather data to rebuild IFile object later
providerId = getIntent().getIntExtra(
"org.libreoffice.document_provider_id", 0);
documentUri = (URI) getIntent().getSerializableExtra(
"org.libreoffice.document_uri");
}
} else if (savedInstanceState != null) {
getIntent().setAction(Intent.ACTION_VIEW)
.setData(Uri.parse(savedInstanceState.getString(KEY_INTENT_URI)));
urlToLoad = getIntent().getData().toString();
providerId = savedInstanceState.getInt(KEY_PROVIDER_ID);
if (savedInstanceState.getString(KEY_DOCUMENT_URI) != null) {
try {
documentUri = new URI(savedInstanceState.getString(KEY_DOCUMENT_URI));
urlToLoad = documentUri.toString();
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
isDocEditable = savedInstanceState.getBoolean(KEY_IS_EDITABLE);
} else {
//User can't reach here but if he/she does then
Toast.makeText(this, getString(R.string.failed_to_load_file), Toast.LENGTH_SHORT).show();
finish();
}
mWebView = findViewById(R.id.browser);
WebSettings webSettings = mWebView.getSettings();
webSettings.setJavaScriptEnabled(true);
mWebView.addJavascriptInterface(this, "LOOLMessageHandler");
// allow debugging (when building the debug version); see details in
// https://developers.google.com/web/tools/chrome-devtools/remote-debugging/webviews
boolean isChromeDebugEnabled = sPrefs.getBoolean("ENABLE_CHROME_DEBUGGING", false);
if ((getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0 || isChromeDebugEnabled) {
WebView.setWebContentsDebuggingEnabled(true);
}
getMainHandler();
clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
nativeMsgThread = new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
nativeLooper = Looper.myLooper();
nativeHandler = new Handler(nativeLooper);
Looper.loop();
}
});
nativeMsgThread.start();
mWebView.setWebChromeClient(new WebChromeClient() {
@Override
public boolean onShowFileChooser(WebView mWebView, ValueCallback<Uri[]> filePathCallback, WebChromeClient.FileChooserParams fileChooserParams) {
if (valueCallback != null) {
valueCallback.onReceiveValue(null);
valueCallback = null;
}
valueCallback = filePathCallback;
Intent intent = fileChooserParams.createIntent();
try {
intent.setType("image/*");
startActivityForResult(intent, REQUEST_SELECT_IMAGE_FILE);
} catch (ActivityNotFoundException e) {
valueCallback = null;
Toast.makeText(LOActivity.this, getString(R.string.cannot_open_file_chooser), Toast.LENGTH_LONG).show();
return false;
}
return true;
}
});
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
Log.i(TAG, "asking for read storage permission");
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
PERMISSION_WRITE_EXTERNAL_STORAGE);
} else {
loadDocument();
}
}
@Override
protected void onNewIntent(Intent intent) {
Log.i(TAG, "onNewIntent");
if (documentLoaded) {
postMobileMessageNative("save dontTerminateEdit=true dontSaveIfUnmodified=true");
}
final Intent finalIntent = intent;
mProgressDialog.indeterminate(R.string.exiting);
getMainHandler().post(new Runnable() {
@Override
public void run() {
documentLoaded = false;
postMobileMessageNative("BYE");
copyTempBackToIntent();
runOnUiThread(new Runnable() {
@Override
public void run() {
mProgressDialog.dismiss();
setIntent(finalIntent);
init();
}
});
}
});
super.onNewIntent(intent);
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(KEY_INTENT_URI, getIntent().getData().toString());
outState.putInt(KEY_PROVIDER_ID, providerId);
if (documentUri != null) {
outState.putString(KEY_DOCUMENT_URI, documentUri.toString());
}
//If this activity was opened via contentUri
outState.putBoolean(KEY_IS_EDITABLE, isDocEditable);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case PERMISSION_WRITE_EXTERNAL_STORAGE:
if (permissions.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
loadDocument();
} else {
Toast.makeText(this, getString(R.string.storage_permission_required), Toast.LENGTH_SHORT).show();
finishAndRemoveTask();
break;
}
break;
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
/** When we get the file via a content: URI, we need to put it to a temp file. */
private boolean copyFileToTemp() {
ContentResolver contentResolver = getContentResolver();
InputStream inputStream = null;
OutputStream outputStream = null;
// CSV files need a .csv suffix to be opened in Calc.
String suffix = null;
String intentType = getIntent().getType();
// K-9 mail uses the first, GMail uses the second variant.
if ("text/comma-separated-values".equals(intentType) || "text/csv".equals(intentType))
suffix = ".csv";
try {
try {
Uri uri = getIntent().getData();
inputStream = contentResolver.openInputStream(uri);
mTempFile = File.createTempFile("LibreOffice", suffix, this.getCacheDir());
outputStream = new FileOutputStream(mTempFile);
byte[] buffer = new byte[1024];
int length;
long bytes = 0;
while ((length = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
bytes += length;
}
Log.i(TAG, "Success copying " + bytes + " bytes from " + uri + " to " + mTempFile);
} finally {
if (inputStream != null)
inputStream.close();
if (outputStream != null)
outputStream.close();
}
} catch (FileNotFoundException e) {
Log.e(TAG, "file not found: " + e.getMessage());
return false;
} catch (IOException e) {
Log.e(TAG, "exception: " + e.getMessage());
return false;
}
return true;
}
/** Check that we have created a temp file, and if yes, copy it back to the content: URI. */
private void copyTempBackToIntent() {
if (!isDocEditable || mTempFile == null || getIntent().getData() == null || !getIntent().getData().getScheme().equals(ContentResolver.SCHEME_CONTENT))
return;
ContentResolver contentResolver = getContentResolver();
InputStream inputStream = null;
OutputStream outputStream = null;
try {
try {
inputStream = new FileInputStream(mTempFile);
Uri uri = getIntent().getData();
try {
outputStream = contentResolver.openOutputStream(uri, "wt");
}
catch (FileNotFoundException e) {
Log.i(TAG, "failed with the 'wt' mode, trying without: " + e.getMessage());
outputStream = contentResolver.openOutputStream(uri);
}
byte[] buffer = new byte[1024];
int length;
long bytes = 0;
while ((length = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
bytes += length;
}
Log.i(TAG, "Success copying " + bytes + " bytes from " + mTempFile + " to " + uri);
} finally {
if (inputStream != null)
inputStream.close();
if (outputStream != null)
outputStream.close();
}
} catch (FileNotFoundException e) {
Log.e(TAG, "file not found: " + e.getMessage());
} catch (Exception e) {
Log.e(TAG, "exception: " + e.getMessage());
}
}
@Override
protected void onResume() {
super.onResume();
Log.i(TAG, "onResume..");
}
@Override
protected void onPause() {
// A Save similar to an autosave
if (documentLoaded)
postMobileMessageNative("save dontTerminateEdit=true dontSaveIfUnmodified=true");
super.onPause();
Log.d(TAG, "onPause() - hinting to save, we might need to return to the doc");
}
@Override
protected void onDestroy() {
nativeLooper.quit();
// Remove the webview from the hierarchy & destroy
final ViewGroup viewGroup = (ViewGroup) mWebView.getParent();
if (viewGroup != null)
viewGroup.removeView(mWebView);
mWebView.destroy();
mWebView = null;
// Most probably the native part has already got a 'BYE' from
// finishWithProgress(), but it is actually better to send it twice
// than never, so let's call it from here too anyway
documentLoaded = false;
postMobileMessageNative("BYE");
mProgressDialog.dismiss();
super.onDestroy();
Log.i(TAG, "onDestroy() - we know we are leaving the document");
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
if (resultCode != RESULT_OK) {
if (requestCode == REQUEST_SELECT_IMAGE_FILE) {
valueCallback.onReceiveValue(null);
valueCallback = null;
}
return;
}
switch (requestCode) {
case REQUEST_SELECT_IMAGE_FILE:
if (valueCallback == null)
return;
valueCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, intent));
valueCallback = null;
return;
case REQUEST_SAVEAS_PDF:
case REQUEST_SAVEAS_RTF:
case REQUEST_SAVEAS_ODT:
case REQUEST_SAVEAS_ODP:
case REQUEST_SAVEAS_ODS:
case REQUEST_SAVEAS_DOCX:
case REQUEST_SAVEAS_PPTX:
case REQUEST_SAVEAS_XLSX:
case REQUEST_SAVEAS_DOC:
case REQUEST_SAVEAS_PPT:
case REQUEST_SAVEAS_XLS:
case REQUEST_SAVEAS_EPUB:
if (intent == null) {
return;
}
String format = getFormatForRequestCode(requestCode);
if (format != null) {
InputStream inputStream = null;
OutputStream outputStream = null;
try {
final File tempFile = File.createTempFile("LibreOffice", "." + format, this.getCacheDir());
LOActivity.this.saveAs(tempFile.toURI().toString(), format);
inputStream = new FileInputStream(tempFile);
try {
outputStream = getContentResolver().openOutputStream(intent.getData(), "wt");
}
catch (FileNotFoundException e) {
Log.i(TAG, "failed with the 'wt' mode, trying without: " + e.getMessage());
outputStream = getContentResolver().openOutputStream(intent.getData());
}
byte[] buffer = new byte[4096];
int len;
while ((len = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, len);
}
outputStream.flush();
} catch (Exception e) {
Toast.makeText(this, "Something went wrong while Saving as: " + e.getMessage(), Toast.LENGTH_SHORT).show();
e.printStackTrace();
} finally {
try {
if (inputStream != null)
inputStream.close();
if (outputStream != null)
outputStream.close();
} catch (Exception e) {
}
}
return;
}
}
Toast.makeText(this, "Unknown request", Toast.LENGTH_LONG).show();
}
private String getFormatForRequestCode(int requestCode) {
switch(requestCode) {
case REQUEST_SAVEAS_PDF: return "pdf";
case REQUEST_SAVEAS_RTF: return "rtf";
case REQUEST_SAVEAS_ODT: return "odt";
case REQUEST_SAVEAS_ODP: return "odp";
case REQUEST_SAVEAS_ODS: return "ods";
case REQUEST_SAVEAS_DOCX: return "docx";
case REQUEST_SAVEAS_PPTX: return "pptx";
case REQUEST_SAVEAS_XLSX: return "xlsx";
case REQUEST_SAVEAS_DOC: return "doc";
case REQUEST_SAVEAS_PPT: return "ppt";
case REQUEST_SAVEAS_XLS: return "xls";
case REQUEST_SAVEAS_EPUB: return "epub";
}
return null;
}
/** Show the Saving progress and finish the app. */
private void finishWithProgress() {
mProgressDialog.indeterminate(R.string.exiting);
// The 'BYE' takes a considerable amount of time, we need to post it
// so that it starts after the saving progress is actually shown
getMainHandler().post(new Runnable() {
@Override
public void run() {
documentLoaded = false;
postMobileMessageNative("BYE");
copyTempBackToIntent();
runOnUiThread(new Runnable() {
@Override
public void run() {
mProgressDialog.dismiss();
}
});
finishAndRemoveTask();
}
});
}
@Override
public void onBackPressed() {
if (mMobileWizardVisible)
{
// just return one level up in the mobile-wizard (or close it)
callFakeWebsocketOnMessage("'mobile: mobilewizardback'");
return;
} else if (mIsEditModeActive) {
callFakeWebsocketOnMessage("'mobile: readonlymode'");
return;
}
finishWithProgress();
}
private void loadDocument() {
mProgressDialog.determinate(R.string.loading);
// setup the LOOLWSD
ApplicationInfo applicationInfo = getApplicationInfo();
String dataDir = applicationInfo.dataDir;
Log.i(TAG, String.format("Initializing LibreOfficeKit, dataDir=%s\n", dataDir));
String cacheDir = getApplication().getCacheDir().getAbsolutePath();
String apkFile = getApplication().getPackageResourcePath();
AssetManager assetManager = getResources().getAssets();
createLOOLWSD(dataDir, cacheDir, apkFile, assetManager, urlToLoad);
// trigger the load of the document
String finalUrlToLoad = "file:///android_asset/dist/loleaflet.html?file_path=" +
urlToLoad + "&closebutton=1";
// set the language
String language = getResources().getConfiguration().locale.toLanguageTag();
Log.i(TAG, "Loading with language: " + language);
finalUrlToLoad += "&lang=" + language;
if (isDocEditable) {
finalUrlToLoad += "&permission=edit";
} else {
finalUrlToLoad += "&permission=readonly";
}
if (isDocDebuggable) {
finalUrlToLoad += "&debug=true";
}
// load the page
mWebView.loadUrl(finalUrlToLoad);
documentLoaded = true;
loadDocumentMillis = android.os.SystemClock.uptimeMillis();
}
static {
System.loadLibrary("androidapp");
}
public SharedPreferences getPrefs() {
return sPrefs;
}
/**
* Initialize the LOOLWSD to load 'loadFileURL'.
*/
public native void createLOOLWSD(String dataDir, String cacheDir, String apkFile, AssetManager assetManager, String loadFileURL);
/**
* Passing messages from JS (instead of the websocket communication).
*/
@JavascriptInterface
public void postMobileMessage(String message) {
Log.d(TAG, "postMobileMessage: " + message);
String[] messageAndParameterArray= message.split(" ", 2); // the command and the rest (that can potentially contain spaces too)
if (beforeMessageFromWebView(messageAndParameterArray)) {
postMobileMessageNative(message);
afterMessageFromWebView(messageAndParameterArray);
}
}
/**
* Call the post method form C++
*/
public native void postMobileMessageNative(String message);
/**
* Passing messages from JS (instead of the websocket communication).
*/
@JavascriptInterface
public void postMobileError(String message) {
// TODO handle this
Log.d(TAG, "postMobileError: " + message);
}
/**
* Passing messages from JS (instead of the websocket communication).
*/
@JavascriptInterface
public void postMobileDebug(String message) {
// TODO handle this
Log.d(TAG, "postMobileDebug: " + message);
}
/**
* Provide the info that this app is actually running under ChromeOS - so
* has to mostly look like on desktop.
*/
@JavascriptInterface
public boolean isChromeOS() {
return isChromeOS(this);
}
/**
* Passing message the other way around - from Java to the FakeWebSocket in JS.
*/
void callFakeWebsocketOnMessage(final String message) {
// call from the UI thread
if (mWebView != null)
mWebView.post(new Runnable() {
public void run() {
if (mWebView == null) {
Log.i(TAG, "Skipped forwarding to the WebView: " + message);
return;
}
Log.i(TAG, "Forwarding to the WebView: " + message);
/* Debug only: in case the message is too long, truncated in the logcat, and you need to see it.
final int size = 80;
for (int start = 0; start < message.length(); start += size) {
Log.i(TAG, "split: " + message.substring(start, Math.min(message.length(), start + size)));
}
*/
mWebView.loadUrl("javascript:window.TheFakeWebSocket.onmessage({'data':" + message + "});");
}
});
// update progress bar when loading
if (message.startsWith("'statusindicator") || message.startsWith("'error:")) {
runOnUiThread(new Runnable() {
public void run() {
// update progress bar if it exists
final String statusIndicatorSetValue = "'statusindicatorsetvalue: ";
if (message.startsWith(statusIndicatorSetValue)) {
int start = statusIndicatorSetValue.length();
int end = message.indexOf("'", start);
int progress = 0;
try {
progress = Integer.parseInt(message.substring(start, end));
} catch (Exception e) {
}
mProgressDialog.determinateProgress(progress);
}
else if (message.startsWith("'statusindicatorfinish:") || message.startsWith("'error:")) {
mProgressDialog.dismiss();
if (BuildConfig.GOOGLE_PLAY_ENABLED && rateAppController != null)
rateAppController.askUserForRating();
}
}
});
}
}
/**
* return true to pass the message to the native part or false to block the message
*/
private boolean beforeMessageFromWebView(String[] messageAndParam) {
switch (messageAndParam[0]) {
case "BYE":
finishWithProgress();
return false;
case "PRINT":
getMainHandler().post(new Runnable() {
@Override
public void run() {
LOActivity.this.initiatePrint();
}
});
return false;
case "SLIDESHOW":
initiateSlideShow();
return false;
case "SAVE":
copyTempBackToIntent();
sendBroadcast(messageAndParam[0], messageAndParam[1]);
return false;
case "downloadas":
initiateSaveAs(messageAndParam[1]);
return false;
case "uno":
switch (messageAndParam[1]) {
case ".uno:Paste":
return performPaste();
default:
break;
}
break;
case "DIM_SCREEN": {
getMainHandler().post(new Runnable() {
@Override
public void run() {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
});
return false;
}
case "LIGHT_SCREEN": {
getMainHandler().post(new Runnable() {
@Override
public void run() {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
});
return false;
}
case "MOBILEWIZARD": {
switch (messageAndParam[1]) {
case "show":
mMobileWizardVisible = true;
break;
case "hide":
mMobileWizardVisible = false;
break;
}
return false;
}
case "HYPERLINK": {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(messageAndParam[1]));
startActivity(intent);
return false;
}
case "EDITMODE": {
switch (messageAndParam[1]) {
case "on":
mIsEditModeActive = true;
break;
case "off":
mIsEditModeActive = false;
break;
}
return false;
}
case "loadwithpassword": {
mProgressDialog.determinate(R.string.loading);
return true;
}
}
return true;
}
private void initiateSaveAs(String optionsString) {
Map<String, String> optionsMap = new HashMap<>();
String[] options = optionsString.split(" ");
for (String option : options) {
String[] keyValue = option.split("=", 2);
if (keyValue.length == 2)
optionsMap.put(keyValue[0], keyValue[1]);
}
String format = optionsMap.get("format");
String mime = getMimeForFormat(format);
if (format != null && mime != null) {
String filename = optionsMap.get("name");
if (filename == null)
filename = "document." + format;
int requestID = getRequestIDForFormat(format);
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.setType(mime);
intent.putExtra(Intent.EXTRA_TITLE, filename);
intent.putExtra(Intent.EXTRA_LOCAL_ONLY, false);
File folder = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Uri.fromFile(folder).toString());
intent.putExtra("android.content.extra.SHOW_ADVANCED", true);
startActivityForResult(intent, requestID);
}
}
private int getRequestIDForFormat(String format) {
switch (format) {
case "pdf": return REQUEST_SAVEAS_PDF;
case "rtf": return REQUEST_SAVEAS_RTF;
case "odt": return REQUEST_SAVEAS_ODT;
case "odp": return REQUEST_SAVEAS_ODP;
case "ods": return REQUEST_SAVEAS_ODS;
case "docx": return REQUEST_SAVEAS_DOCX;
case "pptx": return REQUEST_SAVEAS_PPTX;
case "xlsx": return REQUEST_SAVEAS_XLSX;
case "doc": return REQUEST_SAVEAS_DOC;
case "ppt": return REQUEST_SAVEAS_PPT;
case "xls": return REQUEST_SAVEAS_XLS;
case "epub": return REQUEST_SAVEAS_EPUB;
}
return 0;
}
private String getMimeForFormat(String format) {
switch(format) {
case "pdf": return "application/pdf";
case "rtf": return "text/rtf";
case "odt": return "application/vnd.oasis.opendocument.text";
case "odp": return "application/vnd.oasis.opendocument.presentation";
case "ods": return "application/vnd.oasis.opendocument.spreadsheet";
case "docx": return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
case "pptx": return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
case "xlsx": return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
case "doc": return "application/msword";
case "ppt": return "application/vnd.ms-powerpoint";
case "xls": return "application/vnd.ms-excel";
case "epub": return "application/epub+zip";
}
return null;
}
private void afterMessageFromWebView(String[] messageAndParameterArray) {
switch (messageAndParameterArray[0]) {
case "uno":
switch (messageAndParameterArray[1]) {
case ".uno:Copy":
case ".uno:Cut":
populateClipboard();
break;
default:
break;
}
break;
default:
break;
}
}
private void initiatePrint() {
PrintManager printManager = (PrintManager) getSystemService(PRINT_SERVICE);
PrintDocumentAdapter printAdapter = new PrintAdapter(LOActivity.this);
printManager.print("Document", printAdapter, new PrintAttributes.Builder().build());
}
private void initiateSlideShow() {
mProgressDialog.indeterminate(R.string.loading);
nativeHandler.post(new Runnable() {
@Override
public void run() {
Log.v(TAG, "saving svg for slideshow by " + Thread.currentThread().getName());
final String slideShowFileUri = new File(LOActivity.this.getCacheDir(), "slideShow.svg").toURI().toString();
LOActivity.this.saveAs(slideShowFileUri, "svg");
LOActivity.this.runOnUiThread(new Runnable() {
@Override
public void run() {
mProgressDialog.dismiss();
Intent slideShowActIntent = new Intent(LOActivity.this, SlideShowActivity.class);
slideShowActIntent.putExtra(SlideShowActivity.SVG_URI_KEY, slideShowFileUri);
LOActivity.this.startActivity(slideShowActIntent);
}
});
}
});
}
/** Send message back to the shell (for example for the cloud save). */
public void sendBroadcast(String event, String data) {
Intent intent = new Intent(LO_ACTIVITY_BROADCAST);
intent.putExtra(LO_ACTION_EVENT, event);
intent.putExtra(LO_ACTION_DATA, data);
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
}
public native void saveAs(String fileUri, String format);
public native boolean getClipboardContent(LokClipboardData aData);
public native void setClipboardContent(LokClipboardData aData);
public native void paste(String mimeType, byte[] data);
public native void postUnoCommand(String command, String arguments, boolean bNotifyWhenFinished);
/// Returns a magic that specifies this application - and this document.
private final String getClipboardMagic() {
return CLIPBOARD_LOOL_SIGNATURE + Long.toString(loadDocumentMillis);
}
/// Needs to be executed after the .uno:Copy / Paste has executed
public final void populateClipboard()
{
File clipboardFile = new File(getApplicationContext().getCacheDir(), CLIPBOARD_FILE_PATH);
if (clipboardFile.exists())
clipboardFile.delete();
LokClipboardData clipboardData = new LokClipboardData();
if (!LOActivity.this.getClipboardContent(clipboardData))
Log.e(TAG, "no clipboard to copy");
else
{
clipboardData.writeToFile(clipboardFile);
String text = clipboardData.getText();
String html = clipboardData.getHtml();
if (html != null) {
int idx = html.indexOf("<meta name=\"generator\" content=\"");
if (idx < 0)
idx = html.indexOf("<meta http-equiv=\"content-type\" content=\"text/html;");
if (idx >= 0) { // inject our magic
StringBuffer newHtml = new StringBuffer(html);
newHtml.insert(idx, "<meta name=\"origin\" content=\"" + getClipboardMagic() + "\"/>\n");
html = newHtml.toString();
}
if (text == null || text.length() == 0)
Log.i(TAG, "set text to clipoard with: text '" + text + "' and html '" + html + "'");
clipData = ClipData.newHtmlText(ClipDescription.MIMETYPE_TEXT_HTML, text, html);
clipboardManager.setPrimaryClip(clipData);
}
}
}
/// Do the paste, and return true if we should short-circuit the paste locally
private final boolean performPaste()
{
clipData = clipboardManager.getPrimaryClip();
ClipDescription clipDesc = clipData != null ? clipData.getDescription() : null;
if (clipDesc != null) {
if (clipDesc.hasMimeType(ClipDescription.MIMETYPE_TEXT_HTML)) {
final String html = clipData.getItemAt(0).getHtmlText();
// Check if the clipboard content was made with the app
if (html.contains(CLIPBOARD_LOOL_SIGNATURE)) {
// Check if the clipboard content is from the same app instance
if (html.contains(getClipboardMagic())) {
Log.i(TAG, "clipboard comes from us - same instance: short circuit it " + html);
return true;
} else {
Log.i(TAG, "clipboard comes from us - other instance: paste from clipboard file");
File clipboardFile = new File(getApplicationContext().getCacheDir(), CLIPBOARD_FILE_PATH);
LokClipboardData clipboardData = null;
if (clipboardFile.exists())
clipboardData = LokClipboardData.createFromFile(clipboardFile);
if (clipboardData != null) {
LOActivity.this.setClipboardContent(clipboardData);
LOActivity.this.postUnoCommand(".uno:Paste", null, false);
} else {
// Couldn't get data from the clipboard file, but we can still paste html
byte[] htmlByteArray = html.getBytes(Charset.forName("UTF-8"));
LOActivity.this.paste("text/html", htmlByteArray);
}
}
} else {
Log.i(TAG, "foreign html '" + html + "'");
byte[] htmlByteArray = html.getBytes(Charset.forName("UTF-8"));
LOActivity.this.paste("text/html", htmlByteArray);
}
}
else if (clipDesc.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
final ClipData.Item clipItem = clipData.getItemAt(0);
String text = clipItem.getText().toString();
byte[] textByteArray = text.getBytes(Charset.forName("UTF-16"));
LOActivity.this.paste("text/plain;charset=utf-16", textByteArray);
}
}
return false;
}
}
/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */