android: Split the actual editing Activity into an own library.

This way, it is more naturally visible what is the actuall app (with the
initial recent documents / file picker) and the editing part.

Change-Id: Ia764f2900939e980f703e3da9f9abd6c0aee7cbb
private/mmeeks/clipboard
Jan Holesovsky 2019-07-11 11:29:13 +02:00
parent fa6e1054a9
commit 2b13c69d75
26 changed files with 821 additions and 729 deletions

6
.gitignore vendored
View File

@ -95,9 +95,9 @@ ios/Mobile/Assets.xcassets/AppIcon.appiconset
BUNDLE-VERSION
# android stuff
/android/app/src/main/assets/dist
/android/app/src/main/assets/hello-world.od*
/android/app/src/main/cpp/CMakeLists.txt
/android/lib/src/main/assets/dist
/android/lib/src/main/assets/hello-world.od*
/android/lib/src/main/cpp/CMakeLists.txt
# backup and temporary editor files: the only convenience rules allowed here.
*~

20
android/.gitignore vendored
View File

@ -3,13 +3,13 @@
/android.iml
/gradle
/local.properties
/app/src/main/assets/etc/
/app/src/main/assets/example.odt
/app/src/main/assets/license.txt
/app/src/main/assets/license.html
/app/src/main/assets/notice.txt
/app/src/main/assets/program/
/app/src/main/assets/share/
/app/src/main/assets/unpack/
/app/src/main/assets/user/
/app/src/main/cpp/lib
/lib/src/main/assets/etc/
/lib/src/main/assets/example.odt
/lib/src/main/assets/license.txt
/lib/src/main/assets/license.html
/lib/src/main/assets/notice.txt
/lib/src/main/assets/program/
/lib/src/main/assets/share/
/lib/src/main/assets/unpack/
/lib/src/main/assets/user/
/lib/src/main/cpp/lib

View File

