New Rescan feature: We will still populate the initial File choser by the MediaStore, but now we can give the user the opportunity to rescan the file sytem for the actual files, *if* the

MediaStore is out of sync. The new scanner will recursivly scan through all the files on the device and will find the files with the extensions .zim or .zimaa.
This commit is contained in:
Rashiq Ahmad 2013-12-06 15:17:35 +01:00
parent 9f0a4e355f
commit 61598ad3c4
9 changed files with 433 additions and 150 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 677 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 876 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,15 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="fill_parent"> android:layout_height="fill_parent">
<ListView
<ListView
android:id="@+id/zimfilelist" android:id="@+id/zimfilelist"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" > android:layout_height="match_parent">
</ListView> </ListView>
<TextView <TextView
android:id="@+id/zimfilelist_nozimfilesfound_view" android:id="@+id/zimfilelist_nozimfilesfound_view"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -20,5 +19,15 @@
android:paddingLeft="10dp" android:paddingLeft="10dp"
android:paddingRight="10dp" android:paddingRight="10dp"
android:text="@string/error_nozimfilesfound" android:text="@string/error_nozimfilesfound"
android:visibility="gone" /> android:visibility="gone"/>
</LinearLayout>
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:visibility="gone">
</ProgressBar>
</RelativeLayout>

View File

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content" >
<TextView
android:id="@+id/zim_file_list_entry_path"
style="@android:style/TextAppearance.Large"
android:paddingBottom="15dp"
android:paddingTop="15dp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</LinearLayout>

11
res/menu/fileselector.xml Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_rescan"
android:icon="@drawable/action_refresh"
android:title="@string/menu_rescan"
android:showAsAction="always"/>
</menu>

View File

@ -14,6 +14,7 @@
<string name="menu_exitfullscreen">Exit full screen</string> <string name="menu_exitfullscreen">Exit full screen</string>
<string name="menu_exit">Exit</string> <string name="menu_exit">Exit</string>
<string name="menu_share">Share with friends</string> <string name="menu_share">Share with friends</string>
<string name="menu_rescan">Rescan</string>
<string name="save_image">Save Image</string> <string name="save_image">Save Image</string>
<string name="save_image_error">An error occured when trying to save an image!</string> <string name="save_image_error">An error occured when trying to save an image!</string>
<string name="save_image_saved">Saved image as %s in your Pictures folder.</string> <string name="save_image_saved">Saved image as %s in your Pictures folder.</string>

View File

