Java tutorial
/* * FlightIntel for Pilots * * Copyright 2011 Nadeem Hasan <nhasan@nadmm.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.nadmm.airports; import java.io.File; import java.io.FileInputStream; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.UUID; import java.util.zip.GZIPInputStream; import org.apache.http.impl.client.DefaultHttpClient; import android.app.AlertDialog; import android.app.ProgressDialog; import android.content.ContentValues; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.database.Cursor; import android.database.MatrixCursor; import android.database.sqlite.SQLiteDatabase; import android.graphics.Color; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.ResultReceiver; import android.provider.BaseColumns; import android.sax.Element; import android.sax.EndElementListener; import android.sax.EndTextElementListener; import android.sax.RootElement; import android.text.format.DateUtils; import android.text.format.Formatter; import android.text.format.Time; import android.util.Log; import android.util.TimeFormatException; import android.util.Xml; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.Button; import android.widget.ListView; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import com.nadmm.airports.DatabaseManager.Catalog; import com.nadmm.airports.afd.AfdMainActivity; import com.nadmm.airports.utils.ExternalStorageActivity; import com.nadmm.airports.utils.NetworkUtils; import com.nadmm.airports.utils.SectionedCursorAdapter; import com.nadmm.airports.utils.SystemUtils; import com.nadmm.airports.utils.UiUtils; public final class DownloadActivity extends ActivityBase { private static final String TAG = DownloadActivity.class.getName(); //private static final String HOST = "10.0.2.2"; //private static final String HOST = "192.168.1.117"; private static final String HOST = "commondatastorage.googleapis.com"; private static final Integer PORT = 80; //private static final String PATH = "/~nhasan/fadds"; private static final String PATH = "/flightintel/database"; private static final String MANIFEST = "manifest.xml"; private final Map<String, ProgressTracker> mTrackers = new HashMap<String, ProgressTracker>(); final class DataInfo implements Comparable<DataInfo> { public String type; public String desc; public int version; public String fileName; public int size; public Time start; public Time end; public DataInfo() { } public DataInfo(DataInfo info) { type = info.type; desc = info.desc; version = info.version; fileName = info.fileName; size = info.size; start = info.start; end = info.end; } @Override public boolean equals(Object o) { DataInfo info = (DataInfo) o; return type.equals(info.type) && version == info.version; } @Override public int compareTo(DataInfo info) { if (!type.equals(info.type)) { return type.compareTo(info.type); } return Time.compare(start, info.start); } } private final ArrayList<DataInfo> mInstalledData = new ArrayList<DataInfo>(); private final ArrayList<DataInfo> mAvailableData = new ArrayList<DataInfo>(); private DatabaseManager mDbManager; private DownloadTask mDownloadTask; private Handler mHandler; private ListView mListView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mDbManager = DatabaseManager.instance(this); mDownloadTask = null; mHandler = new Handler(); setContentView(createContentView(R.layout.download_list_view)); // Add the footer view View footer = inflate(R.layout.download_footer); mListView = (ListView) findViewById(android.R.id.list); mListView.addFooterView(footer); mListView.setFooterDividersEnabled(true); Button btnDownload = (Button) findViewById(R.id.btnDownload); btnDownload.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { checkNetworkAndDownload(); } }); Button btnDelete = (Button) findViewById(R.id.btnDelete); btnDelete.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { checkDelete(); } }); Intent intent = getIntent(); if (intent.hasExtra("MSG")) { String msg = intent.getStringExtra("MSG"); Toast.makeText(this, msg, Toast.LENGTH_LONG).show(); } checkData(false); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: Intent afd = new Intent(this, AfdMainActivity.class); startActivity(afd); return true; default: return super.onOptionsItemSelected(item); } } private void checkData(boolean startDownload) { CheckDataTask task = new CheckDataTask(); task.execute(startDownload); } private void checkNetworkAndDownload() { NetworkUtils.checkNetworkAndDownload(this, new Runnable() { @Override public void run() { download(); } }); } private void download() { if (SystemUtils.isExternalStorageAvailable()) { mHandler.post(new Runnable() { @Override public void run() { mDownloadTask = new DownloadTask(DownloadActivity.this); mDownloadTask.execute(); } }); } else { Intent intent = new Intent(this, ExternalStorageActivity.class); startActivity(intent); finish(); } } private final class ProgressTracker { public TextView msgText; public TextView statusText; public ProgressBar progressBar; public ProgressTracker(View v) { msgText = (TextView) v.findViewById(R.id.download_msg); statusText = (TextView) v.findViewById(R.id.download_status); progressBar = (ProgressBar) v.findViewById(R.id.download_progress); } public void initProgress(int resid, int max) { progressBar.setMax(max); msgText.setText(resid); setProgress(0); showProgress(); } public void showProgress() { progressBar.setVisibility(View.VISIBLE); statusText.setVisibility(View.VISIBLE); msgText.setVisibility(View.VISIBLE); } public void hideProgress() { progressBar.setVisibility(View.GONE); statusText.setVisibility(View.GONE); msgText.setVisibility(View.GONE); } public void setProgress(int progress) { progressBar.setProgress(progress); if (progress < progressBar.getMax()) { Context context = DownloadActivity.this; statusText.setText(String.format("%s of %s", Formatter.formatShortFileSize(context, progress), Formatter.formatShortFileSize(context, progressBar.getMax()))); } else { msgText.setText(R.string.install_done); statusText.setVisibility(View.GONE); } } } private final class DownloadCursor extends MatrixCursor { private static final String SECTION = "SECTION"; private static final String TYPE = "TYPE"; private static final String DESC = "DESC"; private static final String DATES = "DATES"; private static final String MSG = "MSG"; private static final String EXPIRED = "EXPIRED"; private int mId = 0; private final long mNow = System.currentTimeMillis(); private final long mSpeed = 500; public DownloadCursor() { super(new String[] { BaseColumns._ID, SECTION, TYPE, DESC, DATES, MSG, EXPIRED }); } public void addRow(int section, DataInfo info) { RowBuilder builder = newRow(); builder.add(mId++).add(section).add(info.type).add(info.desc) .add("Effective " + DateUtils.formatDateRange(DownloadActivity.this, info.start.toMillis(false), info.end.toMillis(false) + 1000, DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_ABBREV_ALL)) .add(String.format("%s (%s @ %dkbps)", Formatter.formatShortFileSize(DownloadActivity.this, info.size), DateUtils.formatElapsedTime(info.size / (mSpeed * 1024 / 8)), mSpeed)) .add((mNow > info.end.toMillis(false)) ? "Y" : "N"); } } private final class DownloadListAdapter extends SectionedCursorAdapter { public DownloadListAdapter(Context context, Cursor c) { super(context, R.layout.download_list_item, c, R.layout.list_item_header); } @Override public String getSectionName() { Cursor c = getCursor(); int section = c.getInt(c.getColumnIndex(DownloadCursor.SECTION)); return getResources().getString(section); } @Override public View newView(Context context, Cursor c, ViewGroup parent) { View view = super.newView(context, c, parent); int section = c.getInt(c.getColumnIndex(DownloadCursor.SECTION)); if (section == R.string.download_available) { String type = c.getString(c.getColumnIndex(DownloadCursor.TYPE)); mTrackers.put(type, new ProgressTracker(view)); } else { View msg = view.findViewById(R.id.download_msg); msg.setVisibility(View.GONE); View status = view.findViewById(R.id.download_status); status.setVisibility(View.GONE); } return view; } @Override public boolean areAllItemsEnabled() { return true; } @Override public boolean isEnabled(int position) { return false; } @Override public void bindView(View view, Context context, Cursor cursor) { TextView tv; tv = (TextView) view.findViewById(R.id.download_desc); tv.setText(cursor.getString(cursor.getColumnIndex(DownloadCursor.DESC))); tv = (TextView) view.findViewById(R.id.download_dates); String expired = cursor.getString(cursor.getColumnIndex(DownloadCursor.EXPIRED)); tv.setText(cursor.getString(cursor.getColumnIndex(DownloadCursor.DATES))); if (expired.equals("Y")) { tv.setText(tv.getText() + " (Expired)"); tv.setTextColor(Color.RED); } tv = (TextView) view.findViewById(R.id.download_msg); tv.setText(cursor.getString(cursor.getColumnIndex(DownloadCursor.MSG))); } } private Cursor createCursor() { DownloadCursor c = new DownloadCursor(); for (DataInfo info : mAvailableData) { c.addRow(R.string.download_available, info); } for (DataInfo info : mInstalledData) { c.addRow(R.string.download_installed, info); } return c; } private final class CheckDataTask extends AsyncTask<Boolean, Void, Integer> { private DownloadActivity mActivity; public CheckDataTask() { mActivity = DownloadActivity.this; } @Override protected void onPreExecute() { Button btnDownload = (Button) findViewById(R.id.btnDownload); btnDownload.setEnabled(false); mInstalledData.clear(); mAvailableData.clear(); } @Override protected Integer doInBackground(Boolean... params) { boolean startDownload = params[0]; int result; result = getInstalled(); if (result != 0) { return result; } result = downloadManifest(); if (result != 0) { return result; } result = parseManifest(); if (result != 0) { return result; } processManifest(); if (startDownload) { download(); } return 0; } @Override protected void onPostExecute(Integer result) { if (result != 0) { TextView empty = (TextView) findViewById(android.R.id.empty); empty.setText(R.string.download_error); return; } updateDownloadList(); } private int getInstalled() { mInstalledData.clear(); Time now = new Time(); now.setToNow(); Cursor c = mDbManager.getAllFromCatalog(); if (c.moveToFirst()) { do { DataInfo info = new DataInfo(); info.type = c.getString(c.getColumnIndex(Catalog.TYPE)); info.desc = c.getString(c.getColumnIndex(Catalog.DESCRIPTION)); info.version = c.getInt(c.getColumnIndex(Catalog.VERSION)); String start = c.getString(c.getColumnIndex(Catalog.START_DATE)); String end = c.getString(c.getColumnIndex(Catalog.END_DATE)); Log.i(TAG, info.type + "," + start + "," + end); try { info.start = new Time(); info.start.parse3339(start); info.start.normalize(false); info.end = new Time(); info.end.parse3339(end); info.end.normalize(false); if (now.before(info.end)) { mInstalledData.add(info); } } catch (TimeFormatException e) { UiUtils.showToast(mActivity, e.getMessage()); return -1; } } while (c.moveToNext()); } c.close(); return 0; } private int downloadManifest() { try { if (!NetworkUtils.isNetworkAvailable(mActivity)) { UiUtils.showToast(mActivity, "Please check your network connection"); return -1; } DefaultHttpClient httpClient = new DefaultHttpClient(); File manifest = new File(getCacheDir(), MANIFEST); boolean fetch = true; if (manifest.exists()) { Date now = new Date(); long age = now.getTime() - manifest.lastModified(); if (age < 10 * DateUtils.MINUTE_IN_MILLIS) { fetch = false; } } if (fetch) { NetworkUtils.doHttpGet(mActivity, httpClient, HOST, PORT, PATH + "/" + MANIFEST, "uuid=" + UUID.randomUUID().toString(), manifest); } } catch (Exception e) { UiUtils.showToast(mActivity, e.getMessage()); return -1; } return 0; } private int parseManifest() { FileInputStream in; try { File manifest = new File(getCacheDir(), MANIFEST); in = new FileInputStream(manifest); final DataInfo info = new DataInfo(); RootElement root = new RootElement("manifest"); Element datafile = root.getChild("datafile"); datafile.setEndElementListener(new EndElementListener() { @Override public void end() { mAvailableData.add(new DataInfo(info)); } }); datafile.getChild("type").setEndTextElementListener(new EndTextElementListener() { @Override public void end(String body) { info.type = body; } }); datafile.getChild("desc").setEndTextElementListener(new EndTextElementListener() { @Override public void end(String body) { info.desc = body; } }); datafile.getChild("version").setEndTextElementListener(new EndTextElementListener() { @Override public void end(String body) { info.version = Integer.parseInt(body); } }); datafile.getChild("filename").setEndTextElementListener(new EndTextElementListener() { @Override public void end(String body) { info.fileName = body; } }); datafile.getChild("size").setEndTextElementListener(new EndTextElementListener() { @Override public void end(String body) { info.size = Integer.parseInt(body); } }); datafile.getChild("start").setEndTextElementListener(new EndTextElementListener() { @Override public void end(String body) { info.start = new Time(); info.start.parse3339(body); info.start.normalize(false); } }); datafile.getChild("end").setEndTextElementListener(new EndTextElementListener() { @Override public void end(String body) { info.end = new Time(); info.end.parse3339(body); info.end.normalize(false); } }); Xml.parse(in, Xml.Encoding.UTF_8, root.getContentHandler()); Collections.sort(mAvailableData); in.close(); } catch (Exception e) { UiUtils.showToast(mActivity, e.getMessage()); return -1; } return 0; } private void processManifest() { Time now = new Time(); now.setToNow(); Iterator<DataInfo> it = mAvailableData.iterator(); while (it.hasNext()) { DataInfo available = it.next(); if (now.after(available.end)) { // Expired Log.i(TAG, "Removing expired " + available.type + ":" + available.version); it.remove(); continue; } if (isInstalled(available)) { // Already installed Log.i(TAG, "Removing installed " + available.type + ":" + available.version); it.remove(); continue; } } } private boolean isInstalled(DataInfo available) { Iterator<DataInfo> it = mInstalledData.iterator(); while (it.hasNext()) { DataInfo installed = it.next(); if (available.equals(installed)) { return true; } } return false; } } protected void updateDownloadList() { Cursor c = createCursor(); DownloadListAdapter adapter = new DownloadListAdapter(this, c); mListView.setAdapter(adapter); setContentShown(true); if (c.getCount() == 0) { TextView empty = (TextView) findViewById(android.R.id.empty); empty.setText(R.string.download_error); return; } Button btnDelete = (Button) findViewById(R.id.btnDelete); if (!mInstalledData.isEmpty()) { btnDelete.setVisibility(View.VISIBLE); btnDelete.setEnabled(true); } else { btnDelete.setVisibility(View.GONE); } Button btnDownload = (Button) findViewById(R.id.btnDownload); if (!mAvailableData.isEmpty()) { btnDownload.setVisibility(View.VISIBLE); btnDownload.setEnabled(true); } else { btnDownload.setVisibility(View.GONE); } } private final class DownloadTask extends AsyncTask<Void, Integer, Integer> { private DownloadActivity mActivity; private ProgressTracker mTracker; public DownloadTask(DownloadActivity activity) { mActivity = activity; } @Override protected void onPreExecute() { } @Override protected Integer doInBackground(Void... params) { Iterator<DataInfo> it = mAvailableData.iterator(); if (it.hasNext()) { final DataInfo data = it.next(); mTracker = getTrackerForType(data.type); int result = downloadData(data); if (result < 0) { return result; } result = updateCatalog(data); if (result < 0) { return result; } // Update the displayed list to reflect the installed data mInstalledData.add(data); mAvailableData.remove(data); } else { // No more downloads left, cleanup any expired data cleanupExpiredData(); UiUtils.showToast(mActivity, "Data installation completed successfully"); mDbManager.closeDatabases(); mDbManager.openDatabases(); return 1; } return 0; } @Override protected void onProgressUpdate(Integer... progress) { mTracker.setProgress(progress[0]); } @Override protected void onPostExecute(Integer result) { if (mTracker != null) { mTracker.hideProgress(); } // Update the displayed list to reflect the recently installed data updateDownloadList(); if (result == 0) { // Start the download of the next data file mActivity.download(); } } protected ProgressTracker getTrackerForType(String type) { return mTrackers.get(type); } protected int downloadData(final DataInfo data) { mHandler.post(new Runnable() { @Override public void run() { mTracker.initProgress(R.string.installing, data.size); } }); try { DefaultHttpClient httpClient = new DefaultHttpClient(); if (!DatabaseManager.DATABASE_DIR.exists() && !DatabaseManager.DATABASE_DIR.mkdirs()) { UiUtils.showToast(mActivity, "Unable to create folder on external storage"); return -3; } File dbFile = new File(DatabaseManager.DATABASE_DIR, data.fileName); ResultReceiver receiver = new ResultReceiver(mHandler) { protected void onReceiveResult(int resultCode, Bundle resultData) { long progress = resultData.getLong(NetworkUtils.CONTENT_PROGRESS); publishProgress((int) progress); } }; Bundle result = new Bundle(); NetworkUtils.doHttpGet(mActivity, httpClient, HOST, PORT, PATH + "/" + data.fileName + ".gz", "uuid=" + UUID.randomUUID().toString(), dbFile, receiver, result, GZIPInputStream.class); } catch (Exception e) { UiUtils.showToast(mActivity, e.getMessage()); return -1; } return 0; } protected int updateCatalog(DataInfo data) { Time now = new Time(); now.set(System.currentTimeMillis()); ContentValues values = new ContentValues(); values.put(Catalog.TYPE, data.type); values.put(Catalog.DESCRIPTION, data.desc); values.put(Catalog.VERSION, data.version); values.put(Catalog.START_DATE, data.start.format3339(false)); values.put(Catalog.END_DATE, data.end.format3339(false)); values.put(Catalog.DB_NAME, data.fileName); values.put(Catalog.INSTALL_DATE, now.format3339(false)); Log.i(TAG, "Inserting catalog: type=" + data.type + ", version=" + data.version + ", db=" + data.fileName); int rc = mDbManager.insertCatalogEntry(values); if (rc < 0) { UiUtils.showToast(mActivity, "Failed to update catalog database"); } return rc; } } private void checkDelete() { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setMessage("Are you sure you want to delete all installed data?") .setPositiveButton("Yes", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { DeleteDataTask deleteTask = new DeleteDataTask(); deleteTask.execute((Void) null); } }).setNegativeButton("No", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { } }); AlertDialog alert = builder.create(); alert.show(); return; } private final class DeleteDataTask extends AsyncTask<Void, Void, Integer> { private ProgressDialog mProgressDialog; @Override protected void onPreExecute() { mProgressDialog = ProgressDialog.show(DownloadActivity.this, "", "Deleting installed data. Please wait...", true); } @Override protected Integer doInBackground(Void... params) { int result = 0; // Make sure all the databases we want to delete are closed mDbManager.closeDatabases(); // Get all the catalog entries SQLiteDatabase catalogDb = mDbManager.getCatalogDb(); Cursor cursor = catalogDb.query(Catalog.TABLE_NAME, null, null, null, null, null, null); if (cursor.moveToFirst()) { do { int _id = cursor.getInt(cursor.getColumnIndex(Catalog._ID)); String dbName = cursor.getString(cursor.getColumnIndex(Catalog.DB_NAME)); // Delete the db file on the external device File file = new File(DatabaseManager.DATABASE_DIR, dbName); // Now delete the catalog entry for the file int rows = catalogDb.delete(Catalog.TABLE_NAME, "_id=?", new String[] { Integer.toString(_id) }); if (rows != 1) { // If we could not delete the row, remember the error result = -1; } if (file.exists()) { file.delete(); } } while (cursor.moveToNext()); } cursor.close(); return result; } @Override protected void onPostExecute(Integer result) { mProgressDialog.dismiss(); if (result != 0) { // Some or all data files were not deleted Toast.makeText(getApplicationContext(), "There was an error while deleting installed data", Toast.LENGTH_LONG).show(); } // Refresh the download list view checkData(false); } } protected void cleanupExpiredData() { SQLiteDatabase catalogDb = mDbManager.getCatalogDb(); Time now = new Time(); now.setToNow(); String today = now.format3339(true); Cursor c = mDbManager.getAllFromCatalog(); if (c.moveToFirst()) { do { // Check and delete all the expired databases String end = c.getString(c.getColumnIndex(Catalog.END_DATE)); if (end.compareTo(today) < 0) { // This database has expired, remove it Integer _id = c.getInt(c.getColumnIndex(Catalog._ID)); String dbName = c.getString(c.getColumnIndex(Catalog.DB_NAME)); File file = new File(DatabaseManager.DATABASE_DIR, dbName); if (catalogDb.isOpen() && file.delete()) { // Now delete the catalog entry for the file catalogDb.delete(Catalog.TABLE_NAME, Catalog._ID + "=?", new String[] { Integer.toString(_id) }); } } } while (c.moveToNext()); } c.close(); } @Override public boolean onPrepareOptionsMenu(Menu menu) { menu.findItem(R.id.menu_download).setEnabled(false); Cursor c = mDbManager.getCurrentFromCatalog(); boolean enabled = c.moveToFirst(); c.close(); menu.findItem(R.id.menu_search).setEnabled(enabled); return super.onPrepareOptionsMenu(menu); } }