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.Arrays; import java.util.Calendar; import java.util.List; import java.util.Locale; import android.app.Notification; import android.app.NotificationManager; import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.os.Binder; import android.os.IBinder; import android.preference.PreferenceManager; import android.util.Log; import com.airs.R; import com.airs.helper.Waker; import com.google.api.client.extensions.android.http.AndroidHttp; import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential; import com.google.api.client.googleapis.media.MediaHttpUploader; import com.google.api.client.googleapis.media.MediaHttpUploaderProgressListener; import com.google.api.client.http.FileContent; import com.google.api.client.json.gson.GsonFactory; import com.google.api.services.drive.Drive; import com.google.api.services.drive.DriveScopes; import com.google.api.services.drive.model.ParentReference; public class AIRS_upload_service extends Service implements MediaHttpUploaderProgressListener { // current batch of recordings for sync private static final int SYNC_BATCH = 5000; private final IBinder mBinder = new LocalBinder(); private SharedPreferences settings; private Editor editor; private NotificationManager mNotificationManager; private ConnectivityManager cm; private Notification notification; private long synctime, currenttime, new_synctime, currentstart = 0; private File sync_file; private Uri share_file; private File fconn; // public for sharing file when exiting private BufferedOutputStream os = null; private boolean at_least_once_written = false; private SQLiteDatabase airs_storage; private AIRS_database database_helper; private GoogleAccountCredential credential; private Drive service; private Context context; private AIRS_upload_service this_service; private boolean wifi_only; private String currentFilename; // GDrive folder private String GDrive_Folder; public class LocalBinder extends Binder { AIRS_upload_service getService() { return AIRS_upload_service.this; } } /** * Returns current instance of AIRS_upload_service Service to anybody binding to it * @param intent Reference to calling {@link android.content.Intent} * @return current instance to service */ @Override public IBinder onBind(Intent intent) { return mBinder; } /** * Called when system is running low on memory * @see android.app.Service */ @Override public void onLowMemory() { } /** * Called when starting the service the first time around * @see android.app.Service */ @Override public void onCreate() { Log.v("AIRS", "Started upload service"); // save for later context = this.getApplicationContext(); this_service = this; // get notification manager for later mNotificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE); // now get connectivity manager for net type check cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); // get default preferences and editor settings = PreferenceManager.getDefaultSharedPreferences(context); editor = settings.edit(); // get settings for upload preference wifi_only = settings.getBoolean("UploadWifi", true); // get handle to Google Drive service = getDriveService(context); // get Google drive folder GDrive_Folder = settings.getString("GDriveFolder", "AIRS"); if (service != null) { try { // get database database_helper = new AIRS_database(context); airs_storage = database_helper.getWritableDatabase(); // start sync thread new SyncThread(); } catch(Exception e) { Log.e("AIRS", "Cannot open AIRS database for upload!"); AIRS_upload.setTimer(context); // timer will fire again soon! } } } /** * Called when service is destroyed, e.g., by stopService() * Here, we tear down all recording threads, close all handlers, unregister receivers for battery signal and close the thread for indicating the recording */ @Override public void onDestroy() { Log.v("AIRS", "...destroyed upload service"); } private class SyncThread implements Runnable { SyncThread() { new Thread(this).start(); } public void run() { com.google.api.services.drive.model.File AIRS_dir = null; com.google.api.services.drive.model.File body; com.google.api.services.drive.model.File file; java.io.File fileContent; FileContent mediaContent; Drive.Files.Insert insert; MediaHttpUploader uploader; boolean right_network = true, try_upload = true; // now create the syncfile if (createSyncFile(context) == true) { // try to upload until right network is available while(try_upload == true) { try { right_network = checkRightNetwork(); // only if right network is available, try to upload if (right_network == true) { Log.v("AIRS", "trying to find AIRS recordings directory"); List<com.google.api.services.drive.model.File> files = service.files().list().setQ("mimeType = 'application/vnd.google-apps.folder' AND trashed=false AND 'root' in parents").execute().getItems(); for (com.google.api.services.drive.model.File f : files) { if (f.getTitle().compareTo(GDrive_Folder) == 0) AIRS_dir = f; } if (AIRS_dir == null) { Log.v("AIRS", "...need to create AIRS recordings directory"); // create AIRS recordings directory body = new com.google.api.services.drive.model.File(); body.setTitle(GDrive_Folder); body.setMimeType("application/vnd.google-apps.folder"); AIRS_dir = service.files().insert(body).execute(); } // File's binary content fileContent = new java.io.File(share_file.getPath()); mediaContent = new FileContent("text/plain", fileContent); // File's metadata body = new com.google.api.services.drive.model.File(); body.setTitle(fileContent.getName()); body.setMimeType("text/plain"); body.setParents(Arrays.asList(new ParentReference().setId(AIRS_dir.getId()))); Log.v("AIRS", "...trying to upload AIRS recordings"); // now get the uploader handle and set resumable upload insert = service.files().insert(body, mediaContent); uploader = insert.getMediaHttpUploader(); uploader.setDirectUploadEnabled(false); uploader.setChunkSize(MediaHttpUploader.DEFAULT_CHUNK_SIZE); uploader.setProgressListener(this_service); Log.v("AIRS", "...executing upload AIRS recordings"); do { right_network = checkRightNetwork(); // only if right network is available, try to upload if (right_network == true) { // now execute the upload file = insert.execute(); if (file != null) { Log.v("AIRS", "...writing new sync timestamp"); // write the time until read for later syncs // put sync timestamp into store editor.putLong("SyncTimestamp", new_synctime); // finally commit to storing values!! editor.commit(); // remove temp files sync_file.delete(); // now finish this loop since we are done! try_upload = false; } } else { Log.v("AIRS", "...sleeping until right network becomes available"); Waker.sleep(15000); } }while(right_network == false); } else { Log.v("AIRS", "...sleeping until right network becomes available"); Waker.sleep(15000); } } catch (Exception e) { Log.e("AIRS", "something went wrong in uploading the sync data: " + e.toString()); } } } else { Log.v("AIRS", "...nothing to sync, it seems"); Log.v("AIRS", "...writing new sync timestamp"); // write the time until read for later syncs // put sync timestamp into store editor.putLong("SyncTimestamp", new_synctime); // finally commit to storing values!! editor.commit(); } // set timer again AIRS_upload.setTimer(context); // now stop the overall service all together! stopSelf(); } } private boolean checkRightNetwork() { boolean right_network = true; NetworkInfo netInfo; // check network connectivity netInfo = cm.getActiveNetworkInfo(); // any network available? if (netInfo != null) { // is it the right network (in case wifi only is enabled)? if (netInfo.getType() != ConnectivityManager.TYPE_WIFI && wifi_only == true) right_network = false; } else right_network = false; // no network available anyways return right_network; } private Drive getDriveService(Context context) { String accountName = settings.getString("AIRS_local::accountname", ""); Log.v("AIRS", "account: " + accountName); credential = GoogleAccountCredential.usingOAuth2(context, Arrays.asList(DriveScopes.DRIVE)); credential.setSelectedAccountName(accountName); return new Drive.Builder(AndroidHttp.newCompatibleTransport(), new GsonFactory(), credential) .build(); } public void progressChanged(MediaHttpUploader uploader) { long vibration[] = {0,200,0}; int progress; switch (uploader.getUploadState()) { case MEDIA_IN_PROGRESS: notification = new Notification(R.drawable.notification_icon, context.getString(R.string.Sync_uploading), System.currentTimeMillis()); try { progress = (int)(uploader.getProgress() * 100.0f); } catch(Exception e) { progress = 0; } notification.setLatestEventInfo(context, context.getString(R.string.Sync_uploading), context.getString(R.string.Sync_progress) + String.valueOf(progress) + "%", null); // set the time again for ICS notification.when = System.currentTimeMillis(); // don't allow clearing the notification notification.flags = Notification.FLAG_AUTO_CANCEL | Notification.FLAG_ONLY_ALERT_ONCE; notification.vibrate = vibration; mNotificationManager.notify(9999, notification); break; case MEDIA_COMPLETE: notification = new Notification(R.drawable.notification_icon, context.getString(R.string.Sync_upload), System.currentTimeMillis()); // create pending intent for starting the activity notification.setLatestEventInfo(context, context.getString(R.string.Sync_upload), "", null); // set the time again for ICS notification.when = System.currentTimeMillis(); // don't allow clearing the notification notification.flags = Notification.FLAG_AUTO_CANCEL | Notification.FLAG_ONLY_ALERT_ONCE; notification.vibrate = vibration; mNotificationManager.notify(9999, notification); break; default: break; } } private boolean createSyncFile(Context context) { if (createValueFile(context) == true) return createNoteFile(context); return false; } private boolean createValueFile(Context context) { 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; boolean syncing = true; // get timestamp of last sync synctime = settings.getLong("SyncTimestamp", 0); currenttime = synctime; // sync until just about now! new_synctime = System.currentTimeMillis(); Log.v("AIRS", "start creating values sync file!"); // path for templates File external_storage = context.getExternalFilesDir(null); if (external_storage == null) return false; 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(); } 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 currentFilename = new String(String.valueOf(currentmilli) + ".txt"); fconn = new File(sync_file, currentFilename); 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; while (syncing == true) { query = new String("SELECT Timestamp, Symbol, Value from 'airs_values' WHERE Timestamp > " + String.valueOf(currenttime) + " AND TimeStamp < " + String.valueOf(new_synctime) + " LIMIT " + String.valueOf(SYNC_BATCH)); values = airs_storage.rawQuery(query, null); // garbage collect query = null; if (values == null) { if (at_least_once_written == true) return true; else { os.close(); return false; } } // get number of rows number_values = values.getCount(); // if nothing is read (anymore) if (number_values == 0) { if (at_least_once_written == true) return true; else { os.close(); return false; } } // 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) { if (at_least_once_written == true) return true; else { os.close(); return false; } } Log.v("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(t_column); if (set_timestamp == true) { // set cal.setTimeInMillis(currenttime); // store timestamp // 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(); } // close values to free up memory values.close(); } } catch(Exception e) { try { if (os != null) os.close(); } catch(Exception ex) { } // signal end of synchronization } // now return if (at_least_once_written == true) return true; else return false; } private boolean createNoteFile(Context context) { 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; boolean syncing = true; // get timestamp of last sync synctime = settings.getLong("SyncTimestamp", 0); currenttime = synctime; Log.v("AIRS", "start creating sync notes file!"); try { // now sync the notes, if any syncing = true; while (syncing == true) { query = new String("SELECT Year, Month, Day, Annotation, created, modified from 'airs_annotations' WHERE created > " + String.valueOf(currenttime) + " AND created < " + String.valueOf(new_synctime) + " LIMIT " + String.valueOf(SYNC_BATCH)); values = airs_storage.rawQuery(query, null); // garbage collect query = null; if (values == null) { // purge file os.close(); if (at_least_once_written == true) return true; else return false; } // get number of rows number_values = values.getCount(); // if nothing is read (anymore) if (number_values == 0) { // purge file os.close(); if (at_least_once_written == true) return true; else return false; } // 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(); if (at_least_once_written == true) return true; else return false; } Log.v("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 } // now return if (at_least_once_written == true) return true; else return false; } }