@ -1,33 +1,61 @@
package org.kiwix.kiwixmobile; package org.kiwix.kiwixmobile;
import java.io.File; import android.content.Context;
import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.content.Loader;
import android.database.Cursor; import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment;
import android.os.Parcel;
import android.os.Parcelable;
import android.provider.MediaStore; 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.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.view.Window; import android.view.Window;
import android.widget.Adapter; import android.widget.Adapter;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemClickListener;
import android.widget.SimpleCursorAdapter; import android.widget.ArrayAdapter;
//TODO API level 11 (honeycomb). use compatiblity packages instead import android.widget.ListView;
import android.content.CursorLoader; import android.widget.ProgressBar;
import android.app.LoaderManager; import android.widget.TextView;
public class ZimFileSelectActivity extends Activity implements import java.io.File;
LoaderManager.LoaderCallbacks<Cursor> { 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;
public class ZimFileSelectActivity extends FragmentActivity
implements LoaderManager.LoaderCallbacks<Cursor>, OnItemClickListener {
private static final int LOADER_ID = 0x02; 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 SimpleCursorAdapter mCursorAdapter;
private ListView zimFileList;
private View tmpZimFileList; // Adapter of the Data populated by recanning the Filesystem by ourselves
private RescanDataAdapter mRescanAdapter;
private ArrayList<RescanDataModel> mFiles;
private ListView mZimFileList;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
@ -35,9 +63,11 @@ LoaderManager.LoaderCallbacks<Cursor> {
requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
setProgressBarIndeterminateVisibility(true); setProgressBarIndeterminateVisibility(true);
setContentView(R.layout.zimfilelist); setContentView(R.layout.zimfilelist);
mZimFileList = (ListView) findViewById(R.id.zimfilelist);
mFiles = new ArrayList<RescanDataModel>();
selectZimFile(); selectZimFile();
} }
private void finishResult(String path) { private void finishResult(String path) {
@ -54,95 +84,69 @@ LoaderManager.LoaderCallbacks<Cursor> {
protected void selectZimFile() { protected void selectZimFile() {
// Defines a list of columns to retrieve from the Cursor and load into an output row // Defines a list of columns to retrieve from the Cursor and load into an output row
String[] mZimListColumns = String[] mZimListColumns = {MediaStore.Files.FileColumns.TITLE, MediaStore.Files.FileColumns.DATA};
{
MediaStore.Images.Media.DATA
};
// Defines a list of View IDs that will receive the Cursor columns for each row // Defines a list of View IDs that will receive the Cursor columns for each row
int[] mZimListItems = { R.id.zim_file_list_entry_path}; int[] mZimListItems = {android.R.id.text1, android.R.id.text2};
mCursorAdapter = new SimpleCursorAdapter( mCursorAdapter = new SimpleCursorAdapter(
getApplicationContext(), // The application's Context object // The Context object
R.layout.zimfilelistentry, // A layout in XML for one row in the ListView ZimFileSelectActivity.this,
null, // The cursor, swapped later by cursorloader // A layout in XML for one row in the ListView
mZimListColumns, // A string array of column names in the cursor android.R.layout.simple_list_item_2,
mZimListItems, // An integer array of view IDs in the row layout // 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); Adapter.NO_SELECTION);
// Sets the adapter for the ListView mZimFileList.setAdapter(mCursorAdapter);
setContentView(R.layout.zimfilelist); mZimFileList.setOnItemClickListener(this);
// For a reason I ingore, it seems that time to time getSupportLoaderManager().initLoader(LOADER_ID, null, this);
// 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);
}
});
}
//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("kiwix", " zimFileList.onItemClick");
ListView zimFileList = (ListView) findViewById(R.id.zimfilelist);
Cursor mycursor = (Cursor) zimFileList.getItemAtPosition(position);
//TODO not very clean
finishResult(mycursor.getString(1));
} }
@Override @Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) { public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
//TODO leads to API min 11
Uri uri = MediaStore.Files.getContentUri("external"); Uri uri = MediaStore.Files.getContentUri("external");
String[] projection = { String[] projection = {
MediaStore.Images.Media._ID, MediaStore.Files.FileColumns._ID,
MediaStore.Images.Media.DATA, //Path // File Name
MediaStore.Files.FileColumns.TITLE,
// File Path
MediaStore.Files.FileColumns.DATA
}; };
// exclude media files, they would be here also (perhaps // Exclude media files, they would be here also (perhaps
// somewhat better performance), and filter for zim files // somewhat better performance), and filter for zim files
// (normal and first split) // (normal and first split)
String selection = MediaStore.Files.FileColumns.MEDIA_TYPE + "=" String query = MediaStore.Files.FileColumns.MEDIA_TYPE + "="
+ MediaStore.Files.FileColumns.MEDIA_TYPE_NONE + " AND " + MediaStore.Files.FileColumns.MEDIA_TYPE_NONE + " AND"
+ " ( LOWER(" + + " ( LOWER(" +
MediaStore.Images.Media.DATA + ") LIKE '%.zim'" MediaStore.Images.Media.DATA + ") LIKE '%." + zimFiles[0] + "'"
+ " OR LOWER(" + + " OR LOWER(" +
MediaStore.Images.Media.DATA + ") LIKE '%.zimaa'" MediaStore.Images.Media.DATA + ") LIKE '%." + zimFiles[1] + "'"
+" ) "; + " ) ";
String[] selectionArgs = null; // There is no ? in query so null here
String[] selectionArgs = null; // there is no ? in selection so null here String sortOrder = MediaStore.Images.Media.TITLE; // Sorted alphabetical
String sortOrder = MediaStore.Images.Media.DATA; // unordered
Log.d("kiwix", " Performing query for zim files..."); 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 @Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) { public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
Log.d("kiwix", " DONE query zim files"); Log.d("kiwix", "DONE querying Mediastore for .zim files");
mCursorAdapter.swapCursor(cursor); mCursorAdapter.swapCursor(cursor);
//Done here to avoid that shown while loading. // Done here to avoid that shown while loading.
zimFileList.setEmptyView( findViewById( R.id.zimfilelist_nozimfilesfound_view ) ); mZimFileList.setEmptyView(findViewById(R.id.zimfilelist_nozimfilesfound_view));
setProgressBarIndeterminateVisibility(false); mCursorAdapter.notifyDataSetChanged();
} }
@Override @Override
@ -150,4 +154,279 @@ LoaderManager.LoaderCallbacks<Cursor> {
mCursorAdapter.swapCursor(null); mCursorAdapter.swapCursor(null);
} }
@Override
protected void onSaveInstanceState(Bundle outState) {
// 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<RescanDataModel> 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<Void, Void, Void> {
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<RescanDataModel> FindFiles() {
String directory = new File(
Environment.getExternalStorageDirectory().getAbsolutePath()).toString();
final List<String> fileList = new ArrayList<String>();
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<File> listFiles(File directory, FilenameFilter[] filter,
int recurse) {
Vector<File> files = new Vector<File>();
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<File> files = listFiles(directory, filter, recurse);
File[] arr = new File[files.size()];
return files.toArray(arr);
}
// Create an ArrayList with our RescanDataModel
private ArrayList<RescanDataModel> createDataForAdapter(List<String> list) {
ArrayList<RescanDataModel> data = new ArrayList<RescanDataModel>();
for (String file : list) {
data.add(new RescanDataModel(getTitleFromFilePath(file), file));
}
// Sorting the data in alphabetical order
Collections.sort(data, new Comparator<RescanDataModel>() {
@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<RescanDataModel> CREATOR = new Parcelable.Creator<RescanDataModel>() {
@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<RescanDataModel> {
public RescanDataAdapter(Context context, int textViewResourceId, List<RescanDataModel> 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;
}
}
} }