/* Copyright (C) 2012, Dirk Trossen, airs@dirk-trossen.de Copyright (C) 2014, TecVis LP, support@tecvis.co.uk This program is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation as version 2.1 of the License. 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package com.airs.database; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Locale; import com.airs.R; import com.airs.platform.HandlerUIManager; import android.app.Activity; import android.app.AlertDialog; import android.app.DatePickerDialog; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.preference.PreferenceManager; import android.text.format.Time; import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.DatePicker; import android.widget.ImageButton; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import android.content.*; import android.content.SharedPreferences.Editor; import android.content.res.Configuration; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; /** * Activity to sync the AIRS database * */ public class AIRS_sync extends Activity implements OnClickListener { // states for handler private static final int START_ACTIVITY = 1; private static final int UPDATE_VALUES = 2; private static final int FINISH_NO_VALUES_ACTIVITY = 3; private static final int NO_STORAGE = 4; private static final int NO_SYNC = 0; private static final int SYNC_STARTED = 1; private static final int SYNC_CANCELLED = 2; private static final int SYNC_FINISHED = 2; // current batch of recordings for sync private static final int SYNC_BATCH = 5000; // preferences private SharedPreferences settings; private Editor editor; // other variables private TextView ProgressText; private TextView syncText; private ImageButton startSync; private Button cancelButton; private ProgressBar progressbar; private long synctime; private int read_data_entries = 0; private File external_storage; private Uri share_file; private File sync_file; private File fconn; // public for sharing file when exiting private BufferedOutputStream os = null; private boolean at_least_once_written = false; private long currenttime, currentstart = 0; private WakeLock wl; private int syncing = NO_SYNC; private Context context; // database variables private AIRS_database database_helper; private SQLiteDatabase airs_storage; /** Called when the activity is first created. * @param savedInstanceState a Bundle of the saved state, according to Android lifecycle model */ @Override public void onCreate(Bundle savedInstanceState) { // Set up the window layout super.onCreate(savedInstanceState); // save for later this.context = this.getApplicationContext(); // get default preferences settings = PreferenceManager.getDefaultSharedPreferences(this); editor = settings.edit(); // now open database database_helper = new AIRS_database(this.getApplicationContext()); airs_storage = database_helper.getReadableDatabase(); setContentView(R.layout.sync_dialog); // get progress text view ProgressText = (TextView) findViewById(R.id.sync_progresstext); ProgressText.setVisibility(View.INVISIBLE); // get cancel button cancelButton = (Button)findViewById(R.id.sync_cancel); cancelButton.setOnClickListener(this); cancelButton.setVisibility(View.INVISIBLE); // get start button startSync = (ImageButton)findViewById(R.id.sync_start); startSync.setOnClickListener(this); // hide progress bar first progressbar = (ProgressBar)findViewById(R.id.sync_progress); progressbar.setVisibility(View.INVISIBLE); // get sync timestamp text view syncText = (TextView) findViewById(R.id.sync_text); syncing = NO_SYNC; // create new wakelock PowerManager pm = (PowerManager)this.getSystemService(Context.POWER_SERVICE); wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "AIRS Sync Lock"); } /** Called when the activity is resumed. */ @Override public synchronized void onResume() { super.onPause(); // get timestamp of last sync synctime = settings.getLong("SyncTimestamp", 0); // set sync text view Time timeStamp = new Time(); timeStamp.set(synctime); syncText.setText(getString(R.string.Last_sync) + " " + timeStamp.format("%H:%M:%S on %d.%m.%Y")); // acquire wakelock again if (wl != null) wl.acquire(); } /** Called when the activity is paused. */ @Override public synchronized void onPause() { super.onPause(); // release wake lock if held if (wl != null) wl.release(); } /** Called when the activity is stopped. */ @Override public void onStop() { super.onStop(); } /** Called when the activity is destroyed. */ @Override public void onDestroy() { super.onDestroy(); } /** Called when the configuration of the activity has changed. * @param newConfig new configuration after change */ @Override public void onConfigurationChanged(Configuration newConfig) { //ignore orientation change super.onConfigurationChanged(newConfig); } /** Called when the Options menu is opened * @param menu Reference to the {@link android.view.Menu} */ @Override public boolean onPrepareOptionsMenu(Menu menu) { MenuInflater inflater; menu.clear(); inflater = getMenuInflater(); inflater.inflate(R.menu.options_sync, menu); return true; } /** Called when an option menu item has been selected by the user * @param item Reference to the {@link android.view.MenuItem} clicked on */ @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.sync_about: HandlerUIManager.AboutDialog(getString(R.string.main_Sync), getString(R.string.SyncAbout)); return true; case R.id.sync_setdate: // if regular uploads are selected, do not allow for changing sync date! if (Integer.valueOf(settings.getString("UploadFrequency", "0")) != 0) Toast.makeText(getApplicationContext(), getString(R.string.Regular_sync), Toast.LENGTH_LONG).show(); else { // now get calendar data Calendar cal = Calendar.getInstance(Locale.getDefault()); cal.setTimeInMillis(synctime); int month = cal.get(Calendar.MONTH); int year = cal.get(Calendar.YEAR); int day = cal.get(Calendar.DAY_OF_MONTH); DatePickerDialog dialog = new DatePickerDialog(this, new DatePickerDialog.OnDateSetListener() { @Override public void onDateSet(DatePicker datePicker, int year, int month, int day) { // now form synctime from day/month/year selection Calendar cal = Calendar.getInstance(Locale.getDefault()); cal.set(Calendar.YEAR, year); cal.set(Calendar.MONTH, month); cal.set(Calendar.DAY_OF_MONTH, day); cal.set(Calendar.HOUR, 0); cal.set(Calendar.MINUTE, 0); cal.set(Calendar.SECOND, 0); cal.set(Calendar.MILLISECOND, 1); cal.set(Calendar.AM_PM, Calendar.AM); synctime = cal.getTimeInMillis(); // set sync text view Time timeStamp = new Time(); timeStamp.set(synctime); syncText.setText(getString(R.string.Last_sync) + " " + timeStamp.format("%H:%M:%S on %d.%m.%Y")); // also place in preferences! editor.putLong("SyncTimestamp", synctime); // finally commit to storing values!! editor.commit(); // set timer again AIRS_upload.setTimer(context); } }, year, month, day); dialog.setTitle(getString(R.string.Set_sync_date)); dialog.setMessage(getString(R.string.Set_sync_date2)); dialog.show(); } return true; } return true; } /** Called when a button has been clicked on by the user * @param v Reference to the {@link android.view.View} of the button */ public void onClick(View v) { // if regular uploads are selected, do not allow for manual sync if (Integer.valueOf(settings.getString("UploadFrequency", "0")) != 0) { Toast.makeText(getApplicationContext(), getString(R.string.Regular_sync), Toast.LENGTH_LONG).show(); return; } if (v.getId() == R.id.sync_start && syncing == NO_SYNC) { ProgressText.setText(getString(R.string.Start_synchronising)); ProgressText.setVisibility(View.VISIBLE); progressbar.setVisibility(View.VISIBLE); cancelButton.setVisibility(View.VISIBLE); // signal that syncing has started syncing = SYNC_STARTED; // start sync thread new SyncThread(); } if (v.getId() == R.id.sync_cancel && syncing == SYNC_STARTED) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setMessage(getString(R.string.Interrupt_synchronising)) .setTitle(getString(R.string.main_Sync)) .setCancelable(false) .setPositiveButton(getString(R.string.Yes), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { syncing = SYNC_CANCELLED; ProgressText.setVisibility(View.INVISIBLE); progressbar.setVisibility(View.INVISIBLE); cancelButton.setVisibility(View.INVISIBLE); dialog.dismiss(); } }) .setNegativeButton(getString(R.string.No), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.cancel(); } }); AlertDialog alert = builder.create(); alert.show(); } } // The Handler that gets information back from the other threads, updating the values for the UI private final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { Intent chosen; switch (msg.what) { case START_ACTIVITY: if (syncing == SYNC_FINISHED) { // prepare intent for choosing sharing Intent intent = new Intent(Intent.ACTION_SEND); intent.setType("text/plain"); chosen = Intent.createChooser(intent,getString(R.string.Send_local_recordings)); // anything chosen? if (chosen!=null) { intent.putExtra(Intent.EXTRA_STREAM, share_file); startActivity(chosen); // write current timestamp for later syncs // put sync timestamp into store synctime = System.currentTimeMillis(); editor.putLong("SyncTimestamp", synctime); // finally commit to storing values!! editor.commit(); // set timer again AIRS_upload.setTimer(context); // remove temp files sync_file.delete(); // now finish activity ProgressText.setVisibility(View.INVISIBLE); progressbar.setVisibility(View.INVISIBLE); cancelButton.setVisibility(View.INVISIBLE); // set sync text view Time timeStamp = new Time(); timeStamp.set(synctime); syncText.setText(getString(R.string.Last_sync) + " " + timeStamp.format("%H:%M:%S on %d.%m.%Y")); // reset sync state syncing = NO_SYNC; } else { // now finish activity ProgressText.setVisibility(View.INVISIBLE); progressbar.setVisibility(View.INVISIBLE); cancelButton.setVisibility(View.INVISIBLE); // reset sync state syncing = NO_SYNC; } } break; case FINISH_NO_VALUES_ACTIVITY: Toast.makeText(getApplicationContext(), getString(R.string.No_values_to_synchronise), Toast.LENGTH_LONG).show(); // now finish activity ProgressText.setVisibility(View.INVISIBLE); progressbar.setVisibility(View.INVISIBLE); cancelButton.setVisibility(View.INVISIBLE); // reset sync state syncing = NO_SYNC; break; case UPDATE_VALUES: ProgressText.setText(getString(R.string.Temp_sync_file) + " " + String.valueOf(msg.getData().getLong("Value")/1000)); break; case NO_STORAGE: Toast.makeText(getApplicationContext(), getString(R.string.Cannot_find_storage), Toast.LENGTH_LONG).show(); // now finish activity ProgressText.setVisibility(View.INVISIBLE); progressbar.setVisibility(View.INVISIBLE); cancelButton.setVisibility(View.INVISIBLE); // reset sync state syncing = NO_SYNC; break; default: break; } } }; private class SyncThread implements Runnable { SyncThread() { new Thread(this).start(); } public void run() { int i; // path for templates external_storage = getExternalFilesDir(null); if (external_storage == null) { syncing = SYNC_FINISHED; mHandler.sendMessage(mHandler.obtainMessage(NO_STORAGE)); return; } sync_file = new File(external_storage, "AIRS_temp"); // get files in directory String [] file_list = sync_file.list(null); // remove files in AIRS_temp directory if (file_list != null) for (i=0;i<file_list.length;i++) { File remove = new File(sync_file, file_list[i]); remove.delete(); } SyncValues(); SyncNotes(); } private void SyncValues() { String query; int t_column, s_column, v_column; Cursor values; String value, symbol; String line_to_write; byte[] writebyte; int number_values; int i; long currentmilli; Calendar cal = Calendar.getInstance(); boolean set_timestamp = true; // use handler to start activity Message finish2_msg = mHandler.obtainMessage(FINISH_NO_VALUES_ACTIVITY); read_data_entries = 0; try { // use current milliseconds for filename currentmilli = System.currentTimeMillis(); // open file in public directory sync_file = new File(external_storage, "AIRS_temp"); // make sure that path exists sync_file.mkdirs(); // open file and create, if necessary fconn = new File(sync_file, String.valueOf(currentmilli) + ".txt"); os = new BufferedOutputStream(new FileOutputStream(fconn, true)); // build URI for sharing share_file = Uri.fromFile(fconn); // set timestamp when we will have found the first timestamp set_timestamp = true; currenttime = synctime; while (syncing == SYNC_STARTED) { // query = new String("SELECT Timestamp, Symbol, Value from 'airs_values' WHERE Timestamp > " + String.valueOf(currenttime) + " ORDER BY Timestamp ASC LIMIT " + String.valueOf(SYNC_BATCH)); query = new String("SELECT Timestamp, Symbol, Value from 'airs_values' WHERE Timestamp > " + String.valueOf(currenttime) + " LIMIT " + String.valueOf(SYNC_BATCH)); values = airs_storage.rawQuery(query, null); // garbage collect query = null; if (values == null) { // signal end of synchronization syncing = SYNC_FINISHED; // nothing to write? if (at_least_once_written == false) { // purge file os.close(); mHandler.sendMessage(finish2_msg); } return; } // get number of rows number_values = values.getCount(); // if nothing is read (anymore) if (number_values == 0) { // signal end of synchronization syncing = SYNC_FINISHED; // nothing to write? if (at_least_once_written == false) { // purge file os.close(); mHandler.sendMessage(finish2_msg); } return; } // get column index for timestamp and value t_column = values.getColumnIndex("Timestamp"); s_column = values.getColumnIndex("Symbol"); v_column = values.getColumnIndex("Value"); if (t_column == -1 || v_column == -1 || s_column == -1) { // signal end of synchronization syncing = SYNC_FINISHED; // nothing to write? if (at_least_once_written == false) { // purge file os.close(); mHandler.sendMessage(finish2_msg); } return; } // move to first row to start values.moveToFirst(); // read DB values into arrays for (i=0;i<number_values;i++) { // get timestamp currenttime = values.getLong(t_column); if (set_timestamp == true) { // set cal.setTimeInMillis(currenttime); // store timestamp // String time = new String(cal.getTime().toString() + "\n"); // force a date format to address Android 4.3 changes that changed zzz to 'BST' and similar DateFormat sdf = new SimpleDateFormat ("EEE MMM dd HH:mm:ss ZZZZ yyyy", Locale.getDefault()); String time = new String(sdf.format(cal.getTime()) + "\n"); os.write(time.getBytes(), 0, time.length()); // save for later currentstart = currenttime; // don't set timestamp anymore later set_timestamp = false; at_least_once_written = true; } // get symbol symbol = values.getString(s_column); // get value value = values.getString(v_column); // add empty string as space if (value.compareTo("") == 0) value = " "; // create line to write to file line_to_write = new String("#" + String.valueOf(currenttime-currentstart) + ";" + symbol + ";" + value + "\n"); // now write to file writebyte = line_to_write.getBytes(); os.write(writebyte, 0, writebyte.length); // garbage collect the output data writebyte = null; line_to_write = null; // now move to next row values.moveToNext(); } // increase counter read_data_entries = (int)fconn.length(); Bundle bundle = new Bundle(); bundle.putLong("Value", read_data_entries); Message update_msg = mHandler.obtainMessage(UPDATE_VALUES); update_msg.setData(bundle); mHandler.sendMessage(update_msg); // close values to free up memory values.close(); } // close output file os.close(); } catch(Exception e) { try { if (os != null) os.close(); } catch(Exception ex) { } // signal end of synchronization } // nothing to write? if (at_least_once_written == false) mHandler.sendMessage(finish2_msg); } private void SyncNotes() { String query; int y_column, m_column, d_column, a_column, c_column, mo_column; Cursor values; String value, symbol; String line_to_write; byte[] writebyte; int number_values; int i; // use handler to start activity Message start_msg = mHandler.obtainMessage(START_ACTIVITY); Message finish2_msg = mHandler.obtainMessage(FINISH_NO_VALUES_ACTIVITY); // path for templates File external_storage = getExternalFilesDir(null); if (external_storage == null) { syncing = SYNC_FINISHED; mHandler.sendMessage(mHandler.obtainMessage(NO_STORAGE)); return; } try { currenttime = synctime; // now sync the notes, if any syncing = SYNC_STARTED; while (syncing == SYNC_STARTED) { query = new String("SELECT Year, Month, Day, Annotation, created, modified from 'airs_annotations' WHERE created > " + String.valueOf(currenttime) + " LIMIT " + String.valueOf(SYNC_BATCH)); values = airs_storage.rawQuery(query, null); // garbage collect query = null; if (values == null) { // purge file os.close(); // signal end of synchronization syncing = SYNC_FINISHED; if (at_least_once_written == true) // use handler to start activity mHandler.sendMessage(start_msg); else mHandler.sendMessage(finish2_msg); return; } // get number of rows number_values = values.getCount(); // if nothing is read (anymore) if (number_values == 0) { // purge file os.close(); // signal end of synchronization syncing = SYNC_FINISHED; if (at_least_once_written == true) // use handler to start activity mHandler.sendMessage(start_msg); else mHandler.sendMessage(finish2_msg); return; } // get column index for timestamp and value y_column = values.getColumnIndex("Year"); m_column = values.getColumnIndex("Month"); d_column = values.getColumnIndex("Day"); a_column = values.getColumnIndex("Annotation"); c_column = values.getColumnIndex("created"); mo_column = values.getColumnIndex("modified"); if (y_column == -1 || m_column == -1 || d_column == -1 || a_column == -1 || c_column == -1 || mo_column == -1) { // purge file os.close(); // signal end of synchronization syncing = SYNC_FINISHED; if (at_least_once_written == true) // use handler to start activity mHandler.sendMessage(start_msg); else mHandler.sendMessage(finish2_msg); return; } Log.e("AIRS", "...reading next batch!"); // move to first row to start values.moveToFirst(); // read DB values into arrays for (i=0;i<number_values;i++) { // get timestamp currenttime = values.getLong(c_column); // set symbol symbol = "UN"; // create value as concatenation of year:month:day:modified:annotation value = String.valueOf(values.getInt(y_column)) + ":" + String.valueOf(values.getInt(m_column)) + ":" + String.valueOf(values.getInt(d_column)) + ":" + String.valueOf(values.getLong(mo_column)) + ":" + values.getString(a_column); // create line to write to file line_to_write = new String("#" + String.valueOf(currenttime-currentstart) + ";" + symbol + ";" + value + "\n"); // now write to file writebyte = line_to_write.getBytes(); os.write(writebyte, 0, writebyte.length); // garbage collect the output data writebyte = null; line_to_write = null; // now move to next row values.moveToNext(); } // close values to free up memory values.close(); } // close output file os.close(); } catch(Exception e) { try { if (os != null) os.close(); } catch(Exception ex) { } // signal end of synchronization } if (at_least_once_written == true) // use handler to start activity mHandler.sendMessage(start_msg); else mHandler.sendMessage(finish2_msg); } } }