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; + } + } }