diff --git a/res/drawable-hdpi/action_refresh.png b/res/drawable-hdpi/action_refresh.png
new file mode 100644
index 000000000..00d70792a
Binary files /dev/null and b/res/drawable-hdpi/action_refresh.png differ
diff --git a/res/drawable-mdpi/action_refresh.png b/res/drawable-mdpi/action_refresh.png
new file mode 100644
index 000000000..c2920a7a0
Binary files /dev/null and b/res/drawable-mdpi/action_refresh.png differ
diff --git a/res/drawable-xhdpi/action_refresh.png b/res/drawable-xhdpi/action_refresh.png
new file mode 100644
index 000000000..6aa33d59a
Binary files /dev/null and b/res/drawable-xhdpi/action_refresh.png differ
diff --git a/res/drawable-xxhdpi/action_refresh.png b/res/drawable-xxhdpi/action_refresh.png
new file mode 100644
index 000000000..145c9df47
Binary files /dev/null and b/res/drawable-xxhdpi/action_refresh.png differ
diff --git a/res/layout/zimfilelist.xml b/res/layout/zimfilelist.xml
index c396b87d1..fe9a4fd99 100644
--- a/res/layout/zimfilelist.xml
+++ b/res/layout/zimfilelist.xml
@@ -1,24 +1,33 @@
-
-
-
+
+
-
-
+
+
+
+
+
+
diff --git a/res/layout/zimfilelistentry.xml b/res/layout/zimfilelistentry.xml
deleted file mode 100644
index 2084b2b7c..000000000
--- a/res/layout/zimfilelistentry.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/res/menu/fileselector.xml b/res/menu/fileselector.xml
new file mode 100644
index 000000000..ec0aa6708
--- /dev/null
+++ b/res/menu/fileselector.xml
@@ -0,0 +1,11 @@
+
+
+
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 5fd98fdd3..c641795e0 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -14,6 +14,7 @@
Exit full screen
Exit
Share with friends
+ Rescan
Save Image
An error occured when trying to save an image!
Saved image as %s in your Pictures folder.
diff --git a/src/org/kiwix/kiwixmobile/ZimFileSelectActivity.java b/src/org/kiwix/kiwixmobile/ZimFileSelectActivity.java
index 0bf849be1..2e67f6243 100644
--- a/src/org/kiwix/kiwixmobile/ZimFileSelectActivity.java
+++ b/src/org/kiwix/kiwixmobile/ZimFileSelectActivity.java
@@ -1,153 +1,432 @@
package org.kiwix.kiwixmobile;
-import java.io.File;
-
-import android.app.Activity;
+import android.content.Context;
import android.content.Intent;
-import android.content.Loader;
import android.database.Cursor;
import android.net.Uri;
+import android.os.AsyncTask;
import android.os.Bundle;
+import android.os.Environment;
+import android.os.Parcel;
+import android.os.Parcelable;
import android.provider.MediaStore;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.support.v4.widget.SimpleCursorAdapter;
import android.util.Log;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
import android.view.View;
+import android.view.ViewGroup;
import android.view.Window;
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;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
-public class ZimFileSelectActivity extends Activity implements
-LoaderManager.LoaderCallbacks {
+import java.io.File;
+import java.io.FilenameFilter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Vector;
- private static final int LOADER_ID = 0x02;
- private SimpleCursorAdapter mCursorAdapter;
- private ListView zimFileList;
- private View tmpZimFileList;
+public class ZimFileSelectActivity extends FragmentActivity
+ implements LoaderManager.LoaderCallbacks, OnItemClickListener {
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
- setProgressBarIndeterminateVisibility(true);
- setContentView(R.layout.zimfilelist);
- selectZimFile();
+ private static final int LOADER_ID = 0x02;
+ // array of valid audio file extensions
+ private static final String[] zimFiles = {"zim", "zimaa"};
- }
+ // Adapter of the Data populated by the MediaStore
+ private SimpleCursorAdapter mCursorAdapter;
- 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();
- }
- }
+ // Adapter of the Data populated by recanning the Filesystem by ourselves
+ private RescanDataAdapter mRescanAdapter;
- 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
- };
+ private ArrayList mFiles;
- // Defines a list of View IDs that will receive the Cursor columns for each row
- int[] mZimListItems = { R.id.zim_file_list_entry_path};
+ private ListView mZimFileList;
- 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);
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+ setProgressBarIndeterminateVisibility(true);
+ setContentView(R.layout.zimfilelist);
- // Sets the adapter for the ListView
- setContentView(R.layout.zimfilelist);
+ mZimFileList = (ListView) findViewById(R.id.zimfilelist);
+ mFiles = new ArrayList();
- // For a reason I ingore, it seems that time to time
- // tmpZimFileList is not castable in ListView. Kelson
- tmpZimFileList = findViewById(R.id.zimfilelist);
- if (tmpZimFileList instanceof ListView) {
- zimFileList = (ListView) tmpZimFileList;
- 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);
- }
- });
- }
+ selectZimFile();
+ }
- //TODO close cursor when done
- //allNonMediaFiles.close();
- }
+ 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.Files.FileColumns.TITLE, MediaStore.Files.FileColumns.DATA};
- private void onListItemClick(AdapterView> adapter, View view, int position, long arg) {
- // TODO Auto-generated method stub
- Log.d("kiwix", " zimFileList.onItemClick");
+ // Defines a list of View IDs that will receive the Cursor columns for each row
+ int[] mZimListItems = {android.R.id.text1, android.R.id.text2};
- ListView zimFileList = (ListView) findViewById(R.id.zimfilelist);
- Cursor mycursor = (Cursor) zimFileList.getItemAtPosition(position);
- //TODO not very clean
- finishResult(mycursor.getString(1));
- }
+ mCursorAdapter = new SimpleCursorAdapter(
+ // The Context object
+ ZimFileSelectActivity.this,
+ // A layout in XML for one row in the ListView
+ android.R.layout.simple_list_item_2,
+ // The cursor, swapped later by cursorloader
+ null,
+ // A string array of column names in the cursor
+ mZimListColumns,
+ // An integer array of view IDs in the row layout
+ mZimListItems,
+ // Flags for the Adapter
+ Adapter.NO_SELECTION);
- @Override
- public Loader onCreateLoader(int i, Bundle bundle) {
- //TODO leads to API min 11
- Uri uri = MediaStore.Files.getContentUri("external");
+ mZimFileList.setAdapter(mCursorAdapter);
+ mZimFileList.setOnItemClickListener(this);
- String[] projection = {
- MediaStore.Images.Media._ID,
- MediaStore.Images.Media.DATA, //Path
- };
+ getSupportLoaderManager().initLoader(LOADER_ID, null, this);
+ }
- // 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'"
- +" ) ";
+ @Override
+ public Loader onCreateLoader(int i, Bundle bundle) {
+ Uri uri = MediaStore.Files.getContentUri("external");
+ String[] projection = {
+ MediaStore.Files.FileColumns._ID,
+ // File Name
+ MediaStore.Files.FileColumns.TITLE,
+ // File Path
+ MediaStore.Files.FileColumns.DATA
+ };
- String[] selectionArgs = null; // there is no ? in selection so null here
+ // Exclude media files, they would be here also (perhaps
+ // somewhat better performance), and filter for zim files
+ // (normal and first split)
+ String query = MediaStore.Files.FileColumns.MEDIA_TYPE + "="
+ + MediaStore.Files.FileColumns.MEDIA_TYPE_NONE + " AND"
+ + " ( LOWER(" +
+ MediaStore.Images.Media.DATA + ") LIKE '%." + zimFiles[0] + "'"
+ + " OR LOWER(" +
+ MediaStore.Images.Media.DATA + ") LIKE '%." + zimFiles[1] + "'"
+ + " ) ";
+ String[] selectionArgs = null; // There is no ? in query so null here
- String sortOrder = MediaStore.Images.Media.DATA; // unordered
- Log.d("kiwix", " Performing query for zim files...");
+ String sortOrder = MediaStore.Images.Media.TITLE; // Sorted alphabetical
+ Log.d("kiwix", " Performing query for zim files...");
+ return new CursorLoader(this, uri, projection, query, selectionArgs, sortOrder);
+ }
- return new CursorLoader(this, uri, projection, selection, selectionArgs, sortOrder);
+ @Override
+ public void onLoadFinished(Loader cursorLoader, Cursor cursor) {
+ Log.d("kiwix", "DONE querying Mediastore for .zim files");
+ mCursorAdapter.swapCursor(cursor);
+ // Done here to avoid that shown while loading.
+ mZimFileList.setEmptyView(findViewById(R.id.zimfilelist_nozimfilesfound_view));
+ mCursorAdapter.notifyDataSetChanged();
+ }
- }
+ @Override
+ public void onLoaderReset(Loader cursorLoader) {
+ mCursorAdapter.swapCursor(null);
+ }
- @Override
- public void onLoadFinished(Loader cursorLoader, Cursor cursor) {
- Log.d("kiwix", " DONE query zim files");
- mCursorAdapter.swapCursor(cursor);
- //Done here to avoid that shown while loading.
- zimFileList.setEmptyView( findViewById( R.id.zimfilelist_nozimfilesfound_view ) );
- setProgressBarIndeterminateVisibility(false);
- }
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
- @Override
- public void onLoaderReset(Loader cursorLoader) {
- mCursorAdapter.swapCursor(null);
- }
+ // Check, if the user has rescanned the file system, if he has, then we want to save this list,
+ // so this can be shown again, if the actvitity is recreated (on a device rotation for example)
+ if (!mFiles.isEmpty()) {
+ outState.putParcelableArrayList("rescanData", mFiles);
+ }
+ super.onSaveInstanceState(outState);
+ }
+ @Override
+ protected void onRestoreInstanceState(Bundle savedInstanceState) {
+
+ // Get the rescanned data, if available. Create an Adapter for the ListView and display the list
+ if (savedInstanceState.getParcelableArrayList("rescanData") != null) {
+ ArrayList data = savedInstanceState.getParcelableArrayList("rescanData");
+ mRescanAdapter = new RescanDataAdapter(ZimFileSelectActivity.this, 0, data);
+
+ mZimFileList.setAdapter(mRescanAdapter);
+ }
+ super.onRestoreInstanceState(savedInstanceState);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ final MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.fileselector, menu);
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+
+ switch (item.getItemId()) {
+ case R.id.menu_rescan:
+ // Execute our AsyncTask, that scans the file system for the actual data
+ new RescanFileSystem().execute();
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+
+ Log.d("kiwix", " mZimFileList.onItemClick");
+
+ String file;
+
+ // Check which one of the Adapters is currently filling the ListView.
+ // If the data is populated by the LoaderManager cast the current selected item to Cursor,
+ // if the data is populated by the ArrayAdapter, then cast it to the RescanDataModel class.
+ if (mZimFileList.getItemAtPosition(position) instanceof RescanDataModel) {
+
+ RescanDataModel data = (RescanDataModel) mZimFileList.getItemAtPosition(position);
+ file = data.getPath();
+
+ } else {
+ Cursor cursor = (Cursor) mZimFileList.getItemAtPosition(position);
+ file = cursor.getString(1);
+ }
+
+ finishResult(file);
+ }
+
+ // This AsyncTask will scan the file system for files with the Extension ".zim" or ".zimaa"
+ private class RescanFileSystem extends AsyncTask {
+
+ ProgressBar mProgressBar;
+
+ @Override
+ protected void onPreExecute() {
+ mProgressBar = (ProgressBar) findViewById(R.id.progressBar);
+ mProgressBar.setVisibility(View.VISIBLE);
+
+ super.onPreExecute();
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+
+ mFiles = FindFiles();
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ mRescanAdapter = new RescanDataAdapter(ZimFileSelectActivity.this, 0, mFiles);
+
+ mZimFileList.setAdapter(mRescanAdapter);
+
+ mProgressBar.setVisibility(View.GONE);
+
+ super.onPostExecute(result);
+ }
+
+ private ArrayList FindFiles() {
+ String directory = new File(
+ Environment.getExternalStorageDirectory().getAbsolutePath()).toString();
+ final List fileList = new ArrayList();
+ FilenameFilter[] filter = new FilenameFilter[zimFiles.length];
+
+ int i = 0;
+ for (final String extension : zimFiles) {
+ filter[i] = new FilenameFilter() {
+ public boolean accept(File dir, String name) {
+ return name.endsWith("." + extension);
+ }
+ };
+ i++;
+ }
+
+ File[] foundFiles = listFilesAsArray(new File(directory), filter, -1);
+ for (File f : foundFiles) {
+ fileList.add(f.getAbsolutePath());
+ }
+
+ return createDataForAdapter(fileList);
+ }
+
+ private Collection listFiles(File directory, FilenameFilter[] filter,
+ int recurse) {
+
+ Vector files = new Vector();
+
+ File[] entries = directory.listFiles();
+
+ if (entries != null) {
+ for (File entry : entries) {
+ for (FilenameFilter filefilter : filter) {
+ if (filter == null || filefilter.accept(directory, entry.getName())) {
+ files.add(entry);
+ }
+ }
+ if ((recurse <= -1) || (recurse > 0 && entry.isDirectory())) {
+ recurse--;
+ files.addAll(listFiles(entry, filter, recurse));
+ recurse++;
+ }
+ }
+ }
+ return files;
+ }
+
+ public File[] listFilesAsArray(File directory, FilenameFilter[] filter, int recurse) {
+ Collection files = listFiles(directory, filter, recurse);
+
+ File[] arr = new File[files.size()];
+ return files.toArray(arr);
+ }
+
+ // Create an ArrayList with our RescanDataModel
+ private ArrayList createDataForAdapter(List list) {
+
+ ArrayList data = new ArrayList();
+ for (String file : list) {
+
+ data.add(new RescanDataModel(getTitleFromFilePath(file), file));
+ }
+
+ // Sorting the data in alphabetical order
+ Collections.sort(data, new Comparator() {
+ @Override
+ public int compare(RescanDataModel a, RescanDataModel b) {
+ return a.getTitle().compareToIgnoreCase(b.getTitle());
+ }
+ });
+
+ return data;
+ }
+
+ // Remove the file path and the extension and return a file name for the given file path
+ private String getTitleFromFilePath(String path) {
+ return new File(path).getName().replaceFirst("[.][^.]+$", "");
+ }
+ }
+
+ // This items class stores the Data for the ArrayAdapter.
+ // We Have to implement Parcelable, so we can store ArrayLists with this generic type in the Bundle
+ // of onSaveInstanceState() and retrieve it later on in onRestoreInstanceState()
+ private class RescanDataModel implements Parcelable {
+
+ // Interface that must be implemented and provided as a public CREATOR field.
+ // It generates instances of your Parcelable class from a Parcel.
+ public Parcelable.Creator CREATOR = new Parcelable.Creator() {
+
+ @Override
+ public RescanDataModel createFromParcel(Parcel source) {
+ return new RescanDataModel(source);
+ }
+
+ @Override
+ public RescanDataModel[] newArray(int size) {
+ return new RescanDataModel[size];
+ }
+
+ };
+
+ private String mTitle;
+
+ private String mPath;
+
+ private RescanDataModel(String title, String path) {
+ mTitle = title;
+ mPath = path;
+ }
+
+ // This constructor will be called when this class is generated by a Parcel.
+ // We have to read the previously written Data in this Parcel.
+ public RescanDataModel(Parcel parcel) {
+ String[] data = new String[2];
+ parcel.readStringArray(data);
+ mTitle = data[0];
+ mTitle = data[1];
+ }
+
+ public String getTitle() {
+ return mTitle;
+ }
+
+ public String getPath() {
+ return mPath;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ // Write the data to the Parcel, so we can restore this Data later on.
+ // It will be restored by the RescanDataModel(Parcel parcel) constructor.
+ dest.writeArray(new String[]{mTitle, mPath});
+ }
+ }
+
+ // The Adapter for the ListView for when the ListView is populated with the rescanned files
+ private class RescanDataAdapter extends ArrayAdapter {
+
+ public RescanDataAdapter(Context context, int textViewResourceId, List objects) {
+ super(context, textViewResourceId, objects);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+
+ ViewHolder holder;
+
+ // Check if we should inflate the layout for a new row, or if we can reuse a view.
+ if (convertView == null) {
+ convertView = View.inflate(getContext(), android.R.layout.simple_list_item_2, null);
+ holder = new ViewHolder();
+ holder.title = (TextView) convertView.findViewById(android.R.id.text1);
+ holder.path = (TextView) convertView.findViewById(android.R.id.text2);
+ convertView.setTag(holder);
+ } else {
+ holder = (ViewHolder) convertView.getTag();
+ }
+ holder.title.setText(getItem(position).getTitle());
+ holder.path.setText(getItem(position).getPath());
+ return convertView;
+ }
+
+ // We are using the ViewHolder pattern in order to optimize the ListView by reusing
+ // Views and saving them to this item class, and not inlating the layout every time
+ // we need to create a row.
+ private class ViewHolder {
+
+ TextView title;
+
+ TextView path;
+ }
+ }
}