diff --git a/AndroidManifest.xml b/AndroidManifest.xml new file mode 100644 index 000000000..5828d46d1 --- /dev/null +++ b/AndroidManifest.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Kiwix_icon_transparent_512x512.png b/Kiwix_icon_transparent_512x512.png new file mode 100644 index 000000000..00014e358 Binary files /dev/null and b/Kiwix_icon_transparent_512x512.png differ diff --git a/Kiwix_icon_transparent_600x600.png b/Kiwix_icon_transparent_600x600.png new file mode 100644 index 000000000..cf8f4df1b Binary files /dev/null and b/Kiwix_icon_transparent_600x600.png differ diff --git a/create_libkiwix.so.py b/build-android-with-native.py similarity index 91% rename from create_libkiwix.so.py rename to build-android-with-native.py index e32ecffe7..4981b4507 100755 --- a/create_libkiwix.so.py +++ b/build-android-with-native.py @@ -18,7 +18,7 @@ COMPILE_LIBLZMA = True COMPILE_LIBZIM = True COMPILE_LIBKIWIX = True STRIP_LIBKIWIX = True -COMPILE_APK = False +COMPILE_APK = True # store the OS's environment PATH as we'll mess with it # ORIGINAL_ENVIRON_PATH = os.environ.get('PATH') @@ -45,6 +45,7 @@ ARCHS_SHORT_NAMES = { # store host machine name UNAME = check_output(['uname', '-s']).strip() UARCH = check_output(['uname', '-m']).strip() +SYSTEMS = {'Linux': 'linux', 'Darwin': 'mac'} # compiler version to use # list of available toolchains in /toolchains @@ -56,6 +57,10 @@ NDK_PATH = os.environ.get('NDK_PATH', os.path.join(os.path.dirname(CURRENT_PATH), 'src', 'dependencies', 'android-ndk-r8e')) +SDK_PATH = os.environ.get('ANDROID_HOME', + os.path.join(os.path.dirname(CURRENT_PATH), + 'src', 'dependencies', + 'android-sdk', 'sdk')) # Target Android EABI/version to compile for. # list of available platforms in /platforms @@ -184,8 +189,8 @@ for arch in ARCHS: 'orig': ORIGINAL_ENVIRON['PATH'], 'arch_full': arch_full, 'gccver': COMPILER_VERSION}), - 'CFLAGS': ' -fPIC ' - } + 'CFLAGS': ' -fPIC ', + 'ANDROID_HOME': SDK_PATH} change_env(new_environ) change_env(OPTIMIZATION_ENV) @@ -264,9 +269,9 @@ for arch in ARCHS: os.remove(src.replace('.cpp', '.o')) # compile JNI header - os.chdir(os.path.join(curdir, 'org', 'kiwix', 'kiwixmobile')) + os.chdir(os.path.join(curdir, 'src', 'org', 'kiwix', 'kiwixmobile')) syscall('javac JNIKiwix.java') - os.chdir(curdir) + os.chdir(os.path.join(curdir, 'src')) syscall('javah -jni org.kiwix.kiwixmobile.JNIKiwix') # create libkiwix.so @@ -284,7 +289,9 @@ for arch in ARCHS: + platform_includes + [LIBKIWIX_SRC, os.path.join(LIBZIM_SRC, - 'include')]) + 'include'), + os.path.join(curdir, + 'src')]) }) link_cmd = ('g++ -fPIC -shared -B%(platform)s/sysroot ' @@ -297,10 +304,12 @@ for arch in ARCHS: '/libs/%(arch_short)s/libgnustl_static.a ' '-llog -landroid -lstdc++ -lc ' '%(platform)s/lib/gcc/%(arch_full)s/%(gccver)s/libgcc.a ' - '-o %(platform)s/lib/libkiwix.so' + '-o %(curdir)s/libs/%(arch_short)s/libkiwix.so' % {'kwsrc': LIBKIWIX_SRC, 'platform': platform, 'arch_full': arch_full, + 'arch_short': arch_short, + 'curdir': curdir, 'gccver': COMPILER_VERSION, 'NDK_PATH': NDK_PATH, 'arch_short': arch_short}) @@ -309,14 +318,17 @@ for arch in ARCHS: syscall(compile_cmd) syscall(link_cmd) - for obj in ('kiwix.o', 'reader.o', 'stringTools.o'): + for obj in ('kiwix.o', 'reader.o', 'stringTools.o', + 'src/org_kiwix_kiwixmobile_JNIKiwix.h'): os.remove(obj) if STRIP_LIBKIWIX: syscall('%(platform)s/%(arch_full)s/bin/strip ' - '%(platform)s/lib/libkiwix.so' + '%(curdir)s/libs/%(arch_short)s/libkiwix.so' % {'platform': platform, - 'arch_full': arch_full}) + 'arch_full': arch_full, + 'arch_short': arch_short, + 'curdir': curdir}) os.chdir(curdir) change_env(ORIGINAL_ENVIRON) diff --git a/build.xml b/build.xml new file mode 100644 index 000000000..96dae6625 --- /dev/null +++ b/build.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/create-signed-android-release.sh b/create-signed-android-release.sh new file mode 100755 index 000000000..6e31813ab --- /dev/null +++ b/create-signed-android-release.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +if [ -f "$1" ]; + then + CERTIFICATE=$1 +else + echo "Usage: $0 Kiwix-android.keystore" + echo "You must specify the path of the certificate keystore." + exit 1 +fi + +function die { + echo -n "[ERROR] " + echo -n $1 + echo -n " Aborting. +" + exit 1 +} + +ant release || die "ant release error." +jarsigner -verbose -sigalg MD5withRSA -digestalg SHA1 -keystore $CERTIFICATE bin/Kiwix-release-unsigned.apk kiwix || die "Error signing the package." +jarsigner -verify bin/Kiwix-release-unsigned.apk || die "The package is not properly signed." +zipalign -f -v 4 bin/Kiwix-release-unsigned.apk bin/kiwix-android.apk || die "Could not zipalign the signed package. Please check." + +echo "[SUCCESS] Your signed release package is ready:" +ls -lh bin/kiwix-android.apk diff --git a/proguard.cfg b/proguard.cfg new file mode 100644 index 000000000..12dd0392c --- /dev/null +++ b/proguard.cfg @@ -0,0 +1,36 @@ +-optimizationpasses 5 +-dontusemixedcaseclassnames +-dontskipnonpubliclibraryclasses +-dontpreverify +-verbose +-optimizations !code/simplification/arithmetic,!field/*,!class/merging/* + +-keep public class * extends android.app.Activity +-keep public class * extends android.app.Application +-keep public class * extends android.app.Service +-keep public class * extends android.content.BroadcastReceiver +-keep public class * extends android.content.ContentProvider +-keep public class * extends android.app.backup.BackupAgentHelper +-keep public class * extends android.preference.Preference +-keep public class com.android.vending.licensing.ILicensingService + +-keepclasseswithmembernames class * { + native ; +} + +-keepclasseswithmembernames class * { + public (android.content.Context, android.util.AttributeSet); +} + +-keepclasseswithmembernames class * { + public (android.content.Context, android.util.AttributeSet, int); +} + +-keepclassmembers enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} + +-keep class * implements android.os.Parcelable { + public static final android.os.Parcelable$Creator *; +} diff --git a/project.properties b/project.properties new file mode 100644 index 000000000..8937e94b9 --- /dev/null +++ b/project.properties @@ -0,0 +1,14 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-14 diff --git a/res/drawable-hdpi/action_help.png b/res/drawable-hdpi/action_help.png new file mode 100644 index 000000000..4c65ab2d8 Binary files /dev/null and b/res/drawable-hdpi/action_help.png differ diff --git a/res/drawable-hdpi/action_search.png b/res/drawable-hdpi/action_search.png new file mode 100644 index 000000000..f12e005eb Binary files /dev/null and b/res/drawable-hdpi/action_search.png differ diff --git a/res/drawable-hdpi/device_access_sd_storage.png b/res/drawable-hdpi/device_access_sd_storage.png new file mode 100644 index 000000000..56fceb5a7 Binary files /dev/null and b/res/drawable-hdpi/device_access_sd_storage.png differ diff --git a/res/drawable-hdpi/icon.png b/res/drawable-hdpi/icon.png new file mode 100644 index 000000000..8074c4c57 Binary files /dev/null and b/res/drawable-hdpi/icon.png differ diff --git a/res/drawable-hdpi/kiwix_icon.png b/res/drawable-hdpi/kiwix_icon.png new file mode 100644 index 000000000..83d2cf65f Binary files /dev/null and b/res/drawable-hdpi/kiwix_icon.png differ diff --git a/res/drawable-hdpi/navigation_back.png b/res/drawable-hdpi/navigation_back.png new file mode 100644 index 000000000..cd7671ccf Binary files /dev/null and b/res/drawable-hdpi/navigation_back.png differ diff --git a/res/drawable-hdpi/navigation_forward.png b/res/drawable-hdpi/navigation_forward.png new file mode 100644 index 000000000..f2325bfea Binary files /dev/null and b/res/drawable-hdpi/navigation_forward.png differ diff --git a/res/drawable-ldpi/icon.png b/res/drawable-ldpi/icon.png new file mode 100644 index 000000000..1095584ec Binary files /dev/null and b/res/drawable-ldpi/icon.png differ diff --git a/res/drawable-ldpi/kiwix_icon.png b/res/drawable-ldpi/kiwix_icon.png new file mode 100644 index 000000000..65afac885 Binary files /dev/null and b/res/drawable-ldpi/kiwix_icon.png differ diff --git a/res/drawable-mdpi/action_help.png b/res/drawable-mdpi/action_help.png new file mode 100644 index 000000000..50580cf97 Binary files /dev/null and b/res/drawable-mdpi/action_help.png differ diff --git a/res/drawable-mdpi/action_search.png b/res/drawable-mdpi/action_search.png new file mode 100644 index 000000000..587d9e0bf Binary files /dev/null and b/res/drawable-mdpi/action_search.png differ diff --git a/res/drawable-mdpi/device_access_sd_storage.png b/res/drawable-mdpi/device_access_sd_storage.png new file mode 100644 index 000000000..c10561a77 Binary files /dev/null and b/res/drawable-mdpi/device_access_sd_storage.png differ diff --git a/res/drawable-mdpi/icon.png b/res/drawable-mdpi/icon.png new file mode 100644 index 000000000..a07c69fa5 Binary files /dev/null and b/res/drawable-mdpi/icon.png differ diff --git a/res/drawable-mdpi/kiwix_icon.png b/res/drawable-mdpi/kiwix_icon.png new file mode 100644 index 000000000..7b9000506 Binary files /dev/null and b/res/drawable-mdpi/kiwix_icon.png differ diff --git a/res/drawable-mdpi/navigation_back.png b/res/drawable-mdpi/navigation_back.png new file mode 100644 index 000000000..e0b79763f Binary files /dev/null and b/res/drawable-mdpi/navigation_back.png differ diff --git a/res/drawable-mdpi/navigation_forward.png b/res/drawable-mdpi/navigation_forward.png new file mode 100644 index 000000000..38ea7ba20 Binary files /dev/null and b/res/drawable-mdpi/navigation_forward.png differ diff --git a/res/drawable-xhdpi/action_help.png b/res/drawable-xhdpi/action_help.png new file mode 100644 index 000000000..243704869 Binary files /dev/null and b/res/drawable-xhdpi/action_help.png differ diff --git a/res/drawable-xhdpi/action_search.png b/res/drawable-xhdpi/action_search.png new file mode 100644 index 000000000..3549f84dd Binary files /dev/null and b/res/drawable-xhdpi/action_search.png differ diff --git a/res/drawable-xhdpi/device_access_sd_storage.png b/res/drawable-xhdpi/device_access_sd_storage.png new file mode 100644 index 000000000..105d22e57 Binary files /dev/null and b/res/drawable-xhdpi/device_access_sd_storage.png differ diff --git a/res/drawable-xhdpi/kiwix_icon.png b/res/drawable-xhdpi/kiwix_icon.png new file mode 100644 index 000000000..60656d83c Binary files /dev/null and b/res/drawable-xhdpi/kiwix_icon.png differ diff --git a/res/drawable-xhdpi/navigation_back.png b/res/drawable-xhdpi/navigation_back.png new file mode 100644 index 000000000..3bdda98c3 Binary files /dev/null and b/res/drawable-xhdpi/navigation_back.png differ diff --git a/res/drawable-xhdpi/navigation_forward.png b/res/drawable-xhdpi/navigation_forward.png new file mode 100644 index 000000000..37c4101d9 Binary files /dev/null and b/res/drawable-xhdpi/navigation_forward.png differ diff --git a/res/layout/main.xml b/res/layout/main.xml new file mode 100644 index 000000000..9b293b8f4 --- /dev/null +++ b/res/layout/main.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/zimfilelist.xml b/res/layout/zimfilelist.xml new file mode 100644 index 000000000..4648a2bc4 --- /dev/null +++ b/res/layout/zimfilelist.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/res/layout/zimfilelistentry.xml b/res/layout/zimfilelistentry.xml new file mode 100644 index 000000000..2084b2b7c --- /dev/null +++ b/res/layout/zimfilelistentry.xml @@ -0,0 +1,17 @@ + + + + \ No newline at end of file diff --git a/res/menu/main.xml b/res/menu/main.xml new file mode 100644 index 000000000..16ea4860a --- /dev/null +++ b/res/menu/main.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + diff --git a/res/raw-de/welcome.html b/res/raw-de/welcome.html new file mode 100644 index 000000000..7d2300a50 --- /dev/null +++ b/res/raw-de/welcome.html @@ -0,0 +1,7 @@ + +