@ -1,12 +1,12 @@
clean-local:
rm -rf $(abs_top_srcdir)/android/app/src/main/assets/*
rm -rf $(abs_top_srcdir)/android/lib/src/main/assets/*
rm -rf app/build
all-local: app/src/main/assets/templates/untitled.odg \
app/src/main/assets/templates/untitled.odp \
app/src/main/assets/templates/untitled.ods \
app/src/main/assets/templates/untitled.odt
all-local: lib/src/main/assets/templates/untitled.odg \
lib/src/main/assets/templates/untitled.odp \
lib/src/main/assets/templates/untitled.ods \
lib/src/main/assets/templates/untitled.odt
app/src/main/assets/templates/untitled.%: templates/untitled.%
lib/src/main/assets/templates/untitled.%: templates/untitled.%
@mkdir -p $(dir $@)
@cp -a $< $@

View File

@ -1,4 +1,4 @@
/.externalNativeBuild
/app.iml
/build
/liboSettings.gradle
/appSettings.gradle

View File

@ -0,0 +1,12 @@
ext {
liboWorkdir = '@LOBUILDDIR@/workdir'
liboVersionMinor = '@LOOLWSD_VERSION_MAJOR@'
liboAppName = '@APP_NAME@'
liboVendor = '@VENDOR@'
liboInfoURL = '@INFO_URL@'
}
android.defaultConfig {
applicationId '@ANDROID_PACKAGE_NAME@'
versionCode 20
versionName '@LOOLWSD_VERSION@'
}

View File

@ -1,12 +1,12 @@
apply plugin: 'com.android.application'
// buildhost settings - paths and the like
apply from: 'liboSettings.gradle'
apply from: 'appSettings.gradle'
android {
compileSdkVersion 28
defaultConfig {
// applicationId, versionCode and versionName are defined in liboSettings.gradle
// applicationId, versionCode and versionName are defined in appSettings.gradle
minSdkVersion 21
targetSdkVersion 28
}
@ -36,17 +36,6 @@ android {
'proguard-rules.pro'
}
}
sourceSets {
main {
// let gradle pack the shared library into apk
jniLibs.srcDirs = ['src/main/cpp/lib']
}
}
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
}
}
}
repositories {
@ -65,160 +54,5 @@ dependencies {
//before changing the version please see https://issuetracker.google.com/issues/111662669
implementation 'androidx.preference:preference:1.1.0-alpha01'
implementation project(path: ':lib')
}
task copyUnpackAssets(type: Copy) {
description "copies assets that need to be extracted on the device"
into 'src/main/assets/unpack'
into('program') {
from("${liboInstdir}/${liboEtcFolder}/types") {
includes = [
"offapi.rdb",
"oovbaapi.rdb"
]
}
from("${liboInstdir}/${liboUreMiscFolder}") {
includes = ["types.rdb"]
rename 'types.rdb', 'udkapi.rdb'
}
}
into('user/fonts') {
from "${liboInstdir}/share/fonts/truetype"
// Note: restrict list of fonts due to size considerations - no technical reason anymore
// ToDo: fonts would be good candidate for using Expansion Files instead
includes = [
"Liberation*.ttf",
"Caladea-*.ttf",
"Carlito-*.ttf",
"Gen*.ttf",
"opens___.ttf"
]
}
into('etc/fonts') {
from "${liboSrcRoot}/android/source/"
includes = ['fonts.conf']
filter {
String line ->
line.replaceAll(
'@@APPLICATION_ID@@', new String("${android.defaultConfig.applicationId}")
).replaceAll(
// FIXME Avoid the Android system fonts for the moment,
// the huge Noto Sans fonts have terrible impact on the 1st
// start performance.
// The real solution would be to either make fontconfig
// faster, or at least find a way to avoid only the Noto
// Sans, or present a progressbar or something.
// For the moment, we just copy the Roboto font (needed
// for the dialogs; see MainActivity.copyFonts()) and
// remove the system fonts from the config.
'<dir>/system/fonts</dir>', new String("")
)
}
}
}
task copyAssets(type: Copy) {
description "copies assets that can be accessed within the installed apk"
into 'src/main/assets'
from("${liboSrcRoot}/instdir/") {
includes = ["LICENSE.html", "NOTICE"]
rename "LICENSE.html", "license.html"
rename "NOTICE", "notice.txt"
}
from("${liboExampleDocument}") {
rename ".*", "example.odt"
}
into('program') {
from "${liboInstdir}/program"
includes = ['services.rdb', 'services/services.rdb']
into('resource') {
from "${liboInstdir}/${liboSharedResFolder}"
includes = ['*en-US.res']
}
}
into('share') {
from "${liboInstdir}/share"
// Filter data is needed by e.g. the drawingML preset shape import.
includes = ['registry/**', 'filter/**']
}
}
task createFullConfig(type: Copy) {
description "copies various configuration bits into the apk"
into('src/main/assets/share/config')
from("${liboInstdir}/share/config") {
includes = ['soffice.cfg/**', 'images_colibre.zip']
}
}
task createStrippedConfig {
def preserveDir = file("src/main/assets/share/config/soffice.cfg/empty")
outputs.dir "src/main/assets/share/registry/res"
outputs.file preserveDir
doLast {
file('src/main/assets/share/registry/res').mkdirs()
file("src/main/assets/share/config/soffice.cfg").mkdirs()
// just empty file
preserveDir.text = ""
}
}
task createRCfiles {
inputs.file "liboSettings.gradle"
dependsOn copyUnpackAssets, copyAssets
def sofficerc = file('src/main/assets/unpack/program/sofficerc')
def fundamentalrc = file('src/main/assets/program/fundamentalrc')
def bootstraprc = file('src/main/assets/program/bootstraprc')
def unorc = file('src/main/assets/program/unorc')
def versionrc = file('src/main/assets/program/versionrc')
outputs.files sofficerc, fundamentalrc, unorc, bootstraprc, versionrc
doLast {
sofficerc.text = '''\
[Bootstrap]
Logo=1
NativeProgress=1
URE_BOOTSTRAP=file:///assets/program/fundamentalrc
HOME=$APP_DATA_DIR/cache
OSL_SOCKET_PATH=$APP_DATA_DIR/cache
'''.stripIndent()
fundamentalrc.text = '''\
[Bootstrap]
LO_LIB_DIR=file://$APP_DATA_DIR/lib/
BRAND_BASE_DIR=file:///assets
BRAND_SHARE_SUBDIR=share
CONFIGURATION_LAYERS=xcsxcu:${BRAND_BASE_DIR}/share/registry res:${BRAND_BASE_DIR}/share/registry
URE_BIN_DIR=file:///assets/ure/bin/dir/nothing-here/we-can/exec-anyway
'''.stripIndent()
bootstraprc.text = '''\
[Bootstrap]
InstallMode=<installmode>
ProductKey=LibreOffice ''' + "${liboVersionMajor}.${liboVersionMinor}" + '''
UserInstallation=file://$APP_DATA_DIR
'''.stripIndent()
unorc.text = '''\
[Bootstrap]
URE_INTERNAL_LIB_DIR=file://$APP_DATA_DIR/lib/
UNO_TYPES=file://$APP_DATA_DIR/program/udkapi.rdb file://$APP_DATA_DIR/program/offapi.rdb file://$APP_DATA_DIR/program/oovbaapi.rdb
UNO_SERVICES=file:///assets/program/services.rdb file:///assets/program/services/services.rdb
'''.stripIndent()
versionrc.text = '''\
[Version]
AllLanguages=en-US
BuildVersion=
buildid=''' + "${liboGitFullCommit}" + '''
ReferenceOOoMajorMinor=4.1
'''.stripIndent()
}
}
// creating the UI stuff is cheap, don't bother only applying it for the flavor..
preBuild.dependsOn 'createRCfiles',
'createFullConfig'

View File

@ -9,508 +9,9 @@
package org.libreoffice.androidapp;
import android.Manifest;
import android.content.ContentResolver;
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.preference.PreferenceManager;
import android.print.PrintAttributes;
import android.print.PrintDocumentAdapter;
import android.print.PrintManager;
import android.util.Log;
import android.webkit.JavascriptInterface;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
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.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 androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
public class MainActivity extends AppCompatActivity {
final static String TAG = "MainActivity";
private static final String ASSETS_EXTRACTED_PREFS_KEY = "ASSETS_EXTRACTED";
private static final int PERMISSION_READ_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 File mTempFile = null;
private int providerId;
@Nullable
private URI documentUri;
private String urlToLoad;
private WebView mWebView;
private SharedPreferences sPrefs;
private Handler mainHandler;
private boolean isDocEditable = false;
private boolean isDocDebuggable = BuildConfig.DEBUG;
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;
}
}
/** Copies fonts except the NotoSans from the system to our location.
* This is necessary because the NotoSans is huge and fontconfig needs
* ages to parse them.
*/
private static boolean copyFonts(String fromPath, String targetDir) {
try {
File target = new File(targetDir);
if (!target.exists())
target.mkdirs();
File from = new File(fromPath);
File[] files = from.listFiles();
for (File fontFile : files) {
String fontFileName = fontFile.getName();
if (!fontFileName.equals("Roboto-Regular.ttf")) {
Log.i(TAG, "Ignored font file: " + fontFile);
continue;
}
else {
Log.i(TAG, "Copying font file: " + fontFile);
}
// copy the font file over
InputStream in = new FileInputStream(fontFile);
try {
OutputStream out = new FileOutputStream(targetDir + "/" + fontFile.getName());
try {
byte[] buffer = new byte[4096];
int len;
while ((len = in.read(buffer)) > 0) {
out.write(buffer, 0, len);
}
} finally {
out.close();
}
} finally {
in.close();
}
}
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "copyFonts failed: " + e.getMessage());
return false;
}
return true;
}
private void updatePreferences() {
if (sPrefs.getInt(ASSETS_EXTRACTED_PREFS_KEY, 0) != BuildConfig.VERSION_CODE) {
if (copyFromAssets(getAssets(), "unpack", getApplicationInfo().dataDir) &&
copyFonts("/system/fonts", getApplicationInfo().dataDir + "/user/fonts")) {
sPrefs.edit().putInt(ASSETS_EXTRACTED_PREFS_KEY, BuildConfig.VERSION_CODE).apply();
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
sPrefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
updatePreferences();
setContentView(R.layout.activity_main);
AssetManager assetManager = getResources().getAssets();
isDocDebuggable = sPrefs.getBoolean(KEY_ENABLE_SHOW_DEBUG_INFO, false) && BuildConfig.DEBUG;
ApplicationInfo applicationInfo = getApplicationInfo();
String dataDir = applicationInfo.dataDir;
Log.i(TAG, String.format("Initializing LibreOfficeKit, dataDir=%s\n", dataDir));
//redirectStdio(true);
String cacheDir = getApplication().getCacheDir().getAbsolutePath();
String apkFile = getApplication().getPackageResourcePath();
if (getIntent().getData() != null) {
if (getIntent().getData().getScheme().equals(ContentResolver.SCHEME_CONTENT)) {
isDocEditable = false;
Toast.makeText(this, getResources().getString(R.string.temp_file_saving_disabled), Toast.LENGTH_SHORT).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().getPath();
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();
}
createLOOLWSD(dataDir, cacheDir, apkFile, assetManager, urlToLoad);
mWebView = findViewById(R.id.browser);
mWebView.setWebViewClient(new WebViewClient());
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
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if ((getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) {
WebView.setWebContentsDebuggingEnabled(true);
}
}
mainHandler = new Handler(getMainLooper());
}
@Override
protected void onStart() {
super.onStart();
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
Log.i(TAG, "asking for read storage permission");
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
PERMISSION_READ_EXTERNAL_STORAGE);
} else {
loadDocument();
}
}
@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_READ_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();
finish();
break;
}
break;
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
private boolean copyFileToTemp() {
ContentResolver contentResolver = getContentResolver();
FileChannel inputChannel = null;
FileChannel outputChannel = 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 {
AssetFileDescriptor assetFD = contentResolver.openAssetFileDescriptor(getIntent().getData(), "r");
if (assetFD == null) {
Log.e(TAG, "couldn't create assetfiledescriptor from " + getIntent().getDataString());
return false;
}
inputChannel = assetFD.createInputStream().getChannel();
mTempFile = File.createTempFile("LibreOffice", suffix, this.getCacheDir());
outputChannel = new FileOutputStream(mTempFile).getChannel();
long bytesTransferred = 0;
// might not copy all at once, so make sure everything gets copied....
while (bytesTransferred < inputChannel.size()) {
bytesTransferred += outputChannel.transferFrom(inputChannel, bytesTransferred, inputChannel.size());
}
Log.e(TAG, "Success copying " + bytesTransferred + " bytes");
return true;
} finally {
if (inputChannel != null) inputChannel.close();
if (outputChannel != null) outputChannel.close();
}
} catch (FileNotFoundException e) {
return false;
} catch (IOException e) {
return false;
}
}
@Override
protected void onResume() {
super.onResume();
Log.i(TAG, "onResume..");
// check for config change
updatePreferences();
}
@Override
protected void onPause() {
super.onPause();
Log.d(TAG, "onPause() - unload the document");
postMobileMessageNative("BYE");
}
private void loadDocument() {
String finalUrlToLoad = "file:///android_asset/dist/loleaflet.html?file_path=" +
urlToLoad + "&closebutton=1";
if (isDocEditable) {
finalUrlToLoad += "&permission=edit";
} else {
finalUrlToLoad += "&permission=readonly";
}
if (isDocDebuggable) {
finalUrlToLoad += "&debug=true";
}
mWebView.loadUrl(finalUrlToLoad);
}
static {
System.loadLibrary("androidapp");
}
/**
* 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);
if (interceptMsgFromWebView(message)) {
postMobileMessageNative(message);
}
// Going back to document browser on BYE (called when pressing the top left exit button)
if (message.equals("BYE"))
finish();
}
/**
* 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);
}
/**
* Passing message the other way around - from Java to the FakeWebSocket in JS.
*/
void callFakeWebsocketOnMessage(final String message) {
// call from the UI thread
mWebView.post(new Runnable() {
public void run() {
Log.i(TAG, "Forwarding to the WebView: " + message);
mWebView.loadUrl("javascript:window.TheFakeWebSocket.onmessage({'data':" + message + "});");
}
});
}
/**
* return true to pass the message to the native part and false to block the message
*/
boolean interceptMsgFromWebView(String message) {
if (message.equals("PRINT")) {
mainHandler.post(new Runnable() {
@Override
public void run() {
initiatePrint();
}
});
return false;
} else if (message.equals("SLIDESHOW")) {
initiateSlideShow();
return false;
}
return true;
}
private void initiatePrint() {
PrintManager printManager = (PrintManager) getSystemService(PRINT_SERVICE);
PrintDocumentAdapter printAdapter = new PrintAdapter(MainActivity.this);
printManager.print("Document", printAdapter, new PrintAttributes.Builder().build());
}
private void initiateSlideShow() {
final AlertDialog slideShowProgress = new AlertDialog.Builder(this)
.setCancelable(false)
.setView(R.layout.dialog_loading)
.create();
new AsyncTask<Void, Void, String>() {
@Override
protected void onPreExecute() {
super.onPreExecute();
slideShowProgress.show();
}
@Override
protected String doInBackground(Void... voids) {
Log.v(TAG, "saving svg for slideshow by " + Thread.currentThread().getName());
String slideShowFileUri = new File(getCacheDir(), "slideShow.svg").toURI().toString();
saveAs(slideShowFileUri, "svg");
return slideShowFileUri;
}
@Override
protected void onPostExecute(String slideShowFileUri) {
super.onPostExecute(slideShowFileUri);
slideShowProgress.dismiss();
Intent slideShowActIntent = new Intent(MainActivity.this, SlideShowActivity.class);
slideShowActIntent.putExtra(SlideShowActivity.SVG_URI_KEY, slideShowFileUri);
startActivity(slideShowActIntent);
}
}.execute();
}
public native void saveAs(String fileUri, String format);
import org.libreoffice.androidlib.LOActivity;
public class MainActivity extends LOActivity {
}
/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */

