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: Ia764f2900939e980f703e3da9f9abd6c0aee7cbbprivate/mmeeks/clipboard
parent
fa6e1054a9
commit
2b13c69d75
|
@ -95,9 +95,9 @@ ios/Mobile/Assets.xcassets/AppIcon.appiconset
|
||||||
BUNDLE-VERSION
|
BUNDLE-VERSION
|
||||||
|
|
||||||
# android stuff
|
# android stuff
|
||||||
/android/app/src/main/assets/dist
|
/android/lib/src/main/assets/dist
|
||||||
/android/app/src/main/assets/hello-world.od*
|
/android/lib/src/main/assets/hello-world.od*
|
||||||
/android/app/src/main/cpp/CMakeLists.txt
|
/android/lib/src/main/cpp/CMakeLists.txt
|
||||||
|
|
||||||
# backup and temporary editor files: the only convenience rules allowed here.
|
# backup and temporary editor files: the only convenience rules allowed here.
|
||||||
*~
|
*~
|
||||||
|
|
|
@ -3,13 +3,13 @@
|
||||||
/android.iml
|
/android.iml
|
||||||
/gradle
|
/gradle
|
||||||
/local.properties
|
/local.properties
|
||||||
/app/src/main/assets/etc/
|
/lib/src/main/assets/etc/
|
||||||
/app/src/main/assets/example.odt
|
/lib/src/main/assets/example.odt
|
||||||
/app/src/main/assets/license.txt
|
/lib/src/main/assets/license.txt
|
||||||
/app/src/main/assets/license.html
|
/lib/src/main/assets/license.html
|
||||||
/app/src/main/assets/notice.txt
|
/lib/src/main/assets/notice.txt
|
||||||
/app/src/main/assets/program/
|
/lib/src/main/assets/program/
|
||||||
/app/src/main/assets/share/
|
/lib/src/main/assets/share/
|
||||||
/app/src/main/assets/unpack/
|
/lib/src/main/assets/unpack/
|
||||||
/app/src/main/assets/user/
|
/lib/src/main/assets/user/
|
||||||
/app/src/main/cpp/lib
|
/lib/src/main/cpp/lib
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
clean-local:
|
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
|
rm -rf app/build
|
||||||
|
|
||||||
all-local: app/src/main/assets/templates/untitled.odg \
|
all-local: lib/src/main/assets/templates/untitled.odg \
|
||||||
app/src/main/assets/templates/untitled.odp \
|
lib/src/main/assets/templates/untitled.odp \
|
||||||
app/src/main/assets/templates/untitled.ods \
|
lib/src/main/assets/templates/untitled.ods \
|
||||||
app/src/main/assets/templates/untitled.odt
|
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 $@)
|
@mkdir -p $(dir $@)
|
||||||
@cp -a $< $@
|
@cp -a $< $@
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/.externalNativeBuild
|
/.externalNativeBuild
|
||||||
/app.iml
|
/app.iml
|
||||||
/build
|
/build
|
||||||
/liboSettings.gradle
|
/appSettings.gradle
|
||||||
|
|
|
@ -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@'
|
||||||
|
}
|
|
@ -1,12 +1,12 @@
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
// buildhost settings - paths and the like
|
// buildhost settings - paths and the like
|
||||||
apply from: 'liboSettings.gradle'
|
apply from: 'appSettings.gradle'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 28
|
compileSdkVersion 28
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// applicationId, versionCode and versionName are defined in liboSettings.gradle
|
// applicationId, versionCode and versionName are defined in appSettings.gradle
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 28
|
targetSdkVersion 28
|
||||||
}
|
}
|
||||||
|
@ -36,17 +36,6 @@ android {
|
||||||
'proguard-rules.pro'
|
'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 {
|
repositories {
|
||||||
|
@ -65,160 +54,5 @@ dependencies {
|
||||||
|
|
||||||
//before changing the version please see https://issuetracker.google.com/issues/111662669
|
//before changing the version please see https://issuetracker.google.com/issues/111662669
|
||||||
implementation 'androidx.preference:preference:1.1.0-alpha01'
|
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'
|
|
||||||
|
|
|
@ -9,508 +9,9 @@
|
||||||
|
|
||||||
package org.libreoffice.androidapp;
|
package org.libreoffice.androidapp;
|
||||||
|
|
||||||
import android.Manifest;
|
import org.libreoffice.androidlib.LOActivity;
|
||||||
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);
|
|
||||||
|
|
||||||
|
public class MainActivity extends LOActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */
|
/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */
|
||||||
|
|
|
@ -32,7 +32,6 @@ import android.os.Handler;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
import android.text.InputType;
|
|
||||||
import android.text.TextWatcher;
|
import android.text.TextWatcher;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.ContextMenu;
|
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.IDocumentProvider;
|
||||||
import org.libreoffice.androidapp.storage.IFile;
|
import org.libreoffice.androidapp.storage.IFile;
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
|
||||||
import java.io.BufferedOutputStream;
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileFilter;
|
import java.io.FileFilter;
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.FilenameFilter;
|
import java.io.FilenameFilter;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
/build
|
||||||
|
/lib.iml
|
||||||
|
/libSettings.gradle
|
|
@ -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'
|
|
@ -1,22 +1,11 @@
|
||||||
ext {
|
ext {
|
||||||
liboSrcRoot = '@LOBUILDDIR@'
|
liboSrcRoot = '@LOBUILDDIR@'
|
||||||
liboWorkdir = '@LOBUILDDIR@/workdir'
|
|
||||||
liboInstdir = '@LOBUILDDIR@/instdir'
|
liboInstdir = '@LOBUILDDIR@/instdir'
|
||||||
liboEtcFolder = 'program'
|
liboEtcFolder = 'program'
|
||||||
liboUreMiscFolder = 'program'
|
liboUreMiscFolder = 'program'
|
||||||
liboSharedResFolder = 'program/resource'
|
liboSharedResFolder = 'program/resource'
|
||||||
liboUREJavaFolder = 'program/classes'
|
|
||||||
liboShareJavaFolder = 'program/classes'
|
|
||||||
liboExampleDocument = '@LOBUILDDIR@/android/default-document/example.odt'
|
liboExampleDocument = '@LOBUILDDIR@/android/default-document/example.odt'
|
||||||
liboVersionMajor = '@LOOLWSD_VERSION_MAJOR@'
|
liboVersionMajor = '@LOOLWSD_VERSION_MAJOR@'
|
||||||
liboVersionMinor = '@LOOLWSD_VERSION_MAJOR@'
|
liboVersionMinor = '@LOOLWSD_VERSION_MAJOR@'
|
||||||
liboGitFullCommit = '@LOOLWSD_VERSION_HASH@'
|
liboGitFullCommit = '@LOOLWSD_VERSION_HASH@'
|
||||||
liboAppName = '@APP_NAME@'
|
|
||||||
liboVendor = '@VENDOR@'
|
|
||||||
liboInfoURL = '@INFO_URL@'
|
|
||||||
}
|
|
||||||
android.defaultConfig {
|
|
||||||
applicationId '@ANDROID_PACKAGE_NAME@'
|
|
||||||
versionCode 1
|
|
||||||
versionName '@LOOLWSD_VERSION@'
|
|
||||||
}
|
}
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="org.libreoffice.androidlib" />
|
|
@ -55,7 +55,7 @@ JNI_OnLoad(JavaVM* vm, void*) {
|
||||||
return JNI_VERSION_1_6;
|
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()));
|
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());
|
jstring jstr = env->NewStringUTF(js.c_str());
|
||||||
jmethodID callFakeWebsocket = env->GetMethodID(mainActivityClz, "callFakeWebsocketOnMessage", "(Ljava/lang/String;)V");
|
jmethodID callFakeWebsocket = env->GetMethodID(loActivityClz, "callFakeWebsocketOnMessage", "(Ljava/lang/String;)V");
|
||||||
env->CallVoidMethod(mainActivityObj, callFakeWebsocket, jstr);
|
env->CallVoidMethod(loActivityObj, callFakeWebsocket, jstr);
|
||||||
|
|
||||||
if (env->ExceptionCheck())
|
if (env->ExceptionCheck())
|
||||||
env->ExceptionDescribe();
|
env->ExceptionDescribe();
|
||||||
|
@ -144,7 +144,7 @@ static void send2JS(jclass mainActivityClz, jobject mainActivityObj, const std::
|
||||||
|
|
||||||
/// Handle a message from JavaScript.
|
/// Handle a message from JavaScript.
|
||||||
extern "C" JNIEXPORT void JNICALL
|
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);
|
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
|
// Start another thread to read responses and forward them to the JavaScript
|
||||||
jclass clz = env->GetObjectClass(instance);
|
jclass clz = env->GetObjectClass(instance);
|
||||||
jclass mainActivityClz = (jclass) env->NewGlobalRef(clz);
|
jclass loActivityClz = (jclass) env->NewGlobalRef(clz);
|
||||||
jobject mainActivityObj = env->NewGlobalRef(instance);
|
jobject loActivityObj = env->NewGlobalRef(instance);
|
||||||
|
|
||||||
std::thread([mainActivityClz, mainActivityObj, currentFakeClientFd]
|
std::thread([loActivityClz, loActivityObj, currentFakeClientFd]
|
||||||
{
|
{
|
||||||
Util::setThreadName("app2js");
|
Util::setThreadName("app2js");
|
||||||
while (true)
|
while (true)
|
||||||
|
@ -215,7 +215,7 @@ Java_org_libreoffice_androidapp_MainActivity_postMobileMessageNative(JNIEnv *env
|
||||||
return;
|
return;
|
||||||
std::vector<char> buf(n);
|
std::vector<char> buf(n);
|
||||||
n = fakeSocketRead(currentFakeClientFd, buf.data(), n);
|
n = fakeSocketRead(currentFakeClientFd, buf.data(), n);
|
||||||
send2JS(mainActivityClz, mainActivityObj, buf);
|
send2JS(loActivityClz, loActivityObj, buf);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -268,7 +268,7 @@ extern "C" jboolean libreofficekit_initialize(JNIEnv* env, jstring dataDir, jstr
|
||||||
|
|
||||||
/// Create the LOOLWSD instance.
|
/// Create the LOOLWSD instance.
|
||||||
extern "C" JNIEXPORT void JNICALL
|
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));
|
fileURL = std::string(env->GetStringUTFChars(loadFileURL, nullptr));
|
||||||
|
|
||||||
|
@ -309,7 +309,7 @@ Java_org_libreoffice_androidapp_MainActivity_createLOOLWSD(JNIEnv *env, jobject,
|
||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT void JNICALL
|
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_) {
|
jstring fileUri_, jstring format_) {
|
||||||
const char *fileUri = env->GetStringUTFChars(fileUri_, 0);
|
const char *fileUri = env->GetStringUTFChars(fileUri_, 0);
|
||||||
const char *format = env->GetStringUTFChars(format_, 0);
|
const char *format = env->GetStringUTFChars(format_, 0);
|
|
@ -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: */
|
|
@ -7,7 +7,7 @@
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
* 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.Bundle;
|
||||||
import android.os.CancellationSignal;
|
import android.os.CancellationSignal;
|
||||||
|
@ -28,9 +28,9 @@ import java.util.Objects;
|
||||||
public class PrintAdapter extends PrintDocumentAdapter {
|
public class PrintAdapter extends PrintDocumentAdapter {
|
||||||
|
|
||||||
private File printDocFile;
|
private File printDocFile;
|
||||||
private MainActivity mainActivity;
|
private LOActivity mainActivity;
|
||||||
|
|
||||||
PrintAdapter(MainActivity mainActivity) {
|
PrintAdapter(LOActivity mainActivity) {
|
||||||
this.mainActivity = mainActivity;
|
this.mainActivity = mainActivity;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
* 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.annotation.NonNull;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
|
@ -4,7 +4,7 @@
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context=".MainActivity">
|
tools:context=".LOActivity">
|
||||||
|
|
||||||
<WebView
|
<WebView
|
||||||
android:id="@+id/browser"
|
android:id="@+id/browser"
|
|
@ -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>
|
|
@ -1 +1 @@
|
||||||
include ':app'
|
include ':app', ':lib'
|
||||||
|
|
|
@ -790,8 +790,9 @@ AS_IF([test "$ENABLE_IOSAPP" = "true"],
|
||||||
AC_SUBST(IOSAPP_FONTS)
|
AC_SUBST(IOSAPP_FONTS)
|
||||||
|
|
||||||
AC_CONFIG_FILES([Makefile
|
AC_CONFIG_FILES([Makefile
|
||||||
android/app/liboSettings.gradle
|
android/app/appSettings.gradle
|
||||||
android/app/src/main/cpp/CMakeLists.txt
|
android/lib/libSettings.gradle
|
||||||
|
android/lib/src/main/cpp/CMakeLists.txt
|
||||||
android/Makefile
|
android/Makefile
|
||||||
gtk/Makefile
|
gtk/Makefile
|
||||||
ios/config.h
|
ios/config.h
|
||||||
|
|
|
@ -152,13 +152,13 @@ build-loleaflet: | $(LOLEAFLET_L10N_DST) \
|
||||||
$(builddir)/dist/loleaflet.html
|
$(builddir)/dist/loleaflet.html
|
||||||
@echo "build loleaflet completed"
|
@echo "build loleaflet completed"
|
||||||
if ENABLE_ANDROIDAPP
|
if ENABLE_ANDROIDAPP
|
||||||
@rm -rf $(abs_top_srcdir)/android/app/src/main/assets/dist
|
@rm -rf $(abs_top_srcdir)/android/lib/src/main/assets/dist
|
||||||
@cp -a $(builddir)/dist $(abs_top_srcdir)/android/app/src/main/assets/
|
@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/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)/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/app/src/main/assets/dist/images/ ; 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/app/src/main/assets/dist/images/toolbar-bg.svg ; 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
|
||||||
@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
|
||||||
@echo " Now you need to build the actual .apk from Android Studio:"
|
@echo " Now you need to build the actual .apk from Android Studio:"
|
||||||
@echo " Just open the 'android' subdir as a project there and build."
|
@echo " Just open the 'android' subdir as a project there and build."
|
||||||
|
|
Loading…
Reference in New Issue