Willkommen zu Kiwix

+ +Visit Kiwix to find out how +to download zim files, such as the Wikipedia. + + \ No newline at end of file diff --git a/res/raw/kiwix_icon.png b/res/raw/kiwix_icon.png new file mode 100644 index 000000000..60656d83c Binary files /dev/null and b/res/raw/kiwix_icon.png differ diff --git a/res/raw/welcome.html b/res/raw/welcome.html new file mode 100644 index 000000000..388308d44 --- /dev/null +++ b/res/raw/welcome.html @@ -0,0 +1,7 @@ + +

Welcome to Kiwix

+ +Visit Kiwix to find out how +to download zim files, such as the Wikipedia. + + \ No newline at end of file diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml new file mode 100644 index 000000000..f49b80ad8 --- /dev/null +++ b/res/values-de/strings.xml @@ -0,0 +1,13 @@ + + + Kiwix + + Paris + Wikipedia + Seine + + Datei öffnen + Vorwärts + Zurück + Datei auswählen + diff --git a/res/values/strings.xml b/res/values/strings.xml new file mode 100644 index 000000000..5754dc543 --- /dev/null +++ b/res/values/strings.xml @@ -0,0 +1,19 @@ + + + Kiwix + + Paris + Wikipedia + Seine + + Open File + Help + Forward + Back + Find in text + Type to search article + Choose (*.zim) File + Error: The selected zim file could not be found. + Error: The selected file is not a valid zim file. + Error: Loading article (Url: %1$s) failed. + diff --git a/src/org/kiwix/kiwixmobile/JNIKiwix.java b/src/org/kiwix/kiwixmobile/JNIKiwix.java new file mode 100644 index 000000000..adfb93a97 --- /dev/null +++ b/src/org/kiwix/kiwixmobile/JNIKiwix.java @@ -0,0 +1,25 @@ +package org.kiwix.kiwixmobile; +public class JNIKiwix { + public native String getMainPage(); + public native boolean loadZIM(String path); + public native byte[] getContent(String url, JNIKiwixString mimeType, JNIKiwixInt size); + public native boolean searchSuggestions(String prefix, int count); + public native boolean getNextSuggestion(JNIKiwixString title); + public native boolean getPageUrlFromTitle(String title, JNIKiwixString url); + + static { + System.loadLibrary("kiwix"); + } +} + +class JNIKiwixString { + String value; +} + +class JNIKiwixInt { + int value; +} + +class JNIKiwixBool { + boolean value; +} diff --git a/src/org/kiwix/kiwixmobile/KiwixMobileActivity.java b/src/org/kiwix/kiwixmobile/KiwixMobileActivity.java new file mode 100644 index 000000000..aef765221 --- /dev/null +++ b/src/org/kiwix/kiwixmobile/KiwixMobileActivity.java @@ -0,0 +1,385 @@ +package org.kiwix.kiwixmobile; + + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; + +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.ComponentName; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.Window; +import android.webkit.WebBackForwardList; +import android.webkit.WebChromeClient; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.ArrayAdapter; +import android.widget.AutoCompleteTextView; +import android.widget.TextView; +import android.widget.TextView.OnEditorActionListener; +import android.widget.Toast; + + +public class KiwixMobileActivity extends Activity { + /** Called when the activity is first created. */ + + private WebView webView; + private ArrayAdapter adapter; + protected boolean requestClearHistoryAfterLoad; + private static final int ZIMFILESELECT_REQUEST_CODE = 1234; + private static final String PREFS_KIWIX_MOBILE = "kiwix-mobile"; + + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestClearHistoryAfterLoad=false; + + + this.requestWindowFeature(Window.FEATURE_PROGRESS); + this.setProgressBarVisibility(true); + + setContentView(R.layout.main); + webView = (WebView) findViewById(R.id.webview); + + // Get a reference to the AutoCompleteTextView in the layout + AutoCompleteTextView articleSearchtextView = (AutoCompleteTextView) findViewById(R.id.articleSearchTextView); + // Get the string array + //TODO Implement db backend + ArrayList countries = new ArrayList(Arrays.asList(getResources().getStringArray(R.array.articleSearchSuggestionsTrial))); + // Create the adapter and set it to the AutoCompleteTextView + adapter = + new ArrayAdapter(this, android.R.layout.simple_list_item_1, countries); + articleSearchtextView.setAdapter(adapter); + articleSearchtextView.setOnEditorActionListener(new OnEditorActionListener() { + + @Override + public boolean onEditorAction(TextView v, int actionId, + KeyEvent event) { + //Do Stuff + Log.d("zimgap", v+" onEditorAction. "+v.getText()); + // To close softkeyboard + String articleUrl = ZimContentProvider.getPageUrlFromTitle(v.getText().toString()); + Log.d("zimgap", v+" onEditorAction. TextView: "+v.getText()+ " articleUrl: "+articleUrl); + + if (articleUrl!=null) { + webView.requestFocus(); + webView.loadUrl(Uri.parse(ZimContentProvider.CONTENT_URI + +articleUrl).toString()); + return true; + } else { + //FIXME Toast.makeText(this, "Article not found.", Toast.LENGTH_SHORT).show(); //FIXME resource string + + return true; + } + }}); + articleSearchtextView.addTextChangedListener(new TextWatcher() + { + public void afterTextChanged(Editable s) + { + // Abstract Method of TextWatcher Interface. + } + public void beforeTextChanged(CharSequence s, + int start, int count, int after) + { + // Abstract Method of TextWatcher Interface. + } + public void onTextChanged(CharSequence s, + int start, int before, int count) + { + AutoCompleteTextView articleSearchtextView = (AutoCompleteTextView) findViewById(R.id.articleSearchTextView); + Log.d("zimgap", "Adapter:"+adapter.getCount()); + adapter.clear(); + ZimContentProvider.searchSuggestions(s.toString(), 20); + String suggestion; + while ((suggestion = ZimContentProvider.getNextSuggestion())!=null) { + adapter.add(suggestion); + } + } + }); + + + // js includes will not happen unless we enable JS + webView.getSettings().setJavaScriptEnabled(true); + //Does not seem to have impact. (Idea was that + // web page is rendered before loading all pictures) + //webView.getSettings().setRenderPriority(RenderPriority.HIGH); + final Activity activity = this; + + webView.setWebChromeClient(new WebChromeClient(){ + + public void onProgressChanged(WebView view, int progress) { + activity.setProgress(progress * 100); + if (progress==100) { + + Log.d("zimgap", "Loading article finished."); + if (requestClearHistoryAfterLoad) { + Log.d("zimgap", "Loading article finished and requestClearHistoryAfterLoad -> clearHistory"); + webView.clearHistory(); + requestClearHistoryAfterLoad=false; + } + } + } + }); + +// Should basically resemble the behavior when setWebClient not done +// (i.p. internal urls load in webview, external urls in browser) +// as currently no custom setWebViewClient required it is commented + webView.setWebViewClient(new WebViewClient() { + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + if (url.startsWith(ZimContentProvider.CONTENT_URI.toString())) { + // This is my web site, so do not override; let my WebView load the page + return false; + } + // Otherwise, the link is not for a page on my site, so launch another Activity that handles URLs + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + startActivity(intent); + return true; + } + + public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { + String errorString = String.format(getResources().getString(R.string.error_articlenotfound), failingUrl); + //TODO apparently screws up back/forward + webView.loadDataWithBaseURL("file://error",""+errorString+"", "text/html", "utf-8", failingUrl); + } + }); + + //Pinch to zoom + webView.getSettings().setBuiltInZoomControls(true); + //webView.getSettings().setLoadsImagesAutomatically(false); + //Does not make much sense to cache data from zim files.(Not clear whether + // this actually has any effect) + webView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE); + //Workaround to avoid that default zoom is very small. TODO check cause + // and find better solution (e.g. may only be issue on tablets, etc...) + webView.getSettings().setDefaultZoom(WebSettings.ZoomDensity.CLOSE); + if (getIntent().getData()!=null) { + String filePath = getIntent().getData().getEncodedPath(); + Log.d("zimgap", " Kiwix started from a filemanager. Intent filePath: "+filePath+" -> open this zimfile and load main page"); + openZimFile(new File(filePath), false); + + } else if (savedInstanceState!=null) { + Log.d("zimgap", " Kiwix started with a savedInstanceState (That is was closed by OS) -> restore webview state and zimfile (if set)"); + if (savedInstanceState.getString("currentzimfile")!=null) { + openZimFile(new File(savedInstanceState.getString("currentzimfile")), false); + + } + // Restore the state of the WebView + + webView.restoreState(savedInstanceState); + } else { + SharedPreferences settings = getSharedPreferences(PREFS_KIWIX_MOBILE, 0); + String zimfile = settings.getString("currentzimfile", null); + if (zimfile != null) { + Log.d("zimgap", " Kiwix normal start, zimfile loaded last time -> Open last used zimfile "+zimfile); + openZimFile(new File(zimfile), false); + // Alternative would be to restore webView state. But more effort to implement, and actually + // fits better normal android behavior if after closing app ("back" button) state is not maintained. + } else { + Log.d("zimgap", " Kiwix normal start, no zimfile loaded last time -> display welcome page"); + showHelp(); + } + } + } + + + + + @Override + public void onPause() { + super.onPause(); + SharedPreferences settings = getSharedPreferences(PREFS_KIWIX_MOBILE, 0); + SharedPreferences.Editor editor = settings.edit(); + editor.putString("currentzimfile", ZimContentProvider.getZimFile()); + // Commit the edits! + editor.commit(); + + Log.d("zimgap", "onPause Save currentzimfile to preferences:"+ZimContentProvider.getZimFile()); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + // Save the state of the WebView + + webView.saveState(outState); + outState.putString("currentzimfile", ZimContentProvider.getZimFile()); + Log.v("zimgap", "onSaveInstanceState Save currentzimfile to bundle:"+ZimContentProvider.getZimFile()+" and webView state"); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.main, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + Toast.makeText(this, "Tapped home", Toast.LENGTH_SHORT).show(); + break; + case R.id.menu_search: + webView.showFindDialog("", true); + break; + case R.id.menu_forward: + if(webView.canGoForward() == true){ + webView.goForward(); + } + break; + case R.id.menu_back: + if(webView.canGoBack() == true){ + webView.goBack(); + } + break; + case R.id.menu_help: + showHelp(); + break; + case R.id.menu_openfile: + final Intent target = new Intent(Intent.ACTION_GET_CONTENT); + // The MIME data type filter + target.setType("*/*"); + // Only return URIs that can be opened with ContentResolver + target.addCategory(Intent.CATEGORY_OPENABLE); + //Force use of our file selection component. + // (Note may make sense to just define a custom intent instead) + target.setComponent(new ComponentName(getPackageName(), getPackageName()+".ZimFileSelectActivity")); + try { + startActivityForResult(target, ZIMFILESELECT_REQUEST_CODE); + } catch (ActivityNotFoundException e) { + + }break; + } + return super.onOptionsItemSelected(item); + } + + private String readTextFromResource(int resourceID) + { + InputStream raw = getResources().openRawResource(resourceID); + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + int i; + try + { + i = raw.read(); + while (i != -1) + { + stream.write(i); + i = raw.read(); + } + raw.close(); + } + catch (IOException e) + { + e.printStackTrace(); + } + return stream.toString(); + } + + private void showHelp() { + //Load from resource. Use with base url as else no images can be embedded. + webView.loadDataWithBaseURL("file:///android_res/raw/", readTextFromResource(R.raw.welcome), "text/html", "utf-8", null); + } + + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case ZIMFILESELECT_REQUEST_CODE: + if (resultCode == RESULT_OK) { + // The URI of the selected file + final Uri uri = data.getData(); + File file = null; + if (uri != null) { + String path = uri.getPath(); + if (path != null) + file = new File(path); + } + if (file==null) + return; + // Create a File from this Uri + openZimFile(file, true); + } + } + } + + + + + private boolean openZimFile(File file, boolean clearHistory) { + if (file.exists()) { + if (ZimContentProvider.setZimFile(file.getAbsolutePath())!=null) { + //Apparently with webView.clearHistory() only + // history before currently (fully) loaded page is cleared + // -> request clear, actual clear done after load. + // Probably not working in all corners (e.g. zim file openend + // while load in progress, mainpage of new zim file invalid, ... + // but should be good enough. + // Actually probably redundant if no zim file openend before in session, + // but to be on save side don't clear history in such cases. + if (clearHistory) + requestClearHistoryAfterLoad=true; + loadMainPage(); + return true; + } else { + Toast.makeText(this, getResources().getString(R.string.error_fileinvalid), Toast.LENGTH_LONG).show(); + } + + } else { + Toast.makeText(this, getResources().getString(R.string.error_filenotfound), Toast.LENGTH_LONG).show(); + } + return false; + } + + private void loadMainPage() { + String article = ZimContentProvider.getMainPage(); + webView.loadUrl(Uri.parse(ZimContentProvider.CONTENT_URI + + article).toString()); + } + + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if(event.getAction() == KeyEvent.ACTION_DOWN){ + switch(keyCode) + { + case KeyEvent.KEYCODE_BACK: + if(webView.canGoBack() == true){ + /*WebBackForwardList history = webView.copyBackForwardList(); + + if (history.getCurrentIndex() )*/ + + webView.goBack(); + }else{ + finish(); + } + return true; + } + + } + return super.onKeyDown(keyCode, event); + } +} \ No newline at end of file diff --git a/src/org/kiwix/kiwixmobile/ZimContentProvider.java b/src/org/kiwix/kiwixmobile/ZimContentProvider.java new file mode 100644 index 000000000..4cc9c4555 --- /dev/null +++ b/src/org/kiwix/kiwixmobile/ZimContentProvider.java @@ -0,0 +1,229 @@ +package org.kiwix.kiwixmobile; + +/*** + Copyright (c) 2012 CommonsWare, LLC + Licensed under the Apache License, Version 2.0 (the "License"); you may not + use this file except in compliance with the License. You may obtain a copy + of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless required + by applicable law or agreed to in writing, software distributed under the + License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS + OF ANY KIND, either express or implied. See the License for the specific + language governing permissions and limitations under the License. + + From _The Busy Coder's Guide to Android Development_ + http://commonsware.com/Android + */ + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.res.AssetManager; +import android.database.Cursor; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.os.ParcelFileDescriptor.AutoCloseOutputStream; +import android.util.Log; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.HashMap; + +import org.json.JSONException; +import org.json.JSONObject; + +public class ZimContentProvider extends ContentProvider { + public static final Uri CONTENT_URI = Uri.parse("content://org.kiwix.zim/"); + private static final HashMap MIME_TYPES = new HashMap(); + + static { + MIME_TYPES.put(".html", "text/html"); + } + private static String zimFileName; + private static JNIKiwix jniKiwix; + + public synchronized static String setZimFile(String fileName) { + if (!jniKiwix.loadZIM(fileName)) { + Log.e("zimgap", "Unable to open the file " + fileName); + zimFileName = null; + } else { + zimFileName = fileName; + } + return zimFileName; + } + + public static String getZimFile() { + return zimFileName; + } + + public static String getMainPage() { + if (jniKiwix==null) + return null; + else { + return jniKiwix.getMainPage(); + } + } + + public static boolean searchSuggestions(String prefix, int count) { + if (jniKiwix==null) + return false; + else { + return jniKiwix.searchSuggestions(prefix, count); + } + } + + public static String getNextSuggestion() { + if (jniKiwix==null) + return null; + else { + JNIKiwixString title=new JNIKiwixString(); + if (jniKiwix.getNextSuggestion(title)) { + return title.value; + } + else { + return null; + } + } + } + + public static String getPageUrlFromTitle(String title) { + if (jniKiwix==null) + return null; + else { + JNIKiwixString url=new JNIKiwixString(); + if (jniKiwix.getPageUrlFromTitle(title, url)) { + return url.value; + } else { + return null; + } + } + } + + @Override + public boolean onCreate() { + jniKiwix = new JNIKiwix(); + + return (true); + } + + @Override + public String getType(Uri uri) { + String path = uri.toString(); + + for (String extension : MIME_TYPES.keySet()) { + if (path.endsWith(extension)) { + return (MIME_TYPES.get(extension)); + } + } + + return (null); + } + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) + throws FileNotFoundException { + ParcelFileDescriptor[] pipe = null; + + try { + pipe = ParcelFileDescriptor.createPipe(); + new TransferThread(jniKiwix, uri, new AutoCloseOutputStream( + pipe[1])).start(); + } catch (IOException e) { + Log.e(getClass().getSimpleName(), "Exception opening pipe", e); + throw new FileNotFoundException("Could not open pipe for: " + + uri.toString()); + } + + return (pipe[0]); + } + + @Override + public Cursor query(Uri url, String[] projection, String selection, + String[] selectionArgs, String sort) { + throw new RuntimeException("Operation not supported"); + } + + @Override + public Uri insert(Uri uri, ContentValues initialValues) { + throw new RuntimeException("Operation not supported"); + } + + @Override + public int update(Uri uri, ContentValues values, String where, + String[] whereArgs) { + throw new RuntimeException("Operation not supported"); + } + + @Override + public int delete(Uri uri, String where, String[] whereArgs) { + throw new RuntimeException("Operation not supported"); + } + + static class TransferThread extends Thread { + + Uri articleUri; + String articleZimUrl; + OutputStream out; + JNIKiwix jniKiwix; + + TransferThread(JNIKiwix jniKiwix, Uri articleUri, OutputStream out) throws IOException { + this.articleUri = articleUri; + this.jniKiwix = jniKiwix; + Log.d("zimgap", + "Retrieving :" + + articleUri.toString()); + String t = articleUri.toString(); + int pos = articleUri.toString().indexOf(CONTENT_URI.toString()); + if (pos != -1) + t = articleUri.toString().substring( + CONTENT_URI.toString().length()); + this.out = out; + this.articleZimUrl = t; + } + + @Override + public void run() { + byte[] buf = new byte[1024]; + int len; + + try { + JNIKiwixString mime = new JNIKiwixString(); + JNIKiwixInt size = new JNIKiwixInt(); + byte[] data = jniKiwix.getContent(articleZimUrl, mime, size); + // Log.d("zimgap","articleDataByteArray:"+articleDataByteArray.toString()); + // ByteArrayInputStream articleDataInputStream = new + // ByteArrayInputStream(articleDataByteArray.toByteArray()); + // Log.d("zimgap","article data loaded from zime file"); + + //ByteArrayInputStream articleDataInputStream = new ByteArrayInputStream( + // articleDataByteArray.toByteArray()); + ByteArrayInputStream articleDataInputStream = new ByteArrayInputStream(data); + while ((len = articleDataInputStream.read(buf)) > 0) { + out.write(buf, 0, len); + } + + articleDataInputStream.close(); + out.flush(); + + Log.d("zimgap", "reading " + articleZimUrl + + " finished."); + } catch (IOException e) { + Log.e(getClass().getSimpleName(), "Exception reading article " + + articleZimUrl + " from zim file", e); + } catch (NullPointerException e) { + Log.e(getClass().getSimpleName(), "Exception reading article " + + articleZimUrl + " from zim file", e); + + } finally { + try { + out.close(); + } catch (IOException e) { + } + + } + } + } +} \ No newline at end of file diff --git a/src/org/kiwix/kiwixmobile/ZimFileSelectActivity.java b/src/org/kiwix/kiwixmobile/ZimFileSelectActivity.java new file mode 100644 index 000000000..b0e1aae86 --- /dev/null +++ b/src/org/kiwix/kiwixmobile/ZimFileSelectActivity.java @@ -0,0 +1,141 @@ +package org.kiwix.kiwixmobile; + +import java.io.File; + +import android.app.Activity; +import android.content.Intent; +import android.content.Loader; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.provider.MediaStore; +import android.util.Log; +import android.view.View; +import android.widget.Adapter; +import android.widget.AdapterView; +import android.widget.ListView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.SimpleCursorAdapter; +//TODO API level 11 (honeycomb). use compatiblity packages instead +import android.content.CursorLoader; +import android.app.LoaderManager; + +public class ZimFileSelectActivity extends Activity implements +LoaderManager.LoaderCallbacks { + + private static final int LOADER_ID = 0x02; + private SimpleCursorAdapter mCursorAdapter; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.zimfilelist); + selectZimFile(); + + + } + + private void finishResult(String path) { + if (path != null) { + File file = new File(path); + Uri uri = Uri.fromFile(file); + setResult(RESULT_OK, new Intent().setData(uri)); + finish(); + } else { + setResult(RESULT_CANCELED); + finish(); + } + } + + protected void selectZimFile() { + // Defines a list of columns to retrieve from the Cursor and load into an output row + String[] mZimListColumns = + { + MediaStore.Images.Media.DATA + }; + + // Defines a list of View IDs that will receive the Cursor columns for each row + int[] mZimListItems = { R.id.zim_file_list_entry_path}; + + mCursorAdapter = new SimpleCursorAdapter( + getApplicationContext(), // The application's Context object + R.layout.zimfilelistentry, // A layout in XML for one row in the ListView + null, // The cursor, swapped later by cursorloader + mZimListColumns, // A string array of column names in the cursor + mZimListItems, // An integer array of view IDs in the row layout + Adapter.NO_SELECTION); + + // Sets the adapter for the ListView + setContentView(R.layout.zimfilelist); + + + ListView zimFileList = (ListView) findViewById(R.id.zimfilelist); + getLoaderManager().initLoader(LOADER_ID, null, this); + + zimFileList.setAdapter(mCursorAdapter); + zimFileList.setOnItemClickListener(new OnItemClickListener() { + public void onItemClick(AdapterView arg0, View arg1, int arg2, long arg3) { + // TODO Auto-generated method stub + onListItemClick((ListView) arg0, arg0, arg2, arg3); + } + }); + //TODO close cursor when done + //allNonMediaFiles.close(); + } + + + private void onListItemClick(AdapterView adapter, View view, int position, long arg) { + // TODO Auto-generated method stub + Log.d("zimgap", " zimFileList.onItemClick"); + + ListView zimFileList = (ListView) findViewById(R.id.zimfilelist); + Cursor mycursor = (Cursor) zimFileList.getItemAtPosition(position); + //TODO not very clean + finishResult(mycursor.getString(1)); + } + + @Override + public Loader onCreateLoader(int i, Bundle bundle) { + //TODO leads to API min 11 + Uri uri = MediaStore.Files.getContentUri("external"); + + String[] projection = { + MediaStore.Images.Media._ID, + MediaStore.Images.Media.DATA, //Path + }; + + // exclude media files, they would be here also (perhaps + // somewhat better performance), and filter for zim files + // (normal and first split) + String selection = MediaStore.Files.FileColumns.MEDIA_TYPE + "=" + + MediaStore.Files.FileColumns.MEDIA_TYPE_NONE + " AND " + + " ( LOWER(" + + MediaStore.Images.Media.DATA + ") LIKE '%.zim'" + + " OR LOWER(" + + MediaStore.Images.Media.DATA + ") LIKE '%.zimaa'" + +" ) "; + + + String[] selectionArgs = null; // there is no ? in selection so null here + + + String sortOrder = MediaStore.Images.Media.DATA; // unordered + Log.d("zimgap", " Performing query for zim files..."); + + + return new CursorLoader(this, uri, projection, selection, selectionArgs, sortOrder); + + } + + @Override + public void onLoadFinished(Loader cursorLoader, Cursor cursor) { + Log.d("zimgap", " DONE query zim files"); + mCursorAdapter.swapCursor(cursor); + } + + @Override + public void onLoaderReset(Loader cursorLoader) { + mCursorAdapter.swapCursor(null); + } + +}