Adam
Adam

Reputation: 2570

Loading File into ListFragment with Multiple Choice

In Android, How would one save a text file from the internet to the sdcard, load the file from the sdcard and separate the entries of the file by commas into a ListView?

It's important that more than one of the entries in the ListView can be selected at once. The use of fragments would be nice too, as it would make it easier to use with different screen sizes such as phones vs. tablets.

Upvotes: 0

Views: 1141

Answers (1)

Adam
Adam

Reputation: 2570

I see your question as 2 different problems, each with their own solutions and obstacles, that come together in the end. My examples are all compiled against API16 (4.1 Jelly Bean) with a minimum of API11 (3.0 Honeycomb). - WARNING - large amounts of text incoming.

Load from Internet

Loading from the internet seems overwhelming at first, although it is simple in the end. First you want to ensure that the device has a connection. To do this, you create a method called getConnectivityStatus as seen below:

public boolean getConnectivityStatus() {
    ConnectivityManager cm = (ConnectivityManager) this
            .getSystemService(CONNECTIVITY_SERVICE);
    NetworkInfo info = cm.getActiveNetworkInfo();
    if (info != null)
        return info.isConnected();
    else
        return false;
}

If a connection exists, you'll want to create a directory to save the file and download the file using the DownloadManager class. To do this, simply say:

File directory = new File(Environment.getExternalStorageDirectory(), "ExampleDirectory");
if (!directory.exists())
        directory.mkdir();

Next, you'll want to download the file with the method downloadFile(String), passing in the file name you'll want. If you want only a single copy of the file at any given time, you'll have to delete the old file if it exists before downloading, or you will have multiple files such as examplefile.txt; examplefile-1.txt; examplefile-2.txt; Place this first portion of code in the method you want to start the download from, such as onClick:

String FILE_NAME = "examplefile.txt",
File examplefile = new File(Environment.getExternalStorageDirectory()
        + "/ExampleDirectory", FILE_NAME);
if (examplefile.exists()) {
    boolean deleted = examplefile.delete();
    if (deleted) {
        if (getConnectivityStatus())
            downloadFile(FILE_NAME);
    }
}

The downloadFile(String) method:

public void downloadFile(String FILE_NAME) {
    String url = "http://www.example.com/filetobedownloaded.txt";
    DownloadManager.Request request = new DownloadManager.Request(
            Uri.parse(url));
    request.setDescription("Example file to be displayed.");
    request.setTitle(FILE_NAME);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        request.allowScanningByMediaScanner();
        request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE);
    }
    request.setDestinationInExternalPublicDir("ExampleDirectory", FILE_NAME);

    DownloadManager manager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
    manager.enqueue(request);
}

You can also register a receiver to return a callback when the download is complete. To do so, simple register the receiver in the onCreate method like so: registerReceiver(onComplete, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); and place the below method in your class:

BroadcastReceiver onComplete = new BroadcastReceiver() {
    public void onReceive(Context ctxt, Intent intent) {
        if (!started) {
            started = true;
            // perform action upon completion
        }
    }
};

Here is the final DownloadFile.java class:

public class DownloadFile extends Activity {

boolean started = false;
String url = "http://www.example.com/filetobedownloaded.txt";
String FILE_NAME = "examplefile.txt",
File directory = new File(Environment.getExternalStorageDirectory(), "ExampleDirectory");
File examplefile = new File(Environment.getExternalStorageDirectory()
        + "/ExampleDirectory", FILE_NAME);

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_downloadfile);
    registerReceiver(onComplete, new IntentFilter(
            DownloadManager.ACTION_DOWNLOAD_COMPLETE));
    check();
}

public void check {
    if (!directory.exists())
        directory.mkdir();
    if (!getConnectivityStatus()) {
        if (!started) {
            started = true;
            // perform action if no connection
        }
    }
    if (examplefile.exists()) {
        boolean deleted = examplefile.delete();
            if (deleted && !started) {
                if (getConnectivityStatus())
                    downloadFile(FILE_NAME);
            }
    }
}

public boolean getConnectivityStatus() {
    ConnectivityManager cm = (ConnectivityManager) this
            .getSystemService(CONNECTIVITY_SERVICE);
    NetworkInfo info = cm.getActiveNetworkInfo();
    if (info != null)
        return info.isConnected();
    else
        return false;
}

public void downloadFile(String FILE_NAME) {
    String url = "http://www.example.com/filetobedownloaded.txt";
    DownloadManager.Request request = new DownloadManager.Request(
            Uri.parse(url));
    request.setDescription("Example file to be displayed.");
    request.setTitle(FILE_NAME);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        request.allowScanningByMediaScanner();
        request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE);
    }
    request.setDestinationInExternalPublicDir("ExampleDirectory", FILE_NAME);

    DownloadManager manager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
    manager.enqueue(request);
}

BroadcastReceiver onComplete = new BroadcastReceiver() {
    public void onReceive(Context ctxt, Intent intent) {
        if (!started) {
            started = true;
            // perform action upon completion
        }
    }
};