View File

@ -32,7 +32,6 @@ import android.os.Handler;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
import android.util.Log;
import android.view.ContextMenu;
@ -70,11 +69,8 @@ import org.libreoffice.androidapp.storage.DocumentProviderSettingsActivity;
import org.libreoffice.androidapp.storage.IDocumentProvider;
import org.libreoffice.androidapp.storage.IFile;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;

3
android/lib/.gitignore vendored 100644
View File

@ -0,0 +1,3 @@
/build
/lib.iml
/libSettings.gradle

View File

@ -0,0 +1,210 @@
apply plugin: 'com.android.library'
// buildhost settings - paths and the like
apply from: 'libSettings.gradle'
android {
compileSdkVersion 28
defaultConfig {
minSdkVersion 21
targetSdkVersion 28
versionCode 1
versionName "1.0"
}
buildTypes {
debug {
ndk {
//abiFilters "x86", "armeabi-v7a", "armeabi"
abiFilters "armeabi-v7a"
}
debuggable true
}
release {
ndk {
abiFilters "armeabi-v7a"
}
minifyEnabled false // FIXME disabled before we get a good proguardRules for callFakeWebsocketOnMessage calling from C++
shrinkResources false // FIXME cannot be enabled when minifyEnabled is turned off
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
sourceSets {
main {
// let gradle pack the shared library into apk
jniLibs.srcDirs = ['src/main/cpp/lib']
}
}
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'com.google.android.material:material:1.1.0-alpha04'
}
task copyUnpackAssets(type: Copy) {
description "copies assets that need to be extracted on the device"
into 'src/main/assets/unpack'
into('program') {
from("${liboInstdir}/${liboEtcFolder}/types") {
includes = [
"offapi.rdb",
"oovbaapi.rdb"
]
}
from("${liboInstdir}/${liboUreMiscFolder}") {
includes = ["types.rdb"]
rename 'types.rdb', 'udkapi.rdb'
}
}
into('user/fonts') {
from "${liboInstdir}/share/fonts/truetype"
// Note: restrict list of fonts due to size considerations - no technical reason anymore
// ToDo: fonts would be good candidate for using Expansion Files instead
includes = [
"Liberation*.ttf",
"Caladea-*.ttf",
"Carlito-*.ttf",
"Gen*.ttf",
"opens___.ttf"
]
}
into('etc/fonts') {
from "${liboSrcRoot}/android/source/"
includes = ['fonts.conf']
filter {
String line ->
line.replaceAll(
'@@APPLICATION_ID@@', new String("${android.defaultConfig.applicationId}")
).replaceAll(
// FIXME Avoid the Android system fonts for the moment,
// the huge Noto Sans fonts have terrible impact on the 1st
// start performance.
// The real solution would be to either make fontconfig
// faster, or at least find a way to avoid only the Noto
// Sans, or present a progressbar or something.
// For the moment, we just copy the Roboto font (needed
// for the dialogs; see LOActivity.copyFonts()) and
// remove the system fonts from the config.
'<dir>/system/fonts</dir>', new String("")
)
}
}
}
task copyAssets(type: Copy) {
description "copies assets that can be accessed within the installed apk"
into 'src/main/assets'
from("${liboSrcRoot}/instdir/") {
includes = ["LICENSE.html", "NOTICE"]
rename "LICENSE.html", "license.html"
rename "NOTICE", "notice.txt"
}
from("${liboExampleDocument}") {
rename ".*", "example.odt"
}
into('program') {
from "${liboInstdir}/program"
includes = ['services.rdb', 'services/services.rdb']
into('resource') {
from "${liboInstdir}/${liboSharedResFolder}"
includes = ['*en-US.res']
}
}
into('share') {
from "${liboInstdir}/share"
// Filter data is needed by e.g. the drawingML preset shape import.
includes = ['registry/**', 'filter/**']
}
}
task createFullConfig(type: Copy) {
description "copies various configuration bits into the apk"
into('src/main/assets/share/config')
from("${liboInstdir}/share/config") {
includes = ['soffice.cfg/**', 'images_colibre.zip']
}
}
task createStrippedConfig {
def preserveDir = file("src/main/assets/share/config/soffice.cfg/empty")
outputs.dir "src/main/assets/share/registry/res"
outputs.file preserveDir
doLast {
file('src/main/assets/share/registry/res').mkdirs()
file("src/main/assets/share/config/soffice.cfg").mkdirs()
// just empty file
preserveDir.text = ""
}
}
task createRCfiles {
inputs.file "libSettings.gradle"
dependsOn copyUnpackAssets, copyAssets
def sofficerc = file('src/main/assets/unpack/program/sofficerc')
def fundamentalrc = file('src/main/assets/program/fundamentalrc')
def bootstraprc = file('src/main/assets/program/bootstraprc')
def unorc = file('src/main/assets/program/unorc')
def versionrc = file('src/main/assets/program/versionrc')
outputs.files sofficerc, fundamentalrc, unorc, bootstraprc, versionrc
doLast {
sofficerc.text = '''\
[Bootstrap]
Logo=1
NativeProgress=1
URE_BOOTSTRAP=file:///assets/program/fundamentalrc
HOME=$APP_DATA_DIR/cache
OSL_SOCKET_PATH=$APP_DATA_DIR/cache
'''.stripIndent()
fundamentalrc.text = '''\
[Bootstrap]
LO_LIB_DIR=file://$APP_DATA_DIR/lib/
BRAND_BASE_DIR=file:///assets
BRAND_SHARE_SUBDIR=share
CONFIGURATION_LAYERS=xcsxcu:${BRAND_BASE_DIR}/share/registry res:${BRAND_BASE_DIR}/share/registry
URE_BIN_DIR=file:///assets/ure/bin/dir/nothing-here/we-can/exec-anyway
'''.stripIndent()
bootstraprc.text = '''\
[Bootstrap]
InstallMode=<installmode>
ProductKey=LibreOffice ''' + "${liboVersionMajor}.${liboVersionMinor}" + '''
UserInstallation=file://$APP_DATA_DIR
'''.stripIndent()
unorc.text = '''\
[Bootstrap]
URE_INTERNAL_LIB_DIR=file://$APP_DATA_DIR/lib/
UNO_TYPES=file://$APP_DATA_DIR/program/udkapi.rdb file://$APP_DATA_DIR/program/offapi.rdb file://$APP_DATA_DIR/program/oovbaapi.rdb
UNO_SERVICES=file:///assets/program/services.rdb file:///assets/program/services/services.rdb
'''.stripIndent()
versionrc.text = '''\
[Version]
AllLanguages=en-US
BuildVersion=
buildid=''' + "${liboGitFullCommit}" + '''
ReferenceOOoMajorMinor=4.1
'''.stripIndent()
}
}
// creating the UI stuff is cheap, don't bother only applying it for the flavor..
preBuild.dependsOn 'createRCfiles',
'createFullConfig'

View File

@ -1,22 +1,11 @@
ext {
liboSrcRoot = '@LOBUILDDIR@'
liboWorkdir = '@LOBUILDDIR@/workdir'
liboInstdir = '@LOBUILDDIR@/instdir'
liboEtcFolder = 'program'
liboUreMiscFolder = 'program'
liboSharedResFolder = 'program/resource'
liboUREJavaFolder = 'program/classes'
liboShareJavaFolder = 'program/classes'
liboExampleDocument = '@LOBUILDDIR@/android/default-document/example.odt'
liboVersionMajor = '@LOOLWSD_VERSION_MAJOR@'
liboVersionMinor = '@LOOLWSD_VERSION_MAJOR@'
liboGitFullCommit = '@LOOLWSD_VERSION_HASH@'
liboAppName = '@APP_NAME@'
liboVendor = '@VENDOR@'
liboInfoURL = '@INFO_URL@'
}
android.defaultConfig {
applicationId '@ANDROID_PACKAGE_NAME@'
versionCode 1
versionName '@LOOLWSD_VERSION@'
}

21
android/lib/proguard-rules.pro vendored 100644
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.libreoffice.androidlib" />

View File

@ -55,7 +55,7 @@ JNI_OnLoad(JavaVM* vm, void*) {
return JNI_VERSION_1_6;
}
static void send2JS(jclass mainActivityClz, jobject mainActivityObj, const std::vector<char>& buffer)
static void send2JS(jclass loActivityClz, jobject loActivityObj, const std::vector<char>& buffer)
{
LOG_DBG("Send to JS: " << LOOLProtocol::getAbbreviatedMessage(buffer.data(), buffer.size()));
@ -133,8 +133,8 @@ static void send2JS(jclass mainActivityClz, jobject mainActivityObj, const std::
}
jstring jstr = env->NewStringUTF(js.c_str());
jmethodID callFakeWebsocket = env->GetMethodID(mainActivityClz, "callFakeWebsocketOnMessage", "(Ljava/lang/String;)V");
env->CallVoidMethod(mainActivityObj, callFakeWebsocket, jstr);
jmethodID callFakeWebsocket = env->GetMethodID(loActivityClz, "callFakeWebsocketOnMessage", "(Ljava/lang/String;)V");
env->CallVoidMethod(loActivityObj, callFakeWebsocket, jstr);
if (env->ExceptionCheck())
env->ExceptionDescribe();
@ -144,7 +144,7 @@ static void send2JS(jclass mainActivityClz, jobject mainActivityObj, const std::
/// Handle a message from JavaScript.
extern "C" JNIEXPORT void JNICALL
Java_org_libreoffice_androidapp_MainActivity_postMobileMessageNative(JNIEnv *env, jobject instance, jstring message)
Java_org_libreoffice_androidlib_LOActivity_postMobileMessageNative(JNIEnv *env, jobject instance, jstring message)
{
const char *string_value = env->GetStringUTFChars(message, nullptr);
@ -172,10 +172,10 @@ Java_org_libreoffice_androidapp_MainActivity_postMobileMessageNative(JNIEnv *env
// Start another thread to read responses and forward them to the JavaScript
jclass clz = env->GetObjectClass(instance);
jclass mainActivityClz = (jclass) env->NewGlobalRef(clz);
jobject mainActivityObj = env->NewGlobalRef(instance);
jclass loActivityClz = (jclass) env->NewGlobalRef(clz);
jobject loActivityObj = env->NewGlobalRef(instance);
std::thread([mainActivityClz, mainActivityObj, currentFakeClientFd]
std::thread([loActivityClz, loActivityObj, currentFakeClientFd]
{
Util::setThreadName("app2js");
while (true)
@ -215,7 +215,7 @@ Java_org_libreoffice_androidapp_MainActivity_postMobileMessageNative(JNIEnv *env
return;
std::vector<char> buf(n);
n = fakeSocketRead(currentFakeClientFd, buf.data(), n);
send2JS(mainActivityClz, mainActivityObj, buf);
send2JS(loActivityClz, loActivityObj, buf);
}
}
else
@ -268,7 +268,7 @@ extern "C" jboolean libreofficekit_initialize(JNIEnv* env, jstring dataDir, jstr
/// Create the LOOLWSD instance.
extern "C" JNIEXPORT void JNICALL
Java_org_libreoffice_androidapp_MainActivity_createLOOLWSD(JNIEnv *env, jobject, jstring dataDir, jstring cacheDir, jstring apkFile, jobject assetManager, jstring loadFileURL)
Java_org_libreoffice_androidlib_LOActivity_createLOOLWSD(JNIEnv *env, jobject, jstring dataDir, jstring cacheDir, jstring apkFile, jobject assetManager, jstring loadFileURL)
{
fileURL = std::string(env->GetStringUTFChars(loadFileURL, nullptr));
@ -309,7 +309,7 @@ Java_org_libreoffice_androidapp_MainActivity_createLOOLWSD(JNIEnv *env, jobject,
extern "C"
JNIEXPORT void JNICALL
Java_org_libreoffice_androidapp_MainActivity_saveAs(JNIEnv *env, jobject instance,
Java_org_libreoffice_androidlib_LOActivity_saveAs(JNIEnv *env, jobject instance,
jstring fileUri_, jstring format_) {
const char *fileUri = env->GetStringUTFChars(fileUri_, 0);
const char *format = env->GetStringUTFChars(format_, 0);

View File

@ -0,0 +1,515 @@
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
/*
* This file is part of the LibreOffice project.
*
* 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.content.ContentResolver;
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.Handler;
import android.preference.PreferenceManager;
import android.print.PrintAttributes;
import android.print.PrintDocumentAdapter;
import android.print.PrintManager;
import android.util.Log;
import android.webkit.JavascriptInterface;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
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.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 androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
public class LOActivity extends AppCompatActivity {
final static String TAG = "LOActivity";
private static final String ASSETS_EXTRACTED_PREFS_KEY = "ASSETS_EXTRACTED";
private static final int PERMISSION_READ_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 File mTempFile = null;
private int providerId;
@Nullable
private URI documentUri;
private String urlToLoad;
private WebView mWebView;
private SharedPreferences sPrefs;
private Handler mainHandler;
private boolean isDocEditable = false;
private boolean isDocDebuggable = BuildConfig.DEBUG;
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;
}
}
/** Copies fonts except the NotoSans from the system to our location.
* This is necessary because the NotoSans is huge and fontconfig needs
* ages to parse them.
*/
private static boolean copyFonts(String fromPath, String targetDir) {
try {
File target = new File(targetDir);
if (!target.exists())
target.mkdirs();
File from = new File(fromPath);
File[] files = from.listFiles();
for (File fontFile : files) {
String fontFileName = fontFile.getName();
if (!fontFileName.equals("Roboto-Regular.ttf")) {
Log.i(TAG, "Ignored font file: " + fontFile);
continue;
}
else {
Log.i(TAG, "Copying font file: " + fontFile);
}
// copy the font file over
InputStream in = new FileInputStream(fontFile);
try {
OutputStream out = new FileOutputStream(targetDir + "/" + fontFile.getName());
try {
byte[] buffer = new byte[4096];
int len;
while ((len = in.read(buffer)) > 0) {
out.write(buffer, 0, len);
}
} finally {
out.close();
}
} finally {
in.close();
}
}
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "copyFonts failed: " + e.getMessage());
return false;
}
return true;
}
private void updatePreferences() {
if (sPrefs.getInt(ASSETS_EXTRACTED_PREFS_KEY, 0) != BuildConfig.VERSION_CODE) {
if (copyFromAssets(getAssets(), "unpack", getApplicationInfo().dataDir) &&
copyFonts("/system/fonts", getApplicationInfo().dataDir + "/user/fonts")) {
sPrefs.edit().putInt(ASSETS_EXTRACTED_PREFS_KEY, BuildConfig.VERSION_CODE).apply();
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
sPrefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
updatePreferences();
setContentView(R.layout.activity_main);
AssetManager assetManager = getResources().getAssets();
isDocDebuggable = sPrefs.getBoolean(KEY_ENABLE_SHOW_DEBUG_INFO, false) && BuildConfig.DEBUG;
ApplicationInfo applicationInfo = getApplicationInfo();
String dataDir = applicationInfo.dataDir;
Log.i(TAG, String.format("Initializing LibreOfficeKit, dataDir=%s\n", dataDir));
//redirectStdio(true);
String cacheDir = getApplication().getCacheDir().getAbsolutePath();
String apkFile = getApplication().getPackageResourcePath();
if (getIntent().getData() != null) {
if (getIntent().getData().getScheme().equals(ContentResolver.SCHEME_CONTENT)) {
isDocEditable = false;
Toast.makeText(this, getResources().getString(R.string.temp_file_saving_disabled), Toast.LENGTH_SHORT).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().getPath();
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();
}
createLOOLWSD(dataDir, cacheDir, apkFile, assetManager, urlToLoad);
mWebView = findViewById(R.id.browser);
mWebView.setWebViewClient(new WebViewClient());
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
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if ((getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) {
WebView.setWebContentsDebuggingEnabled(true);
}
}
mainHandler = new Handler(getMainLooper());
}
@Override
protected void onStart() {
super.onStart();
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
Log.i(TAG, "asking for read storage permission");
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
PERMISSION_READ_EXTERNAL_STORAGE);
} else {
loadDocument();
}
}
@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_READ_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();
finish();
break;
}
break;
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
private boolean copyFileToTemp() {
ContentResolver contentResolver = getContentResolver();
FileChannel inputChannel = null;
FileChannel outputChannel = 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 {
AssetFileDescriptor assetFD = contentResolver.openAssetFileDescriptor(getIntent().getData(), "r");
if (assetFD == null) {
Log.e(TAG, "couldn't create assetfiledescriptor from " + getIntent().getDataString());
return false;
}
inputChannel = assetFD.createInputStream().getChannel();
mTempFile = File.createTempFile("LibreOffice", suffix, this.getCacheDir());
outputChannel = new FileOutputStream(mTempFile).getChannel();
long bytesTransferred = 0;
// might not copy all at once, so make sure everything gets copied....
while (bytesTransferred < inputChannel.size()) {
bytesTransferred += outputChannel.transferFrom(inputChannel, bytesTransferred, inputChannel.size());
}
Log.e(TAG, "Success copying " + bytesTransferred + " bytes");
return true;
} finally {
if (inputChannel != null) inputChannel.close();
if (outputChannel != null) outputChannel.close();
}
} catch (FileNotFoundException e) {
return false;
} catch (IOException e) {
return false;
}
}
@Override
protected void onResume() {
super.onResume();
Log.i(TAG, "onResume..");
// check for config change
updatePreferences();
}
@Override
protected void onPause() {
super.onPause();
Log.d(TAG, "onPause() - unload the document");
postMobileMessageNative("BYE");
}
private void loadDocument() {
String finalUrlToLoad = "file:///android_asset/dist/loleaflet.html?file_path=" +
urlToLoad + "&closebutton=1";
if (isDocEditable) {
finalUrlToLoad += "&permission=edit";
} else {
finalUrlToLoad += "&permission=readonly";
}
if (isDocDebuggable) {
finalUrlToLoad += "&debug=true";
}
mWebView.loadUrl(finalUrlToLoad);
}
static {
System.loadLibrary("androidapp");
}
/**
* 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);
if (interceptMsgFromWebView(message)) {
postMobileMessageNative(message);
}
// Going back to document browser on BYE (called when pressing the top left exit button)
if (message.equals("BYE"))
finish();
}
/**
* 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);
}
/**
* Passing message the other way around - from Java to the FakeWebSocket in JS.
*/
void callFakeWebsocketOnMessage(final String message) {
// call from the UI thread
mWebView.post(new Runnable() {
public void run() {
Log.i(TAG, "Forwarding to the WebView: " + message);
mWebView.loadUrl("javascript:window.TheFakeWebSocket.onmessage({'data':" + message + "});");
}
});
}
/**
* return true to pass the message to the native part and false to block the message
*/
boolean interceptMsgFromWebView(String message) {
if (message.equals("PRINT")) {
mainHandler.post(new Runnable() {
@Override
public void run() {
initiatePrint();
}
});
return false;
} else if (message.equals("SLIDESHOW")) {
initiateSlideShow();
return false;
}
return true;
}
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() {
final AlertDialog slideShowProgress = new AlertDialog.Builder(this)
.setCancelable(false)
.setView(R.layout.dialog_loading)
.create();
new AsyncTask<Void, Void, String>() {
@Override
protected void onPreExecute() {
super.onPreExecute();
slideShowProgress.show();
}
@Override
protected String doInBackground(Void... voids) {
Log.v(TAG, "saving svg for slideshow by " + Thread.currentThread().getName());
String slideShowFileUri = new File(getCacheDir(), "slideShow.svg").toURI().toString();
saveAs(slideShowFileUri, "svg");
return slideShowFileUri;
}
@Override
protected void onPostExecute(String slideShowFileUri) {
super.onPostExecute(slideShowFileUri);
slideShowProgress.dismiss();
Intent slideShowActIntent = new Intent(LOActivity.this, SlideShowActivity.class);
slideShowActIntent.putExtra(SlideShowActivity.SVG_URI_KEY, slideShowFileUri);
startActivity(slideShowActIntent);
}
}.execute();
}
public native void saveAs(String fileUri, String format);
}
/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */

View File

@ -7,7 +7,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.libreoffice.androidapp;
package org.libreoffice.androidlib;
import android.os.Bundle;
import android.os.CancellationSignal;
@ -28,9 +28,9 @@ import java.util.Objects;
public class PrintAdapter extends PrintDocumentAdapter {
private File printDocFile;
private MainActivity mainActivity;
private LOActivity mainActivity;
PrintAdapter(MainActivity mainActivity) {
PrintAdapter(LOActivity mainActivity) {
this.mainActivity = mainActivity;
}

View File

@ -7,7 +7,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.libreoffice.androidapp;
package org.libreoffice.androidlib;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

View File

@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
tools:context=".LOActivity">
<WebView
android:id="@+id/browser"

View File

@ -0,0 +1,8 @@
<resources>
<string name="temp_file_saving_disabled">This file is read-only, saving is disabled.</string>
<string name="storage_permission_required">Storage permission is required</string>
<string name="failed_to_load_file">Failed to determine the file to load</string>
<!-- Loading SlideShow Dialog Strings -->
<string name="loading">Loading...</string>
</resources>

View File

@ -1 +1 @@
include ':app'
include ':app', ':lib'

View File

@ -790,8 +790,9 @@ AS_IF([test "$ENABLE_IOSAPP" = "true"],
AC_SUBST(IOSAPP_FONTS)
AC_CONFIG_FILES([Makefile
android/app/liboSettings.gradle
android/app/src/main/cpp/CMakeLists.txt
android/app/appSettings.gradle
android/lib/libSettings.gradle
android/lib/src/main/cpp/CMakeLists.txt
android/Makefile
gtk/Makefile
ios/config.h

View File

@ -152,13 +152,13 @@ build-loleaflet: | $(LOLEAFLET_L10N_DST) \
$(builddir)/dist/loleaflet.html
@echo "build loleaflet completed"
if ENABLE_ANDROIDAPP
@rm -rf $(abs_top_srcdir)/android/app/src/main/assets/dist
@cp -a $(builddir)/dist $(abs_top_srcdir)/android/app/src/main/assets/
@if test -d "$(APP_BRANDING_DIR)" ; then cp -a "$(APP_BRANDING_DIR)/branding.css" "$(APP_BRANDING_DIR)/branding.js" $(abs_top_srcdir)/android/app/src/main/assets/dist/ ; else touch $(abs_top_srcdir)/android/app/src/main/assets/dist/branding.css ; fi
@if test -d "$(APP_BRANDING_DIR)" ; then cp -a "$(APP_BRANDING_DIR)"/*.svg $(abs_top_srcdir)/android/app/src/main/assets/dist/images/ ; fi
@if test -d "$(APP_BRANDING_DIR)" ; then cp -a "$(APP_BRANDING_DIR)/toolbar-bg-logo.svg" $(abs_top_srcdir)/android/app/src/main/assets/dist/images/toolbar-bg.svg ; fi
@rm -rf $(abs_top_srcdir)/android/lib/src/main/assets/dist
@cp -a $(builddir)/dist $(abs_top_srcdir)/android/lib/src/main/assets/
@if test -d "$(APP_BRANDING_DIR)" ; then cp -a "$(APP_BRANDING_DIR)/branding.css" "$(APP_BRANDING_DIR)/branding.js" $(abs_top_srcdir)/android/lib/src/main/assets/dist/ ; else touch $(abs_top_srcdir)/android/lib/src/main/assets/dist/branding.css ; fi
@if test -d "$(APP_BRANDING_DIR)" ; then cp -a "$(APP_BRANDING_DIR)"/*.svg $(abs_top_srcdir)/android/lib/src/main/assets/dist/images/ ; fi
@if test -d "$(APP_BRANDING_DIR)" ; then cp -a "$(APP_BRANDING_DIR)/toolbar-bg-logo.svg" $(abs_top_srcdir)/android/lib/src/main/assets/dist/images/toolbar-bg.svg ; fi
@echo
@echo "Copied JS, HTML and CSS to the Android project (android/app/src/main/assets/dist)."
@echo "Copied JS, HTML and CSS to the Android project (android/lib/src/main/assets/dist)."
@echo
@echo " Now you need to build the actual .apk from Android Studio:"
@echo " Just open the 'android' subdir as a project there and build."