/**
* Copyright (C) 2013 Louis Teboul <louisteboul@gmail.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 2 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, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
**/
package com.secupwn.aimsicd.utils;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.support.v4.app.Fragment;
import android.text.TextUtils;
import com.secupwn.aimsicd.R;
import com.secupwn.aimsicd.ui.fragments.MapFragment;
import com.secupwn.aimsicd.constants.DrawerMenu;
import com.secupwn.aimsicd.service.AimsicdService;
import com.secupwn.aimsicd.service.CellTracker;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import io.freefair.android.injection.app.InjectionAppCompatActivity;
import io.freefair.android.util.logging.AndroidLogger;
import io.freefair.android.util.logging.Logger;
/**
*
* Description: This class contain many various functions to:
*
* - present Toast messages
* - getTimestamp
* - Check network connectivity
* - Download CSV file with BTS data via HTTP API from OCID servers
* - Convert ByteToString
* - unpackListOfStrings
* - Check if SD is writable
* - get System properties
* - Check for SU and BusyBox
*
*/
public class Helpers {
private static final Logger log = AndroidLogger.forClass(Helpers.class);
private static final int CHARS_PER_LINE = 34;
/**
* Description: Long toast message
*
* Notes:
*
* This is only a proxy method to the Toaster class.
* It also takes care of using the Toaster's Singleton.
*
* @param context Application Context
* @param msg Message to send
*/
public static void msgLong(Context context, String msg) {
Toaster.msgLong(context, msg);
}
/**
* Description: Short toast message
*
* Notes:
*
* This is only a proxy method to the Toaster class.
* It also takes care of using the Toaster's Singleton.
*
* @param context Application Context
* @param msg Message to send
*/
public static void msgShort(Context context, String msg) {
Toaster.msgShort(context, msg);
}
/**
* Description: Long toast message
*
* Notes:
*
* This is only a proxy method to the Toaster class.
* It also takes care of using the Toaster's Singleton.
*
* @param context Application Context
* @param msg Message to send
*/
public static void sendMsg(Context context, String msg) {
Toaster.msgLong(context, msg);
}
/**
* Checks if Network connectivity is available to download OpenCellID data
* Requires: android:name="android.permission.ACCESS_NETWORK_STATE"
*/
public static Boolean isNetAvailable(Context context) {
try {
ConnectivityManager cM = (ConnectivityManager)
context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo wifiInfo = cM.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
NetworkInfo mobileInfo = cM.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
if (wifiInfo != null && mobileInfo != null) {
return wifiInfo.isConnected() || mobileInfo.isConnected();
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return false;
}
/**
* Description: Requests Cell data from OpenCellID.org (OCID).
*
* Notes:
*
* The free OCID API has a download limit of 1000 BTSs for each download.
* Thus we need to carefully select the area we choose to download and make sure it is
* centered on the current GPS location. (It is also possible to query the number of
* cells in a particular bounding box (bbox), and use that.)
*
* The bbox is described in the OCID API here:
* http://wiki.opencellid.org/wiki/API#Getting_the_list_of_cells_in_a_specified_area
*
* In an urban area, we could try to limit ourselves to an area radius of ~2 Km.
* The (GSM) Timing Advance is limiting us to 35 Km.
*
* The OCID API payload:
*
* required: key=<apiKey>&BBOX=<latmin>,<lonmin>,<latmax>,<lonmax>
* optional: &mcc=<mcc>&mnc=<mnc>&lac=<lac>&radio=<radio>
* &limit=<limit>&offset=<offset>&format=<format>
*
* Our API query is using: (Lat1,Lon1, Lat2,Lon2, mcc,mnc,lac)
*
* Issues:
*
* [ ] A too restrictive payload leads to many missing BTS in area, but a too liberal
* payload would return many less relevant ones and would cause us to reach the
* OCID API 1000 BTS download limit much faster. The solution would be to make the
* BBOX smaller, but that in turn, would result in the loss of some more distant,
* but still available towers. Possibly making them appears as RED, even when they
* are neither fake nor IMSI-catchers. However, a more realistic BTS picture is
* more useful, especially when sharing that info across different devices using
* on different RAT and MNO.
*
* [ ] We need a smarter way to handle the downloading of the BTS data. The OCID API
* allows for finding how many cells are contained in a query. We can the use this
* info to loop the max query size to get all those cells. The Query format is:
*
* GET: http://<WebServiceURL>/cell/getInAreaSize
*
* The OCID API payload:
*
* required: key=<apiKey>&BBOX=<latmin>,<lonmin>,<latmax>,<lonmax>
* optional: &mcc=<mcc>&mnc=<mnc>&lac=<lac>&radio=<radio>&format=<format>
*
* result: JSON:
* {
* count: 123
* }
*
* [x] Q: How is the BBOX actually calculated from the "radius"?
* A: It's calculated as an inscribed circle to a square of 2*R on each side.
* See ./utils/GeoLocation.java
*
* Dependencies: GeoLocation.java
*
* Used:
* @param cell Current Cell Information
*
*/
public static void getOpenCellData(InjectionAppCompatActivity injectionActivity, Cell cell, char type) {
getOpenCellData(injectionActivity, cell, type, null);
}
public static void getOpenCellData(InjectionAppCompatActivity injectionActivity, Cell cell, char type, final AimsicdService service) {
if (Helpers.isNetAvailable(injectionActivity)) {
if (!"NA".equals(CellTracker.OCID_API_KEY)) {
double earthRadius = 6371.01; // [Km]
int radius = 2; // Use a 2 Km radius with center at GPS location.
if (Double.doubleToRawLongBits(cell.getLat()) != 0 &&
Double.doubleToRawLongBits(cell.getLon()) != 0) {
//New GeoLocation object to find bounding Coordinates
GeoLocation currentLoc = GeoLocation.fromDegrees(cell.getLat(), cell.getLon());
//Calculate the Bounding Box Coordinates using an N Km "radius" //0=min, 1=max
GeoLocation[] boundingCoords = currentLoc.boundingCoordinates(radius, earthRadius);
String boundParameter;
//Request OpenCellID data for Bounding Coordinates (0 = min, 1 = max)
boundParameter = String.valueOf(boundingCoords[0].getLatitudeInDegrees()) + ","
+ String.valueOf(boundingCoords[0].getLongitudeInDegrees()) + ","
+ String.valueOf(boundingCoords[1].getLatitudeInDegrees()) + ","
+ String.valueOf(boundingCoords[1].getLongitudeInDegrees());
log.info("OCID BBOX is set to: " + boundParameter + " with radius " + radius + " Km.");
StringBuilder sb = new StringBuilder();
sb.append("http://www.opencellid.org/cell/getInArea?key=")
.append(CellTracker.OCID_API_KEY).append("&BBOX=")
.append(boundParameter);
log.info("OCID MCC is set to: " + cell.getMobileCountryCode());
if (cell.getMobileCountryCode() != Integer.MAX_VALUE) {
sb.append("&mcc=").append(cell.getMobileCountryCode());
}
log.info("OCID MNC is set to: " + cell.getMobileNetworkCode());
if (cell.getMobileNetworkCode() != Integer.MAX_VALUE) {
sb.append("&mnc=").append(cell.getMobileNetworkCode());
}
sb.append("&format=csv");
new RequestTask(injectionActivity, type, new RequestTask.AsyncTaskCompleteListener() {
@Override
public void onAsyncTaskSucceeded() {
log.verbose("RequestTask's OCID download was successful. Callback rechecking connected cell against database");
service.getCellTracker().compareLacAndOpenDb();
}
@Override
public void onAsyncTaskFailed(String result) { }
}).execute(sb.toString());
}
} else {
Fragment myFragment = injectionActivity.getSupportFragmentManager().findFragmentByTag(String.valueOf(DrawerMenu.ID.MAIN.ALL_CURRENT_CELL_DETAILS));
if (myFragment instanceof MapFragment) {
((MapFragment) myFragment).setRefreshActionButtonState(false);
}
Helpers.sendMsg(injectionActivity, injectionActivity.getString(R.string.no_opencellid_key_detected));
}
} else {
Fragment myFragment = injectionActivity.getSupportFragmentManager().findFragmentByTag(String.valueOf(DrawerMenu.ID.MAIN.ALL_CURRENT_CELL_DETAILS));
if (myFragment instanceof MapFragment) {
((MapFragment) myFragment).setRefreshActionButtonState(false);
}
final AlertDialog.Builder builder = new AlertDialog.Builder(injectionActivity);
builder.setTitle(R.string.no_network_connection_title)
.setMessage(R.string.no_network_connection_message);
builder.create().show();
}
}
/**
* Return a String List representing response from invokeOemRilRequestRaw
*
* @param aob Byte array response from invokeOemRilRequestRaw
*/
public static List<String> unpackByteListOfStrings(byte aob[]) {
if (aob.length == 0) {
// WARNING: This one is very chatty!
log.verbose("invokeOemRilRequestRaw: byte-list response Length = 0");
return Collections.emptyList();
}
int lines = aob.length / CHARS_PER_LINE;
String[] display = new String[lines];
for (int i = 0; i < lines; i++) {
int offset, byteCount;
offset = i * CHARS_PER_LINE + 2;
byteCount = 0;
if (offset + byteCount >= aob.length) {
log.error("Unexpected EOF");
break;
}
while (aob[offset + byteCount] != 0 && (byteCount < CHARS_PER_LINE)) {
byteCount += 1;
if (offset + byteCount >= aob.length) {
log.error("Unexpected EOF");
break;
}
}
display[i] = new String(aob, offset, byteCount).trim();
}
int newLength = display.length;
while (newLength > 0 && TextUtils.isEmpty(display[newLength - 1])) {
newLength -= 1;
}
return Arrays.asList(Arrays.copyOf(display, newLength));
}
public static String getSystemProp(Context context, String prop, String def) {
String result = null;
try {
result = SystemPropertiesReflection.get(context, prop);
} catch (IllegalArgumentException iae) {
log.error("Failed to get system property: " + prop, iae);
}
return result == null ? def : result;
}
public static String convertStreamToString(InputStream is) throws Exception {
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line).append("\n");
}
reader.close();
return sb.toString();
}
/**
* Description: Deletes the entire database by removing internal SQLite DB file
*
*
* Dependencies: Used in AIMSICD.java
*
* Notes: See Android developer info: http://tinyurl.com/psz8vmt
*
* WARNING!
* This deletes the entire database, thus any subsequent DB access will FC app.
* Therefore we need to either restart app or run AIMSICDDbAdapter, to rebuild DB.
* See: #581
*
* In addition, since SQLite is kept in memory during lifetime of App, and
* is using Journaling, we have to restart app in order to clear old data
* already in memory.
*
* @param pContext Context of Activity
*/
public static void askAndDeleteDb(final Context pContext) {
AlertDialog lAlertDialog = new AlertDialog.Builder(pContext)
.setNegativeButton(R.string.open_cell_id_button_cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
})
.setPositiveButton(R.string.open_cell_id_button_ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// Probably put in try/catch in case file removal fails...
pContext.stopService(new Intent(pContext, AimsicdService.class));
pContext.deleteDatabase("aimsicd.db");
new RealmHelper(pContext);
pContext.startService(new Intent(pContext, AimsicdService.class));
msgLong(pContext, pContext.getString(R.string.delete_database_msg_success));
}
})
.setMessage(pContext.getString(R.string.clear_database_question))
.setTitle(R.string.clear_database)
.setCancelable(false)
.setIcon(R.drawable.ic_action_delete_database).create();
lAlertDialog.show();
}
}