/* Android IMSI-Catcher Detector | (c) AIMSICD Privacy Project
* -----------------------------------------------------------
* LICENSE: http://git.io/vki47 | TERMS: http://git.io/vki4o
* -----------------------------------------------------------
*/
package com.secupwn.aimsicd.utils;
import android.content.Context;
import android.content.Intent;
import android.support.v4.app.Fragment;
import android.support.v4.content.LocalBroadcastManager;
import com.secupwn.aimsicd.BuildConfig;
import com.secupwn.aimsicd.R;
import com.secupwn.aimsicd.constants.DrawerMenu;
import com.secupwn.aimsicd.constants.TinyDbKeys;
import com.secupwn.aimsicd.service.CellTracker;
import com.secupwn.aimsicd.ui.fragments.MapFragment;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.MultipartBuilder;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.SocketTimeoutException;
import java.util.concurrent.TimeUnit;
import io.freefair.android.injection.annotation.Inject;
import io.freefair.android.injection.app.InjectionAppCompatActivity;
import io.freefair.android.util.logging.Logger;
import io.realm.Realm;
import lombok.Cleanup;
/**
*
* Description:
*
* This class is the request handler for Downloading and Uploading of BTS data and
* the backing up of the database. The download function currently requests a CVS file
* from OCID though an API query. The parsing of this CVS file is done in the
* "AIMSICDDbAdapter.java" adapter, which put the downloaded data into the
* {@link com.secupwn.aimsicd.data.model.Import Import} realm. This should be a read-only table, in the sense that no new
* BTS or info should be added there. The indexing there can be very tricky when
* later displayed in "DbViewerFragment.java", as they are different.
*
* Criticism:
*
* From the original look of it. It seem to first upload newly found BTSs to OCID,
* then it immediately attempts to download a new OCID data set, probably expecting
* to see the new BTS in that data. (If this description is correct?)
*
* This is NOT correct behaviour as:
*
* 1) OCID data takes at least a few minutes before updating their DBs with the
* newly uploaded CSV data file.
* 2) It doesn't make sense to re-download data that is already populated in the
* DBi_bts and and DBi_measure tables.
* 3) This is very bad because if there are fake BTS found by AIMSICD, then we're
* uploading them and thus making users of AIMSICD believe these are good cells.
* Basically we'd be corrupting the OCID data.
*
*
* Issues:
* [ ] There is no onPreExecute here...perhaps that's why the progress bar is not shown?
* see: http://developer.android.com/reference/android/os/AsyncTask.html
* To Fix:
*
* [ ] Explain why BACKUP/RESTORE_DATABASE is in here?
* [ ] Think about what "lookup cell info" (CELL_LOOKUP) should do
* [ ] App is blocked while downloading.
*
*/
public class RequestTask extends BaseAsyncTask<String, Integer, String> {
//Calling from the menu more extensive(more difficult for sever),
// we have to give more time for the server response
public static final int REQUEST_TIMEOUT_MAPS = 80000; // [ms] 80 s Calling from map
public static final int REQUEST_TIMEOUT_MENU = 80000; // [ms] 80 s Calling from menu
public static final char DBE_DOWNLOAD_REQUEST = 1; // OCID download request from "APPLICATION" drawer title
public static final char DBE_DOWNLOAD_REQUEST_FROM_MAP = 2; // OCID download request from "Antenna Map Viewer"
public static final char DBE_UPLOAD_REQUEST = 6; // OCID upload request from "APPLICATION" drawer title
public static final char CELL_LOOKUP = 5; // TODO: "All Current Cell Details (ALL_CURRENT_CELL_DETAILS)"
@Inject
private Logger log;
private RealmHelper mDbAdapter;
private Context mAppContext;
private char mType;
private int mTimeOut;
private AsyncTaskCompleteListener mListener;
@Inject
private OkHttpClient okHttpClient;
/**
*
* @param context App context
* @param type What type of request to be performed (download OCID, upload OCID, DB backup, etc.)
* @param listener Allows the caller of RequestTask to implement success/fail callbacks
*/
public RequestTask(InjectionAppCompatActivity context, char type, AsyncTaskCompleteListener listener) {
super(context);
this.mType = type;
this.mAppContext = context.getApplicationContext();
this.mDbAdapter = new RealmHelper(mAppContext);
this.mTimeOut = REQUEST_TIMEOUT_MAPS;
this.mListener = listener;
}
/**
* @deprecated Use {@link #RequestTask(InjectionAppCompatActivity, char, AsyncTaskCompleteListener)}
* instead because of the listener callback interface.
* @param context
* @param type
*/
@Deprecated
public RequestTask(InjectionAppCompatActivity context, char type) {
this(context, type, null);
log.warn("RequestTask(InjectionAppCompatActivity, char) is deprecated in favour of using listener callbacks");
}
@Override
protected String doInBackground(String... commandString) {
// We need to create a separate case for UPLOADING to DBe (OCID, MLS etc)
switch (mType) {
// OCID upload request from "APPLICATION" drawer title
case DBE_UPLOAD_REQUEST:
try {
@Cleanup Realm realm = Realm.getDefaultInstance();
boolean prepared = mDbAdapter.prepareOpenCellUploadData(realm);
log.info("OCID upload data prepared - " + String.valueOf(prepared));
if (prepared) {
File file = new File((mAppContext.getExternalFilesDir(null) + File.separator) + "OpenCellID/aimsicd-ocid-data.csv");
publishProgress(25, 100);
RequestBody requestBody = new MultipartBuilder()
.type(MultipartBuilder.FORM)
.addFormDataPart("key", CellTracker.OCID_API_KEY)
.addFormDataPart("datafile", "aimsicd-ocid-data.csv", RequestBody.create(MediaType.parse("text/csv"), file))
.build();
Request request = new Request.Builder()
.url("http://www.opencellid.org/measure/uploadCsv")
.post(requestBody)
.build();
publishProgress(60, 100);
Response response = okHttpClient.newCall(request).execute();
publishProgress(80, 100);
if (response != null) {
log.info("OCID Upload Response: "
+ response.code() + " - "
+ response.message());
if (response.code() == 200) {
Realm.Transaction transaction = mDbAdapter.ocidProcessed();
realm.executeTransaction(transaction);
}
publishProgress(95, 100);
}
return "Successful";
} else {
Helpers.msgLong(mAppContext, mAppContext.getString(R.string.no_data_for_publishing));
return null;
}
// all caused by httpclient.execute(httppost);
} catch (UnsupportedEncodingException e) {
log.error("Upload OpenCellID data Exception", e);
} catch (FileNotFoundException e) {
log.error("Upload OpenCellID data Exception", e);
} catch (IOException e) {
log.error("Upload OpenCellID data Exception", e);
} catch (Exception e) {
log.error("Upload OpenCellID data Exception", e);
}
// DOWNLOADING...
case DBE_DOWNLOAD_REQUEST: // OCID download request from "APPLICATION" drawer title
mTimeOut = REQUEST_TIMEOUT_MENU;
case DBE_DOWNLOAD_REQUEST_FROM_MAP: // OCID download request from "Antenna Map Viewer"
int count;
try {
long total;
int progress = 0;
String dirName = getOCDBDownloadDirectoryPath(mAppContext);
File dir = new File(dirName);
if (!dir.exists()) {
dir.mkdirs();
}
File file = new File(dir, OCDB_File_Name);
log.info("DBE_DOWNLOAD_REQUEST write to: " + dirName + OCDB_File_Name);
Request request = new Request.Builder()
.url(commandString[0])
.get()
.build();
Response response;
try {
// OCID's API can be slow. Give it up to a minute to do its job. Since this
// is a backgrounded task, it's ok to wait for a while.
okHttpClient.setReadTimeout(60, TimeUnit.SECONDS);
response = okHttpClient.newCall(request).execute();
okHttpClient.setReadTimeout(10, TimeUnit.SECONDS); // Restore back to default
} catch (SocketTimeoutException e) {
log.warn("Trying to talk to OCID timed out after 60 seconds. API is slammed? Throttled?");
return "Timeout";
}
if (response.code() != 200) {
try {
String error = response.body().string();
Helpers.msgLong(mAppContext, mAppContext.getString(R.string.download_error) + " " + error);
log.error("Download OCID data error: " + error);
} catch (Exception e) {
Helpers.msgLong(mAppContext, mAppContext.getString(R.string.download_error) + " "
+ e.getClass().getName() + " - "
+ e.getMessage());
log.error("Download OCID exception: ", e);
}
return "Error";
} else {
// This returns "-1" for streamed response (Chunked Transfer Encoding)
total = response.body().contentLength();
if (total == -1) {
log.debug("doInBackground DBE_DOWNLOAD_REQUEST total not returned!");
total = 1024; // Let's set it arbitrarily to something other than "-1"
} else {
log.debug("doInBackground DBE_DOWNLOAD_REQUEST total: " + total);
publishProgress((int) (0.25 * total), (int) total); // Let's show something!
}
FileOutputStream output = new FileOutputStream(file, false);
InputStream input = new BufferedInputStream(response.body().byteStream());
byte[] data = new byte[1024];
while ((count = input.read(data)) > 0) {
// writing data to file
output.write(data, 0, count);
progress += count;
publishProgress(progress, (int) total);
}
input.close();
// flushing output
output.flush();
output.close();
}
return "Successful";
} catch (IOException e) {
log.warn("Problem reading data from steam", e);
return null;
}
}
return null;
}
/**
* This is where we:
* <ol>
* <li>Check the success for OCID data download</li>
* <li>call the updateOpenCellID() to populate the {@link com.secupwn.aimsicd.data.model.Import Import} realm</li>
* <li>call the {@link RealmHelper#checkDBe()} to cleanup bad cells from imported data</li>
* <li>present a failure/success toast message</li>
* <li>set a shared preference to indicate that data has been downloaded:
* {@code ocid_downloaded true}</li>
* </ol>
*/
@Override
protected void onPostExecute(String result) {
super.onPostExecute(result);
TinyDB tinydb = TinyDB.getInstance();
@Cleanup Realm realm = Realm.getDefaultInstance();
switch (mType) {
case DBE_DOWNLOAD_REQUEST:
// if `result` is null, it will evaluate to false, no need to check for null
if ("Successful".equals(result)) {
if (mDbAdapter.populateDBeImport(realm)) {
Helpers.msgShort(mAppContext, mAppContext.getString(R.string.opencellid_data_successfully_received));
}
realm.executeTransaction(mDbAdapter.checkDBe());
tinydb.putBoolean("ocid_downloaded", true);
} else if ("Timeout".equals(result)) {
Helpers.msgLong(mAppContext, mAppContext.getString(R.string.download_timed_out));
} else {
Helpers.msgLong(mAppContext, mAppContext.getString(R.string.error_retrieving_opencellid_data));
}
break;
case DBE_DOWNLOAD_REQUEST_FROM_MAP:
if ("Successful".equals(result)) {
if (mDbAdapter.populateDBeImport(realm)) {
Intent intent = new Intent(MapFragment.updateOpenCellIDMarkers);
LocalBroadcastManager.getInstance(mAppContext).sendBroadcast(intent);
Helpers.msgShort(mAppContext, mAppContext.getString(R.string.opencellid_data_successfully_received_markers_updated));
realm.executeTransaction(mDbAdapter.checkDBe());
tinydb.putBoolean("ocid_downloaded", true);
}
} else if ("Timeout".equals(result)) {
Helpers.msgLong(mAppContext, mAppContext.getString(R.string.download_timed_out));
} else {
Helpers.msgLong(mAppContext, mAppContext.getString(R.string.error_retrieving_opencellid_data));
}
showHideMapProgressBar(false);
TinyDB.getInstance().putBoolean(TinyDbKeys.FINISHED_LOAD_IN_MAP, true);
break;
case DBE_UPLOAD_REQUEST:
if ("Successful".equals(result)) {
Helpers.msgShort(mAppContext, mAppContext.getString(R.string.uploaded_bts_data_successfully));
} else {
Helpers.msgLong(mAppContext, mAppContext.getString(R.string.error_uploading_bts_data));
}
break;
}
if (mListener != null) {
if ("Successful".equals(result)) {
mListener.onAsyncTaskSucceeded();
} else {
mListener.onAsyncTaskFailed(result);
}
}
}
@Override
protected void onActivityDetached() {
if (mType == DBE_DOWNLOAD_REQUEST_FROM_MAP) {
showHideMapProgressBar(false);
}
}
@Override
protected void onActivityAttached() {
if (mType == DBE_DOWNLOAD_REQUEST_FROM_MAP) {
showHideMapProgressBar(true);
}
}
@Override
protected void onCancelled() {
super.onCancelled();
if (mType == DBE_DOWNLOAD_REQUEST_FROM_MAP) {
showHideMapProgressBar(false);
}
}
private void showHideMapProgressBar(boolean pFlag) {
InjectionAppCompatActivity lActivity = getActivity();
if (BuildConfig.DEBUG && lActivity == null) {
log.verbose("BaseTask showHideMapProgressBar() activity is null");
}
if (lActivity != null) {
Fragment myFragment = lActivity.getSupportFragmentManager().findFragmentByTag(String.valueOf(DrawerMenu.ID.MAIN.ALL_CURRENT_CELL_DETAILS));
if (myFragment instanceof MapFragment) {
((MapFragment) myFragment).setRefreshActionButtonState(pFlag);
}
}
}
public static final String OCDB_File_Name = "opencellid.csv";
/**
* The folder path to OCDB download.
* @param context
* @return
*/
public static String getOCDBDownloadDirectoryPath(Context context) {
return (context.getExternalFilesDir(null) + File.separator) + "OpenCellID/";
}
/**
* The interface to be implemented by the caller of RequestTask so it can perform contextual
* actions once the async task is completed.
*
* E.g. rechecking current cell in the newly updated database after OCID download.
*/
public interface AsyncTaskCompleteListener {
void onAsyncTaskSucceeded();
void onAsyncTaskFailed(String result);
}
}