Load into ListFragment

In order to load the file into a ListFragment, and later display the selected item, you'll have to create 3 classes and 2 xml files in the layout directory. In my example, I will use MainActivity.java , PreviewFragment.java , SelectionFragment.java , activity_main.xml , and fragment_preview.xml . We'll start with the xml. The first xml file is the one you are viewing, which contains the two fragments we are working with: the ListFragment and the PreviewFragment. The setup is fairly simple; you specify the two fragment, their ids and constraints, and their respective classes. Here is activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal" >

<fragment
    android:id="@+id/fragmentSelection"
    android:layout_width="0dp"
    android:layout_height="match_parent"
    android:layout_weight="2"
    class="com.smarticle.catering.update.SelectionFragment" />

<fragment
    android:id="@+id/fragmentPreview"
    android:layout_width="0dp"
    android:layout_height="match_parent"
    android:layout_weight="1"
    class="com.smarticle.catering.update.PreviewFragment" />
</LinearLayout>

The above layout is optimized for a tablet in landscape mode. You can tweak the arrangement if you feel so inclined.
Next, you'll have to specify the PreviewFragment in xml, also fairly simple since it is only a TextView centered horizontally and vertically which will eventually display the selected items. Here is fragment_preview.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

<TextView
    android:id="@+id/tvPreview"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerInParent="true"
    android:text="@string/app_name"
    android:textAppearance="?android:attr/textAppearanceLarge" />
</RelativeLayout>

The ListFragment will be created at runtime, so it will not need its own xml file.
In order to display the fragments on the screen, you'll have to load the activity_main.xml layout in your activity. This is a very simple task as it looks like every other activity, ever. This is MainActivity.java:

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

That's it, seriously. But now onward. In order to change the text in the PreviewFragment, you'll have to extend the Fragment class in PreviewFragment.java, inflate the view and setup a setText method. The PreviewFragment.java class is shown below:

public class PreviewFragment extends Fragment {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_preview, container,
                false);
        return view;
    }

    public void setText(String item) {
        TextView tvPreview = (TextView) getView().findViewById(R.id.tvPreview);
        tvPreview.setText(item);
    }

}

The lifecycle for a fragment can be found here.
Now you'll have to setup the ListFragment. This will be done in the ListFragment.java class. In the onActivityCreated() method, you'll want to load the file, ensuring that it actually downloaded and is in the correct directory with the load(String) method. At this time, you'll also separate the file by its delimiter into an array. This is the load(String) method:

public void load(String FILE_NAME) {
String[] list;
String FILE_NAME = "examplefile.txt",
File directory = new File(Environment.getExternalStorageDirectory(), "ExampleDirectory");
File examplefile = new File(Environment.getExternalStorageDirectory()
    + "/ExampleDirectory", FILE_NAME);
if (examplefile.exists()) {
    try {
        File myFile = new File(directory + "/" + FILE_NAME);
        FileInputStream fIn = new FileInputStream(myFile);
        BufferedReader myReader = new BufferedReader(new InputStreamReader(
                fIn));
        String aDataRow = "";
        String aBuffer = "";
        while ((aDataRow = myReader.readLine()) != null) {
            aBuffer += aDataRow;
            aBuffer = aBuffer.trim();
            list = aBuffer.split(",");
        }
        myReader.close();
        if (!loaded)
            Toast.makeText(getActivity(),
                    "Done reading '" + FILE_NAME + "'.", Toast.LENGTH_SHORT)
                    .show();
        loaded = true;
        if (!selections.equals("")) {
            for (int i = 0; i < selections.size(); i++) {
                getListView().setItemChecked(selections.get(i), true);
            }
        }
    } catch (Exception e) {
        Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_SHORT)
                .show();
        }
    }
}

This will return the String array list with the contents of examplefile.txt separated by commas. You can replace the commas with whatever delimiter you want, as long as the expression of aBuffer.split(String delimiter) is consistent with the delimiter in the text file. The boolean value loaded is only to make sure a new Toast doesn't appear everytime the activity is recreated, such as on an orientation change.
In the load(String) method, it's also a good time to set up your ListFragment's adapter and mode. You'll want to select a textViewResourceId that allows multiple choice, unless you want single choice. This can be done smoothly after the while statement by simple inserting these lines:

ArrayAdapter<String> adapter = new ArrayAdapter<String>(
                getActivity(),
                android.R.layout.simple_list_item_activated_1, list);
setListAdapter(adapter);
getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);

If single choice is desired, change ListView.CHOICE_MODE_MULTIPLE to ListView.CHOICE_MODE_SINGLE and change android.R.layout.simple_list_item_activated_1 to android.R.layout.simple_list_item_1. Alternatively, if you want checks instead of highlighted, change to android.R.layout.simple_list_item_checked.
In this class, you must also initialize your PreviewFragment, done so in the onActivityCreated method like so: fragment = (PreviewFragment) getFragmentManager().findFragmentById( R.id.fragmentPreview);
Lastly, you'll have to be able to read which items are selected in the ListFragment and display them on the PreviewFragment. This I do with a method named getSelectedItems() shown below:

public void getSelectedItems() {
    cntChoice = getListView().getCount();
    items = "";
    selections.clear();
    SparseBooleanArray sparseBooleanArray = getListView()
            .getCheckedItemPositions();
    for (int i = 0; i < cntChoice; i++) {
        if (sparseBooleanArray.get(i) == true) {
            items += getListView().getItemAtPosition(i).toString()
                    + ";\n";
            selections.add(i);
        }
    }
    if (fragment != null && fragment.isInLayout())
        fragment.setText(items);
}

The String items is what is displayed in the TextView, and selections is an ArrayList<Integer> used to restore the state upon an orientation change. Normally, you would specify an android:configChanges="orientation" in the AndroidManifest.xml file under the <activity > tag, but the problem occurs when using separate layouts for portrait or landscape. If you allow the Manifest to handle orientation changes, the layout is not changed when the orientation is changed because a new activity is not created like it would be under normal circumstances. Therefor, you create static ArrayList<Integer> containing the positions which contain a selected item.
The last thing to do is read when a ListItem has been clicked and to call the getSelectedItems method, a fairly simple task. Insert this below anywhere into your class:

@Override
public void onListItemClick(ListView l, View v, int position, long id) {
    super.onListItemClick(l, v, position, id);
    getItems();
}

You're finished! Now to put it all together. Here is the ListFragment.java class:

public class ListFragment extends ListFragment {

    String FILE_NAME = "examplefile.txt", items = "";
    String[] list;
    static ArrayList<Integer> selections = new ArrayList<Integer>();
    int cntChoice, position;
    static boolean loaded = false;
    File directory = new File(Environment.getExternalStorageDirectory(), "ExampleDirectory");
    File examplefile = new File(Environment.getExternalStorageDirectory()
            + "/ExampleDirectory", FILE_NAME);
    PreviewFragment fragment;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        fragment = (PreviewFragment) getFragmentManager().findFragmentById(
                R.id.fragmentPreview);
        check();
    }

    @Override
    public void onListItemClick(ListView l, View v, int position, long id) {
        super.onListItemClick(l, v, position, id);
        getSelectedItems();
    }

    public void getSelectedItems() {
        cntChoice = getListView().getCount();
        items = "";
        selections.clear();
        SparseBooleanArray sparseBooleanArray = getListView()
                .getCheckedItemPositions();
        for (int i = 0; i < cntChoice; i++) {
            if (sparseBooleanArray.get(i) == true) {
                items += getListView().getItemAtPosition(i).toString()
                        + ";\n";
                selections.add(i);
            }
        }
        if (fragment != null && fragment.isInLayout())
            fragment.setText(items);
    }

    public void check() {
        if (examplefile.exists())
            load(FILE_NAME);
    }

    public void load(String FILE_NAME) {
        try {
            File myFile = new File(directory + "/" + FILE_NAME);
            FileInputStream fIn = new FileInputStream(myFile);
            BufferedReader myReader = new BufferedReader(new InputStreamReader(
                    fIn));
            String aDataRow = "";
            String aBuffer = "";
            while ((aDataRow = myReader.readLine()) != null) {
                aBuffer += aDataRow;
                aBuffer = aBuffer.trim();
                list = aBuffer.split(",");
            }
            ArrayAdapter<String> adapter = new ArrayAdapter<String>(
                    getActivity(),
                    android.R.layout.simple_list_item_activated_1,     list);
            setListAdapter(adapter);
            getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
            myReader.close();
            if (!loaded)
                Toast.makeText(getActivity(),
                        "Done reading '" + FILE_NAME + "'.",     Toast.LENGTH_SHORT)
                        .show();
            loaded = true;
            if (!selections.equals("")) {
                for (int i = 0; i < selections.size(); i++) {
                    getListView().setItemChecked(selections.get(i),     true);
                }
                getSelectedItems();
            }
        } catch (Exception e) {
            Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_SHORT)
                    .show();
        }
    }
}

Conclusion

I hope this solved your problem. I know it's lengthy, but it's thorough, tested and works well. To start this, you can do an Intent intent = new Intent(getBaseContext(), MainActivity.class); startActivity(intent); finish(); in the onReceive method of the receiver in the DownloadFile.java class. I would also suggest placing those same lines of code in the check() method, specifically the portion that is called if there is no connection, in which case, it will load the file that was previously downloaded to the directory. Good luck, happy coding and always remember... 01101000011101000111010001110000011100110011101000101111001011110111011101110111011101110010111001111001011011110111010101110100011101010110001001100101001011100110001101101111011011010010111101110111011000010111010001100011011010000011111101110110001111010110100101110101011000100100101000101101010110000101001101001100001110010110011101101111

Upvotes: 2

Related Questions