.gitignore | 6 android/.gitignore | 20 android/Makefile.am | 12 android/app/.gitignore | 2 android/app/appSettings.gradle.in | 12 android/app/build.gradle | 172 --- android/app/src/main/java/org/libreoffice/androidapp/MainActivity.java | 503 --------- android/app/src/main/java/org/libreoffice/androidapp/ui/LibreOfficeUIActivity.java | 4 android/lib/.gitignore | 3 android/lib/build.gradle | 210 ++++ android/lib/libSettings.gradle.in | 11 android/lib/proguard-rules.pro | 21 android/lib/src/main/AndroidManifest.xml | 2 android/lib/src/main/cpp/androidapp.cpp | 20 android/lib/src/main/java/org/libreoffice/androidlib/LOActivity.java | 515 ++++++++++ android/lib/src/main/java/org/libreoffice/androidlib/PrintAdapter.java | 6 android/lib/src/main/java/org/libreoffice/androidlib/SlideShowActivity.java | 2 android/lib/src/main/res/layout/activity_main.xml | 2 android/lib/src/main/res/values/strings.xml | 8 android/settings.gradle | 2 configure.ac | 5 loleaflet/Makefile.am | 12 22 files changed, 821 insertions(+), 729 deletions(-)
New commits: commit 2b13c69d75476bcc3a38bd0cd9798187e74ed4c4 Author: Jan Holesovsky <ke...@collabora.com> AuthorDate: Thu Jul 11 11:29:13 2019 +0200 Commit: Jan Holesovsky <ke...@collabora.com> CommitDate: Fri Jul 12 11:52:56 2019 +0200 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 diff --git a/.gitignore b/.gitignore index 52ee34543..eea283edd 100644 --- a/.gitignore +++ b/.gitignore @@ -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. *~ diff --git a/android/.gitignore b/android/.gitignore index 6e50fa55a..9bdedd758 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -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 diff --git a/android/Makefile.am b/android/Makefile.am index 9d27873df..fb3076a46 100644 --- a/android/Makefile.am +++ b/android/Makefile.am @@ -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 $< $@ diff --git a/android/app/.gitignore b/android/app/.gitignore index 238a7c9a1..0e5159da5 100644 --- a/android/app/.gitignore +++ b/android/app/.gitignore @@ -1,4 +1,4 @@ /.externalNativeBuild /app.iml /build -/liboSettings.gradle +/appSettings.gradle diff --git a/android/app/appSettings.gradle.in b/android/app/appSettings.gradle.in new file mode 100644 index 000000000..d872ef2ea --- /dev/null +++ b/android/app/appSettings.gradle.in @@ -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@' +} diff --git a/android/app/build.gradle b/android/app/build.gradle index 0ee8baabe..aae9c4bc0 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -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' diff --git a/android/app/src/main/java/org/libreoffice/androidapp/MainActivity.java b/android/app/src/main/java/org/libreoffice/androidapp/MainActivity.java index c2897e304..4c75b27c4 100644 --- a/android/app/src/main/java/org/libreoffice/androidapp/MainActivity.java +++ b/android/app/src/main/java/org/libreoffice/androidapp/MainActivity.java @@ -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: */ diff --git a/android/app/src/main/java/org/libreoffice/androidapp/ui/LibreOfficeUIActivity.java b/android/app/src/main/java/org/libreoffice/androidapp/ui/LibreOfficeUIActivity.java index cc0bb1bc9..9e3f87e22 100644 --- a/android/app/src/main/java/org/libreoffice/androidapp/ui/LibreOfficeUIActivity.java +++ b/android/app/src/main/java/org/libreoffice/androidapp/ui/LibreOfficeUIActivity.java @@ -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; diff --git a/android/lib/.gitignore b/android/lib/.gitignore new file mode 100644 index 000000000..77eda0d14 --- /dev/null +++ b/android/lib/.gitignore @@ -0,0 +1,3 @@ +/build +/lib.iml +/libSettings.gradle diff --git a/android/lib/build.gradle b/android/lib/build.gradle new file mode 100644 index 000000000..f208d302b --- /dev/null +++ b/android/lib/build.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' diff --git a/android/app/liboSettings.gradle.in b/android/lib/libSettings.gradle.in similarity index 54% rename from android/app/liboSettings.gradle.in rename to android/lib/libSettings.gradle.in index 80234582b..3736a35fe 100644 --- a/android/app/liboSettings.gradle.in +++ b/android/lib/libSettings.gradle.in @@ -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@' } diff --git a/android/lib/proguard-rules.pro b/android/lib/proguard-rules.pro new file mode 100644 index 000000000..f1b424510 --- /dev/null +++ b/android/lib/proguard-rules.pro @@ -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 diff --git a/android/lib/src/main/AndroidManifest.xml b/android/lib/src/main/AndroidManifest.xml new file mode 100644 index 000000000..4738b240d --- /dev/null +++ b/android/lib/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="org.libreoffice.androidlib" /> diff --git a/android/app/src/main/cpp/CMakeLists.txt.in b/android/lib/src/main/cpp/CMakeLists.txt.in similarity index 100% rename from android/app/src/main/cpp/CMakeLists.txt.in rename to android/lib/src/main/cpp/CMakeLists.txt.in diff --git a/android/app/src/main/cpp/androidapp.cpp b/android/lib/src/main/cpp/androidapp.cpp similarity index 92% rename from android/app/src/main/cpp/androidapp.cpp rename to android/lib/src/main/cpp/androidapp.cpp index 7f5cf0f74..54541e7c4 100644 --- a/android/app/src/main/cpp/androidapp.cpp +++ b/android/lib/src/main/cpp/androidapp.cpp @@ -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); diff --git a/android/app/src/main/cpp/androidapp.hpp b/android/lib/src/main/cpp/androidapp.hpp similarity index 100% rename from android/app/src/main/cpp/androidapp.hpp rename to android/lib/src/main/cpp/androidapp.hpp diff --git a/android/lib/src/main/java/org/libreoffice/androidlib/LOActivity.java b/android/lib/src/main/java/org/libreoffice/androidlib/LOActivity.java new file mode 100644 index 000000000..861c114c4 --- /dev/null +++ b/android/lib/src/main/java/org/libreoffice/androidlib/LOActivity.java @@ -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: */ diff --git a/android/app/src/main/java/org/libreoffice/androidapp/PrintAdapter.java b/android/lib/src/main/java/org/libreoffice/androidlib/PrintAdapter.java similarity index 96% rename from android/app/src/main/java/org/libreoffice/androidapp/PrintAdapter.java rename to android/lib/src/main/java/org/libreoffice/androidlib/PrintAdapter.java index 52f5bcc95..a10349327 100644 --- a/android/app/src/main/java/org/libreoffice/androidapp/PrintAdapter.java +++ b/android/lib/src/main/java/org/libreoffice/androidlib/PrintAdapter.java @@ -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; } diff --git a/android/app/src/main/java/org/libreoffice/androidapp/SlideShowActivity.java b/android/lib/src/main/java/org/libreoffice/androidlib/SlideShowActivity.java similarity index 98% rename from android/app/src/main/java/org/libreoffice/androidapp/SlideShowActivity.java rename to android/lib/src/main/java/org/libreoffice/androidlib/SlideShowActivity.java index 97407eb44..3a957b154 100644 --- a/android/app/src/main/java/org/libreoffice/androidapp/SlideShowActivity.java +++ b/android/lib/src/main/java/org/libreoffice/androidlib/SlideShowActivity.java @@ -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; diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/lib/src/main/res/layout/activity_main.xml similarity index 94% rename from android/app/src/main/res/layout/activity_main.xml rename to android/lib/src/main/res/layout/activity_main.xml index 244e1bb99..2810a9af9 100644 --- a/android/app/src/main/res/layout/activity_main.xml +++ b/android/lib/src/main/res/layout/activity_main.xml @@ -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" diff --git a/android/app/src/main/res/layout/activity_slide_show.xml b/android/lib/src/main/res/layout/activity_slide_show.xml similarity index 100% rename from android/app/src/main/res/layout/activity_slide_show.xml rename to android/lib/src/main/res/layout/activity_slide_show.xml diff --git a/android/app/src/main/res/layout/dialog_loading.xml b/android/lib/src/main/res/layout/dialog_loading.xml similarity index 100% rename from android/app/src/main/res/layout/dialog_loading.xml rename to android/lib/src/main/res/layout/dialog_loading.xml diff --git a/android/lib/src/main/res/values/strings.xml b/android/lib/src/main/res/values/strings.xml new file mode 100644 index 000000000..5651df183 --- /dev/null +++ b/android/lib/src/main/res/values/strings.xml @@ -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> \ No newline at end of file diff --git a/android/settings.gradle b/android/settings.gradle index e7b4def49..3cbe24935 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1 +1 @@ -include ':app' +include ':app', ':lib' diff --git a/configure.ac b/configure.ac index c9446f35a..681d5bdde 100644 --- a/configure.ac +++ b/configure.ac @@ -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 diff --git a/loleaflet/Makefile.am b/loleaflet/Makefile.am index 2106a0846..5ec53ce0f 100644 --- a/loleaflet/Makefile.am +++ b/loleaflet/Makefile.am @@ -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." _______________________________________________ Libreoffice-commits mailing list libreoffice-comm...@lists.freedesktop.org https://lists.freedesktop.org/mailman/listinfo/libreoffice-commits