/* * Copyright 2011 yingxinwu.g@gmail.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package xink.vpn; import java.io.EOFException; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; import xink.crypto.StreamCrypto; import xink.vpn.stats.VpnConnectivityStats; import xink.vpn.wrapper.InvalidProfileException; import xink.vpn.wrapper.VpnProfile; import xink.vpn.wrapper.VpnState; import xink.vpn.wrapper.VpnType; import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.text.TextUtils; import android.util.Log; /** * Repository of VPN profiles * * @author ywu */ public final class VpnProfileRepository { private static final String TAG = "xink"; private static final String FILE_PROFILES = "profiles"; private static final String FILE_ACT_ID = "active_profile_id"; private static VpnProfileRepository instance; private Context context; private String activeProfileId; private List<VpnProfile> profiles; private VpnState activeVpnState; private VpnConnectivityStats connStats; private VpnProfileRepository(final Context ctx) { this.context = ctx; profiles = new ArrayList<VpnProfile>(); connStats = new VpnConnectivityStats(ctx); } /** * Retrieves the single instance of repository. * * @param ctx * Context * @return singleton */ public static VpnProfileRepository getInstance(final Context ctx) { if (instance == null) { instance = new VpnProfileRepository(ctx); instance.load(); StreamCrypto.init(ctx); } return instance; } /** * Get state of the active vpn. */ public VpnState getActiveVpnState() { if (activeVpnState == null) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); String v = prefs.getString(context.getString(R.string.active_vpn_state_key), context.getString(R.string.active_vpn_state_default)); activeVpnState = VpnState.valueOf(v); } return activeVpnState; } /** * Update state of the active vpn. */ public void setActiveVpnState(final VpnState state) { if (!state.isStable()) return; this.activeVpnState = state; SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); prefs.edit().putString(context.getString(R.string.active_vpn_state_key), state.toString()).commit(); } /** * Retrieves the connectivity stats instance */ public VpnConnectivityStats getConnectivityStats() { return this.connStats; } public void save() { Log.d(TAG, "save, activeId=" + activeProfileId + ", profiles=" + profiles); try { saveActiveProfileId(); saveProfiles(); } catch (IOException e) { Log.e(TAG, "save profiles failed", e); } } private void saveActiveProfileId() throws IOException { ObjectOutputStream os = null; try { os = new ObjectOutputStream(openPrivateFileOutput(FILE_ACT_ID)); os.writeObject(activeProfileId); } finally { if (os != null) { os.close(); } } } private void saveProfiles() throws IOException { ObjectOutputStream os = null; try { os = new ObjectOutputStream(openPrivateFileOutput(FILE_PROFILES)); for (VpnProfile p : profiles) { p.write(os); } } finally { if (os != null) { os.close(); } } } private FileOutputStream openPrivateFileOutput(final String fileName) throws FileNotFoundException { return context.openFileOutput(fileName, Context.MODE_PRIVATE); } private void load() { try { loadActiveProfileId(); loadProfiles(); Log.d(TAG, "loaded, activeId=" + activeProfileId + ", profiles=" + profiles); } catch (Exception e) { Log.e(TAG, "load profiles failed", e); } } private void loadActiveProfileId() throws IOException, ClassNotFoundException { ObjectInputStream is = null; try { is = new ObjectInputStream(context.openFileInput(FILE_ACT_ID)); activeProfileId = (String) is.readObject(); } finally { if (is != null) { is.close(); } } } private void loadProfiles() throws Exception { ObjectInputStream is = null; try { is = new ObjectInputStream(context.openFileInput(FILE_PROFILES)); loadProfilesFrom(is); } finally { if (is != null) { is.close(); } } } private void loadProfilesFrom(final ObjectInputStream is) throws Exception { Object obj = null; try { while (true) { VpnType type = (VpnType) is.readObject(); obj = is.readObject(); loadProfileObject(type, obj, is); } } catch (EOFException eof) { Log.d(TAG, "reach the end of profiles file"); } } private void loadProfileObject(final VpnType type, final Object obj, final ObjectInputStream is) throws Exception { if (obj == null) return; VpnProfile p = VpnProfile.newInstance(type, context); if (p.isCompatible(obj)) { p.read(obj, is); profiles.add(p); } else { Log.e(TAG, "saved profile '" + obj + "' is NOT compatible with " + type); } } public void setActiveProfile(final VpnProfile profile) { Log.i(TAG, "active vpn set to: " + profile); activeProfileId = profile.getId(); } public String getActiveProfileId() { return activeProfileId; } public VpnProfile getActiveProfile() { if (activeProfileId == null) return null; return getProfileById(activeProfileId); } private VpnProfile getProfileById(final String id) { for (VpnProfile p : profiles) { if (p.getId().equals(id)) return p; } return null; } public VpnProfile getProfileByName(final String name) { for (VpnProfile p : profiles) { if (p.getName().equals(name)) return p; } return null; } /** * @return a read-only view of the VpnProfile list. */ public List<VpnProfile> getAllVpnProfiles() { return Collections.unmodifiableList(profiles); } public synchronized void addVpnProfile(final VpnProfile p) { p.postConstruct(); profiles.add(p); } public void checkProfile(final VpnProfile newProfile) { String newName = newProfile.getName(); if (TextUtils.isEmpty(newName)) throw new InvalidProfileException("profile name is empty.", R.string.err_empty_name); for (VpnProfile p : profiles) { if (newProfile != p && newName.equals(p.getName())) throw new InvalidProfileException("duplicated profile name '" + newName + "'.", R.string.err_duplicated_profile_name, newName); } newProfile.validate(); } public synchronized void deleteVpnProfile(final VpnProfile profile) { String id = profile.getId(); boolean removed = profiles.remove(profile); Log.d(TAG, "delete vpn: " + profile + ", removed=" + removed); if (id.equals(activeProfileId)) { activeProfileId = null; Log.d(TAG, "deactivate vpn: " + profile); } } public void backup(final String path) { if (profiles.isEmpty()) { Log.i(TAG, "profile list is empty, will not export"); return; } save(); File dir = ensureDir(path); try { doBackup(dir, FILE_ACT_ID); doBackup(dir, FILE_PROFILES); } catch (Exception e) { throw new AppException("backup failed", e, R.string.err_exp_failed); } } private File ensureDir(final String path) { File dir = new File(path); Utils.ensureDir(dir); return dir; } private void doBackup(final File dir, final String name) throws Exception { InputStream is = context.openFileInput(name); OutputStream os = new FileOutputStream(new File(dir, name)); StreamCrypto.encrypt(is, os); } public void restore(final String dir) { checkExternalData(dir); try { doRestore(dir, FILE_ACT_ID); doRestore(dir, FILE_PROFILES); clean(); load(); } catch (Exception e) { throw new AppException("restore failed", e, R.string.err_imp_failed); } } private void clean() { activeProfileId = null; profiles.clear(); } private void doRestore(final String dir, final String name) throws Exception { InputStream is = new FileInputStream(new File(dir, name)); OutputStream os = openPrivateFileOutput(name); StreamCrypto.decrypt(is, os); } /* * verify data files in external storage. */ private void checkExternalData(final String path) { File id = new File(path, FILE_ACT_ID); File profiles = new File(path, FILE_PROFILES); if (!(verifyDataFile(id) && verifyDataFile(profiles))) throw new AppException("no valid data found in: " + path, R.string.err_imp_nodata); } private boolean verifyDataFile(final File file) { return file.exists() && file.isFile() && file.length() > 0; } /** * Check last backup time. * * @return timestamp of last backup, null for no backup. */ public Date checkLastBackup(final String path) { File id = new File(path, FILE_ACT_ID); if (!verifyDataFile(id)) return null; return new Date(id.lastModified()); } }