From 61598ad3c46384774f8d4ccb51d6dffcff439a8c Mon Sep 17 00:00:00 2001 From: Rashiq Ahmad Date: Fri, 6 Dec 2013 15:17:35 +0100 Subject: [PATCH] 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. --- res/drawable-hdpi/action_refresh.png | Bin 0 -> 677 bytes res/drawable-mdpi/action_refresh.png | Bin 0 -> 492 bytes res/drawable-xhdpi/action_refresh.png | Bin 0 -> 876 bytes res/drawable-xxhdpi/action_refresh.png | Bin 0 -> 1248 bytes res/layout/zimfilelist.xml | 53 +- res/layout/zimfilelistentry.xml | 17 - res/menu/fileselector.xml | 11 + res/values/strings.xml | 1 + .../kiwixmobile/ZimFileSelectActivity.java | 501 ++++++++++++++---- 9 files changed, 433 insertions(+), 150 deletions(-) create mode 100644 res/drawable-hdpi/action_refresh.png create mode 100644 res/drawable-mdpi/action_refresh.png create mode 100644 res/drawable-xhdpi/action_refresh.png create mode 100644 res/drawable-xxhdpi/action_refresh.png delete mode 100644 res/layout/zimfilelistentry.xml create mode 100644 res/menu/fileselector.xml diff --git a/res/drawable-hdpi/action_refresh.png b/res/drawable-hdpi/action_refresh.png new file mode 100644 index 0000000000000000000000000000000000000000..00d70792ae974ed4bb4360104bc4ba98c881ceae GIT binary patch literal 677 zcmV;W0$TlvP)?aoZsX z;_5`g5&-bZRbFeB6zB#z5J|*E6{&DR+yMRnwgnf+Ykoh5Y2_R()FPP}}nQlUriVO$9PRvKULEWpn-@I^zO&uj=JOaL2$ zKmp(okNJ;!Y-lv=^r zeb*CcDkXMI@DgPzmG|e|lErs#v@vU-Je}6K;<~Oa<0L5!05h0ojHQ`2OGf;Z81zCdG{`V*|&?uIV{x$#+blS}N{jc~^WBTfz$cSH~u`U8(u ztJ=lACQ5|iJ_>Yc6QIEq6WA87{x}Erin`%rbpPn?2wXdXZvh4X%CzC5ieaIg00000 LNkvXXu0mjfd7mm; literal 0 HcmV?d00001 diff --git a/res/drawable-mdpi/action_refresh.png b/res/drawable-mdpi/action_refresh.png new file mode 100644 index 0000000000000000000000000000000000000000..c2920a7a04e069523bb7ba7578f5e282ca0b4615 GIT binary patch literal 492 zcmVg{EnKz99 zM&01z)Ow6GLNNM05uT?J5WrnJUwK+_?jAh#Wad3I13LolBCY^fTI|^a9=-(-Q2|3i zz@Grz3Uf=`d!MOC(I^C35>XA{2W6Ww5kNTs+TLg2pXlwHdT}SbFUa%$8U%RNuVzva zCS(uoOi8{c1zPj0i}7z#H^fiA+a8<|KQ+w|J!;lEtS>} z%nxdyVU^6u^?1~Lt+={SBa^q_$bh6S*!fg1lt70W7+W$*>*-ROvU%gBbgq1#T}H8@ i|KXpDoBf7;3ormMYncI1hIiF4v`ZBh#-pr0k#+Ln_c2`&JkhXBa~KRScU3)l~^1=tcS?cCCUUBIqjzroJ4 z+3X@{2cP@u{w=z16av7kFmMYh;v3s4tuPoq}PzQ zU>g}-mjAh%bMZ;9`Lc%z+}|7QHrhk%Ma zy7z~o$5p~saM7LSW-bKg4!vJ0+z(y-&4Ikh@VR)n0OX!)1^LRdT~qSTu=RjamdG5) zuWiY@B)LSEKpU4jz%~iWTz_Md>A9@mNXU;X1ZaE~WoNA1 zP9c`ora9n6^8bCPYYV0V2xC9g-uERyu;h&d=w#~u;(rnXC}w?i$O1T5gRbMS$8tR7 zLiDV&Qk1agKq<&iJDu>Hvx&pR1Fod%b%b%B^)3LFw!4vb5Q3=+Q5P->7 zp)XSrpb(MQtxV^EyMHE`plQ!eL-zpLviafZ1)!Cwm83e=o}DJ1K15VE$d=2~j@#~R zHa0t(uOb)?)jp=eTg&H;4!gRtC3)=qgGoa^Lf~pizOwYVipf`xjob7gKs^%jRvz`} zYS3!mfQGwYcP2fXLDmE4<+uD!PG7V%EM~?QOWYdRB^&x=t;9_TMU6ttC1bOa@ zbRU)3fetShY>EKl+sTseOk5%&A|fIpBErHS0R{kaQ#JhFunA%S0000I0igu^~k-A2?a1F+XG`QaJ@;@%j5B%MC3 zqz4rehMvj))*z%)mXd!o{xxl`uQTRaVa&&<;jP~XFGCxUpR!UpXu{786c#IQ#u-aKyAYY{5 zan#wQ$T@`;0ZYq}r`-M|QSSR}E7!%HkoQk5b$%w*YjI4zm~0*j4BWQFC-P@?)s5jE z9vpjq6jB5%EN+-cUkH-F45bjeb3FLi((|4qUUCSMKMo}Y*`%J0P|nW_uH%JhOAXTZ zS|pw&--bD@gqeWvCY`SbG*f^`3bOJ3H7J)ez!$^!d&yUOH*ElZxnfd28_+Em0j8qs zDAh}W0qkVj9#jh%fYpzw=W_tnauHzi{-Z?23j@5t$4Qy1g)f872cX zYBs_E<=#&Pm}#Qf0S1u6)Z%9l0fOFdGQhs++ddfJI%uMqsRz_}KOq?4viHXp-k-Uc zXy#*)-g^xlmv~mB$`?cYw>^!-JX20RD~lg>GSSS(I^h@PzMV^j&s^w!Z5H%p#oboA zo`PIA>*%337h2q5yIa=E)ipzV21t_sVYjSny5Hp_V;?Cfb4-3Jpeb?BE^}^S8SS*fuTkC?S>5Bq08K)6 z?Mm(yS_{*PvN&~}_d`lSkD9no?16`x#3$-+OLl)yRNL-Z!-m5 z)YzPZ4_r7Us`>t(nR?s4zH=)!eaTQF^5vb{sMK65tVhnZ{kWu7P - - - + + - - + + + + + + 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; + } + } }