/**
* Copyright (C) 2010-2012 Regis Montoya (aka r3gis - www.r3gis.fr)
* This file is part of CSipSimple.
*
* CSipSimple 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.
* If you own a pjsip commercial license you can also redistribute it
* and/or modify it under the terms of the GNU Lesser General Public License
* as an android library.
*
* CSipSimple 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 CSipSimple. If not, see <http://www.gnu.org/licenses/>.
*/
package com.csipsimple.backup;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.text.TextUtils;
import android.text.format.DateFormat;
import com.csipsimple.api.SipManager;
import com.csipsimple.api.SipProfile;
import com.csipsimple.db.DBProvider;
import com.csipsimple.models.Filter;
import com.csipsimple.utils.CallHandlerPlugin;
import com.csipsimple.utils.Log;
import com.csipsimple.utils.PreferencesWrapper;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.Map;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
public final class SipProfileJson {
private final static String THIS_FILE = "SipProfileJson";
private final static String FILTER_KEY = "filters";
private SipProfileJson() {
}
private static JSONObject serializeBaseSipProfile(SipProfile profile) {
ContentValues cv = profile.getDbContentValues();
Columns cols = getSipProfileColumns(true);
return cols.contentValueToJSON(cv);
}
private static JSONObject serializeBaseFilter(Filter filter) {
ContentValues cv = filter.getDbContentValues();
Columns cols = new Columns(Filter.FULL_PROJ, Filter.FULL_PROJ_TYPES);
return cols.contentValueToJSON(cv);
}
public static JSONObject serializeSipProfile(Context context, SipProfile profile) {
JSONObject jsonProfile = serializeBaseSipProfile(profile);
JSONArray jsonFilters = new JSONArray();
Cursor c = Filter.getFiltersCursorForAccount(context, profile.id);
int numRows = c.getCount();
c.moveToFirst();
for (int i = 0; i < numRows; ++i) {
Filter f = new Filter(c);
try {
jsonFilters.put(i, serializeBaseFilter(f));
} catch (JSONException e) {
Log.e(THIS_FILE, "Impossible to add fitler", e);
}
c.moveToNext();
}
c.close();
try {
jsonProfile.put(FILTER_KEY, jsonFilters);
} catch (JSONException e) {
Log.e(THIS_FILE, "Impossible to add fitlers", e);
}
return jsonProfile;
}
public static JSONArray serializeSipProfiles(Context ctxt) {
JSONArray jsonSipProfiles = new JSONArray();
Cursor c = ctxt.getContentResolver().query(SipProfile.ACCOUNT_URI,
DBProvider.ACCOUNT_FULL_PROJECTION, null, null, null);
if (c != null) {
try {
while (c.moveToNext()) {
SipProfile account = new SipProfile(c);
JSONObject p = serializeSipProfile(ctxt, account);
try {
jsonSipProfiles.put(jsonSipProfiles.length(), p);
} catch (JSONException e) {
Log.e(THIS_FILE, "Impossible to add profile", e);
}
}
} catch (Exception e) {
Log.e(THIS_FILE, "Error on looping over sip profiles", e);
} finally {
c.close();
}
}
// Add negative fake accounts
Map<String, String> callHandlers = CallHandlerPlugin.getAvailableCallHandlers(ctxt);
for (String packageName : callHandlers.keySet()) {
final Long externalAccountId = CallHandlerPlugin
.getAccountIdForCallHandler(ctxt, packageName);
SipProfile gsmProfile = new SipProfile();
gsmProfile.id = externalAccountId;
JSONObject p = serializeSipProfile(ctxt, gsmProfile);
try {
jsonSipProfiles.put(jsonSipProfiles.length(), p);
} catch (JSONException e) {
Log.e(THIS_FILE, "Impossible to add profile", e);
}
}
return jsonSipProfiles;
}
public static JSONObject serializeSipSettings(Context ctxt) {
PreferencesWrapper prefs = new PreferencesWrapper(ctxt);
return prefs.serializeSipSettings();
}
private static final String KEY_ACCOUNTS = "accounts";
private static final String KEY_SETTINGS = "settings";
/**
* Save current sip configuration
*
* @param ctxt
* @return
*/
public static boolean saveSipConfiguration(Context ctxt, String filePassword) {
File dir = PreferencesWrapper.getConfigFolder(ctxt);
if (dir != null) {
Date d = new Date();
File file = new File(dir.getAbsoluteFile() + File.separator + "backup_"
+ DateFormat.format("yy-MM-dd_kkmmss", d) + ".json");
Log.d(THIS_FILE, "Out dir " + file.getAbsolutePath());
JSONObject configChain = new JSONObject();
try {
configChain.put(KEY_ACCOUNTS, serializeSipProfiles(ctxt));
} catch (JSONException e) {
Log.e(THIS_FILE, "Impossible to add profiles", e);
}
try {
configChain.put(KEY_SETTINGS, serializeSipSettings(ctxt));
} catch (JSONException e) {
Log.e(THIS_FILE, "Impossible to add profiles", e);
}
try {
// Create file
OutputStream fos = new FileOutputStream(file);
if(!TextUtils.isEmpty(filePassword)) {
Cipher c;
try {
c = Cipher.getInstance("AES");
SecretKeySpec k = new SecretKeySpec(filePassword.getBytes(), "AES");
c.init(Cipher.ENCRYPT_MODE, k);
fos = new CipherOutputStream(fos, c);
} catch (NoSuchAlgorithmException e) {
Log.e(THIS_FILE, "NoSuchAlgorithmException :: ", e);
} catch (NoSuchPaddingException e) {
Log.e(THIS_FILE, "NoSuchPaddingException :: ", e);
} catch (InvalidKeyException e) {
Log.e(THIS_FILE, "InvalidKeyException :: ", e);
}
}
FileWriter fstream = new FileWriter(file.getAbsoluteFile());
BufferedWriter out = new BufferedWriter(fstream);
out.write(configChain.toString(2));
// Close the output stream
out.close();
return true;
} catch (Exception e) {
// Catch exception if any
Log.e(THIS_FILE, "Impossible to save config to disk", e);
return false;
}
}
return false;
}
// --- RESTORE PART --- //
private static boolean restoreSipProfile(JSONObject jsonObj, ContentResolver cr) {
// Restore accounts
Columns cols;
ContentValues cv;
cols = getSipProfileColumns(false);
cv = cols.jsonToContentValues(jsonObj);
long profileId = cv.getAsLong(SipProfile.FIELD_ID);
if(profileId >= 0) {
Uri insertedUri = cr.insert(SipProfile.ACCOUNT_URI, cv);
profileId = ContentUris.parseId(insertedUri);
}
// TODO : else restore call handler in private db
// Restore filters
cols = new Columns(Filter.FULL_PROJ, Filter.FULL_PROJ_TYPES);
try {
JSONArray filtersObj = jsonObj.getJSONArray(FILTER_KEY);
Log.d(THIS_FILE, "We have filters for " + profileId + " > " + filtersObj.length());
for (int i = 0; i < filtersObj.length(); i++) {
JSONObject filterObj = filtersObj.getJSONObject(i);
// Log.d(THIS_FILE, "restoring "+filterObj.toString(4));
cv = cols.jsonToContentValues(filterObj);
cv.put(Filter.FIELD_ACCOUNT, profileId);
cr.insert(SipManager.FILTER_URI, cv);
}
} catch (JSONException e) {
Log.e(THIS_FILE, "Error while restoring filters", e);
}
return false;
}
public static void restoreSipSettings(Context ctxt, JSONObject settingsObj) {
PreferencesWrapper prefs = new PreferencesWrapper(ctxt);
prefs.restoreSipSettings(settingsObj);
}
/**
* Restore a sip configuration
*
* @param ctxt
* @param fileToRestore
* @return
*/
public static boolean restoreSipConfiguration(Context ctxt, File fileToRestore, String filePassword) {
if (fileToRestore == null || !fileToRestore.isFile()) {
return false;
}
StringBuffer contentBuf = new StringBuffer();
try {
BufferedReader buf;
String line;
InputStream is = new FileInputStream(fileToRestore);
if(!TextUtils.isEmpty(filePassword)) {
Cipher c;
try {
c = Cipher.getInstance("AES");
SecretKeySpec k = new SecretKeySpec(filePassword.getBytes(), "AES");
c.init(Cipher.ENCRYPT_MODE, k);
is = new CipherInputStream(is, c);
} catch (NoSuchAlgorithmException e) {
Log.e(THIS_FILE, "NoSuchAlgorithmException :: ", e);
} catch (NoSuchPaddingException e) {
Log.e(THIS_FILE, "NoSuchPaddingException :: ", e);
} catch (InvalidKeyException e) {
Log.e(THIS_FILE, "InvalidKeyException :: ", e);
}
}
InputStreamReader fr = new InputStreamReader(is);
buf = new BufferedReader(fr);
while ((line = buf.readLine()) != null) {
contentBuf.append(line);
}
fr.close();
} catch (FileNotFoundException e) {
Log.e(THIS_FILE, "Error while restoring", e);
} catch (IOException e) {
Log.e(THIS_FILE, "Error while restoring", e);
}
JSONArray accounts = null;
JSONObject settings = null;
// Parse json if some string here
if (contentBuf.length() > 0) {
try {
JSONObject mainJSONObject = new JSONObject(contentBuf.toString());
// Retrieve accounts
accounts = mainJSONObject.getJSONArray(KEY_ACCOUNTS);
// Retrieve settings
settings = mainJSONObject.getJSONObject(KEY_SETTINGS);
} catch (JSONException e) {
Log.e(THIS_FILE, "Error while parsing saved file", e);
}
} else {
return false;
}
if (accounts != null && accounts.length() > 0) {
restoreSipAccounts(ctxt, accounts);
}
if (settings != null) {
restoreSipSettings(ctxt, settings);
return true;
}
return false;
}
public static void restoreSipAccounts(Context ctxt, JSONArray accounts) {
ContentResolver cr = ctxt.getContentResolver();
// Clear old existing accounts
cr.delete(SipProfile.ACCOUNT_URI, "1", null);
cr.delete(SipManager.FILTER_URI, "1", null);
// Add each accounts
for (int i = 0; i < accounts.length(); i++) {
try {
JSONObject account = accounts.getJSONObject(i);
restoreSipProfile(account, cr);
} catch (JSONException e) {
Log.e(THIS_FILE, "Unable to parse item " + i, e);
}
}
}
private static Columns getSipProfileColumns(boolean only_secure) {
Columns cols = new Columns(DBProvider.ACCOUNT_FULL_PROJECTION,
DBProvider.ACCOUNT_FULL_PROJECTION_TYPES);
if(only_secure) {
// Never backup password
cols.removeColumn(SipProfile.FIELD_DATA);
}
return cols;
}
}