/* * 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 static xink.vpn.Constants.*; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import xink.vpn.editor.EditAction; import xink.vpn.editor.VpnProfileEditor; import xink.vpn.wrapper.KeyStore; import xink.vpn.wrapper.VpnProfile; import xink.vpn.wrapper.VpnState; import xink.vpn.wrapper.VpnType; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.net.Uri; import android.os.Bundle; import android.text.TextUtils; import android.util.Log; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.ImageView; import android.widget.ListView; import android.widget.RadioButton; import android.widget.SimpleAdapter; import android.widget.SimpleAdapter.ViewBinder; import android.widget.TextView; import android.widget.Toast; import android.widget.ToggleButton; public class VpnSettings extends Activity { private static final String ROWITEM_KEY = "vpn"; //$NON-NLS-1$ private static final String TAG = "xink"; //$NON-NLS-1$ // views on a single row will bind to the same data object private static final String[] VPN_VIEW_KEYS = new String[] { ROWITEM_KEY, ROWITEM_KEY, ROWITEM_KEY }; private static final int[] VPN_VIEWS = new int[] { R.id.radioActive, R.id.tgbtnConn, R.id.txtStateMsg }; private VpnProfileRepository repository; private ListView vpnListView; private List<Map<String, VpnViewItem>> vpnListViewContent; private VpnViewBinder vpnViewBinder = new VpnViewBinder(); private VpnViewItem activeVpnItem; private SimpleAdapter vpnListAdapter; private VpnActor actor; private BroadcastReceiver stateBroadcastReceiver; private KeyStore keyStore; private Runnable resumeAction; /** Called when the activity is first created. */ @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); repository = VpnProfileRepository.getInstance(getApplicationContext()); actor = new VpnActor(getApplicationContext()); keyStore = new KeyStore(getApplicationContext()); setTitle(R.string.selectVpn); setContentView(R.layout.vpn_list); ((TextView) findViewById(R.id.btnAddVpn)).setOnClickListener(new OnClickListener() { @Override public void onClick(final View v) { onAddVpn(); } }); vpnListViewContent = new ArrayList<Map<String, VpnViewItem>>(); vpnListView = (ListView) findViewById(R.id.listVpns); buildVpnListView(); registerReceivers(); checkAllVpnStatus(); checkHack(false); } /* * Check whether the system is hacked to allow 3rd-party keypair */ private void checkHack(final boolean force) { HackKeyStore hack = new HackKeyStore(this); hack.check(force); } private void checkAllVpnStatus() { new Thread(new Runnable() { @Override public void run() { actor.checkAllStatus(); } }, "vpn-state-checker").start(); //$NON-NLS-1$ } private void buildVpnListView() { loadContent(); vpnListAdapter = new SimpleAdapter(this, vpnListViewContent, R.layout.vpn_profile, VPN_VIEW_KEYS, VPN_VIEWS); vpnListAdapter.setViewBinder(vpnViewBinder); vpnListView.setAdapter(vpnListAdapter); registerForContextMenu(vpnListView); } private void loadContent() { vpnListViewContent.clear(); activeVpnItem = null; String activeProfileId = repository.getActiveProfileId(); List<VpnProfile> allVpnProfiles = repository.getAllVpnProfiles(); for (VpnProfile vpnProfile : allVpnProfiles) { addToVpnListView(activeProfileId, vpnProfile); } } private void addToVpnListView(final String activeProfileId, final VpnProfile vpnProfile) { if (vpnProfile == null) return; VpnViewItem item = makeVpnViewItem(activeProfileId, vpnProfile); Map<String, VpnViewItem> row = new HashMap<String, VpnViewItem>(); row.put(ROWITEM_KEY, item); vpnListViewContent.add(row); } private VpnViewItem makeVpnViewItem(final String activeProfileId, final VpnProfile vpnProfile) { VpnViewItem item = new VpnViewItem(); item.profile = vpnProfile; if (vpnProfile.getId().equals(activeProfileId)) { item.isActive = true; activeVpnItem = item; } return item; } @Override public void onCreateContextMenu(final ContextMenu menu, final View v, final ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.vpn_list_context_menu, menu); AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; VpnViewItem selectedVpnItem = getVpnViewItemAt(info.position); VpnProfile p = selectedVpnItem.profile; menu.setHeaderTitle(p.getName()); // profile can edit only when disconnected boolean isIdle = p.getState() == VpnState.IDLE; menu.findItem(R.id.menu_edit_vpn).setEnabled(isIdle); menu.findItem(R.id.menu_del_vpn).setEnabled(isIdle); } @SuppressWarnings("unchecked") private VpnViewItem getVpnViewItemAt(final int pos) { return ((Map<String, VpnViewItem>) vpnListAdapter.getItem(pos)).get(ROWITEM_KEY); } @Override public boolean onContextItemSelected(final MenuItem item) { boolean consumed = false; int itemId = item.getItemId(); AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo(); VpnViewItem vpnItem = getVpnViewItemAt(info.position); switch (itemId) { case R.id.menu_del_vpn: onDeleteVpn(vpnItem); consumed = true; break; case R.id.menu_edit_vpn: onEditVpn(vpnItem); consumed = true; break; default: consumed = super.onContextItemSelected(item); break; } return consumed; } private void onAddVpn() { startActivityForResult(new Intent(this, VpnTypeSelection.class), REQ_SELECT_VPN_TYPE); } private void onDeleteVpn(final VpnViewItem vpnItem) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setIcon(android.R.drawable.ic_dialog_alert).setTitle(android.R.string.dialog_alert_title).setMessage(R.string.del_vpn_confirm); builder.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { @Override public void onClick(final DialogInterface dialog, final int which) { repository.deleteVpnProfile(vpnItem.profile); buildVpnListView(); } }).setNegativeButton(android.R.string.no, null).show(); } private void onEditVpn(final VpnViewItem vpnItem) { Log.d(TAG, "onEditVpn"); //$NON-NLS-1$ VpnProfile p = vpnItem.profile; editVpn(p); } private void editVpn(final VpnProfile p) { VpnType type = p.getType(); Class<? extends VpnProfileEditor> editorClass = type.getEditorClass(); if (editorClass == null) { Log.d(TAG, "editor class is null for " + type); //$NON-NLS-1$ return; } Intent intent = new Intent(this, editorClass); intent.setAction(EditAction.EDIT.toString()); intent.putExtra(KEY_VPN_PROFILE_NAME, p.getName()); startActivityForResult(intent, REQ_EDIT_VPN); } @Override public boolean onCreateOptionsMenu(final Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.vpn_list_menu, menu); menu.findItem(R.id.menu_about).setIcon(android.R.drawable.ic_menu_info_details); menu.findItem(R.id.menu_help).setIcon(android.R.drawable.ic_menu_help); menu.findItem(R.id.menu_exp).setIcon(android.R.drawable.ic_menu_save); menu.findItem(R.id.menu_imp).setIcon(android.R.drawable.ic_menu_set_as); menu.findItem(R.id.menu_diag).setIcon(android.R.drawable.ic_menu_manage); menu.findItem(R.id.menu_settings).setIcon(android.R.drawable.ic_menu_preferences); return true; } @Override public boolean onPrepareOptionsMenu(final Menu menu) { menu.findItem(R.id.menu_exp).setEnabled(!repository.getAllVpnProfiles().isEmpty()); menu.findItem(R.id.menu_imp).setEnabled(checkLastBackup()); return true; } private boolean checkLastBackup() { return repository.checkLastBackup(getBackupDir()) != null; } /** * Handles item selections */ @Override public boolean onOptionsItemSelected(final MenuItem item) { boolean consumed = true; int itemId = item.getItemId(); switch (itemId) { case R.id.menu_about: showDialog(DLG_ABOUT); break; case R.id.menu_help: openWikiHome(); break; case R.id.menu_exp: showDialog(DLG_BACKUP); break; case R.id.menu_imp: showDialog(DLG_RESTORE); break; case R.id.menu_diag: checkHack(true); break; case R.id.menu_settings: openSettings(); break; default: consumed = super.onContextItemSelected(item); break; } return consumed; } private void openSettings() { startActivity(new Intent(this, Settings.class)); } private AlertDialog createBackupDlg() { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setIcon(android.R.drawable.ic_dialog_info).setTitle(R.string.export).setMessage(getString(R.string.i_exp, getBackupDir())); builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(final DialogInterface dialog, final int which) { doBackup(); } }).setNegativeButton(android.R.string.cancel, null); AlertDialog dlg = builder.create(); dlg.setOnDismissListener(new DialogInterface.OnDismissListener() { @Override public void onDismiss(final DialogInterface dialog) { Log.d(TAG, "onDismiss DLG_BACKUP"); removeDialog(DLG_BACKUP); } }); return dlg; } private void doBackup() { Log.d(TAG, "doBackup"); try { repository.backup(getBackupDir()); Toast.makeText(this, R.string.i_exp_done, Toast.LENGTH_SHORT).show(); } catch (AppException e) { Log.e(TAG, "doBackup failed", e); Utils.showErrMessage(this, e); } } private AlertDialog createRestoreDlg() { String lastBak = makeLastBackupText(); AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setIcon(android.R.drawable.ic_dialog_info).setTitle(R.string.imp).setMessage(getString(R.string.i_imp, lastBak)); builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(final DialogInterface dialog, final int which) { doRestore(); } }).setNegativeButton(android.R.string.cancel, null); AlertDialog dlg = builder.create(); dlg.setOnDismissListener(new DialogInterface.OnDismissListener() { @Override public void onDismiss(final DialogInterface dialog) { Log.d(TAG, "onDismiss DLG_RESTORE"); removeDialog(DLG_RESTORE); } }); return dlg; } private String makeLastBackupText() { Date lastBackup = repository.checkLastBackup(getBackupDir()); SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd HH:mm"); return f.format(lastBackup); } private void doRestore() { Log.d(TAG, "doRestore"); try { repository.restore(getBackupDir()); buildVpnListView(); actor.disconnect(); checkAllVpnStatus(); Toast.makeText(this, R.string.i_imp_done, Toast.LENGTH_SHORT).show(); } catch (AppException e) { Log.e(TAG, "doRestore failed", e); Utils.showErrMessage(this, e); } } private String getBackupDir() { return getString(R.string.exp_dir); } private void openWikiHome() { openUrl(getString(R.string.url_wiki_home)); } @Override protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) { if (data == null) return; switch (requestCode) { case REQ_SELECT_VPN_TYPE: onVpnTypePicked(data); break; case REQ_ADD_VPN: onVpnProfileAdded(data); break; case REQ_EDIT_VPN: onVpnProfileEdited(); break; default: Log.w(TAG, "onActivityResult, unknown reqeustCode " + requestCode + ", result=" + resultCode + ", data=" + data); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ break; } } private void onVpnTypePicked(final Intent data) { VpnType pickedVpnType = (VpnType) data.getExtras().get(KEY_VPN_TYPE); addVpn(pickedVpnType); } private void addVpn(final VpnType vpnType) { Log.i(TAG, "add vpn " + vpnType); //$NON-NLS-1$ Class<? extends VpnProfileEditor> editorClass = vpnType.getEditorClass(); if (editorClass == null) { Log.d(TAG, "editor class is null for " + vpnType); //$NON-NLS-1$ return; } Intent intent = new Intent(this, editorClass); intent.setAction(EditAction.CREATE.toString()); startActivityForResult(intent, REQ_ADD_VPN); } private void onVpnProfileAdded(final Intent data) { Log.i(TAG, "new vpn profile created"); //$NON-NLS-1$ String name = data.getStringExtra(KEY_VPN_PROFILE_NAME); VpnProfile p = repository.getProfileByName(name); addToVpnListView(repository.getActiveProfileId(), p); refreshVpnListView(); } private void onVpnProfileEdited() { Log.i(TAG, "vpn profile modified"); //$NON-NLS-1$ refreshVpnListView(); } private void registerReceivers() { IntentFilter filter = new IntentFilter(); filter.addAction(ACTION_VPN_CONNECTIVITY); stateBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(final Context context, final Intent intent) { String action = intent.getAction(); if (ACTION_VPN_CONNECTIVITY.equals(action)) { onStateChanged(intent); } else { Log.d(TAG, "VPNSettings receiver ignores intent:" + intent); //$NON-NLS-1$ } } }; registerReceiver(stateBroadcastReceiver, filter); } private void onStateChanged(final Intent intent) { //Log.d(TAG, "onStateChanged: " + intent); //$NON-NLS-1$ final String profileName = intent.getStringExtra(BROADCAST_PROFILE_NAME); final VpnState state = Utils.extractVpnState(intent); final int err = intent.getIntExtra(BROADCAST_ERROR_CODE, VPN_ERROR_NO_ERROR); runOnUiThread(new Runnable() { @Override public void run() { stateChanged(profileName, state, err); } }); } private void stateChanged(final String profileName, final VpnState state, final int errCode) { //Log.d(TAG, "stateChanged, '" + profileName + "', state: " + state + ", errCode=" + errCode); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ VpnProfile p = repository.getProfileByName(profileName); if (p == null) { Log.w(TAG, profileName + " NOT found"); //$NON-NLS-1$ return; } p.setState(state); refreshVpnListView(); } @Override protected void onDestroy() { //Log.d(TAG, "VpnSettings onDestroy"); //$NON-NLS-1$ unregisterReceivers(); super.onDestroy(); } @Override protected void onPause() { //Log.d(TAG, "VpnSettings onPause"); //$NON-NLS-1$ save(); super.onPause(); } private void save() { repository.save(); } private void unregisterReceivers() { if (stateBroadcastReceiver != null) { unregisterReceiver(stateBroadcastReceiver); } } private void vpnItemActivated(final VpnViewItem activatedItem) { if (activeVpnItem == activatedItem) return; if (activeVpnItem != null) { activeVpnItem.isActive = false; } activeVpnItem = activatedItem; actor.activate(activeVpnItem.profile); refreshVpnListView(); } private void refreshVpnListView() { runOnUiThread(new Runnable() { @Override public void run() { vpnListAdapter.notifyDataSetChanged(); } }); } @Override protected Dialog onCreateDialog(final int id) { switch (id) { case DLG_ABOUT: return createAboutDlg(); case DLG_BACKUP: return createBackupDlg(); case DLG_RESTORE: return createRestoreDlg(); default: break; } return null; } private Dialog createAboutDlg() { AlertDialog.Builder builder; LayoutInflater inflater = getLayoutInflater(); View layout = inflater.inflate(R.layout.about, (ViewGroup) findViewById(R.id.aboutRoot)); builder = new AlertDialog.Builder(this); builder.setView(layout).setTitle(getString(R.string.about)); bindPackInfo(layout); ImageView imgPaypal = (ImageView) layout.findViewById(R.id.imgPaypalDonate); imgPaypal.setOnClickListener(new OnClickListener() { @Override public void onClick(final View v) { openUrl(getString(R.string.url_paypal_donate)); } }); AlertDialog dlg = builder.create(); dlg.setOnDismissListener(new DialogInterface.OnDismissListener() { @Override public void onDismiss(final DialogInterface dialog) { Log.d(TAG, "onDismiss DLG_ABOUT"); removeDialog(DLG_ABOUT); } }); return dlg; } private void bindPackInfo(final View layout) { try { PackageInfo info = getPackageManager().getPackageInfo(getPackageName(), 0); TextView txtVer = (TextView) layout.findViewById(R.id.txtVersion); txtVer.setText(getString(R.string.pack_ver, getString(R.string.app_name), info.versionName)); } catch (NameNotFoundException e) { Log.e(TAG, "get pack info failed", e); //$NON-NLS-1$ } } private void openUrl(final String url) { Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); startActivity(intent); } @Override protected void onResume() { super.onResume(); Log.d(TAG, "onResume, check and run resume action"); if (resumeAction != null) { Runnable action = resumeAction; resumeAction = null; runOnUiThread(action); } } private void connect(final VpnProfile p) { if (unlockKeyStoreIfNeeded(p)) { actor.connect(p); } } private boolean unlockKeyStoreIfNeeded(final VpnProfile p) { if (!p.needKeyStoreToConnect() || keyStore.isUnlocked()) return true; Log.i(TAG, "keystore is locked, unlock it now and reconnect later."); resumeAction = new Runnable() { @Override public void run() { // redo this after unlock activity return connect(p); } }; keyStore.unlock(this); return false; } private void disconnect() { actor.disconnect(); } final class VpnViewBinder implements ViewBinder { @Override public boolean setViewValue(final View view, final Object data, final String textRepresentation) { if (!(data instanceof VpnViewItem)) return false; VpnViewItem item = (VpnViewItem) data; boolean bound = true; if (view instanceof RadioButton) { bindVpnItem((RadioButton) view, item); } else if (view instanceof ToggleButton) { bindVpnState((ToggleButton) view, item); } else if (view instanceof TextView) { bindVpnStateMsg(((TextView) view), item); } else { bound = false; Log.d(TAG, "unknown view, not bound: v=" + view + ", data=" + textRepresentation); //$NON-NLS-1$ //$NON-NLS-2$ } return bound; } private void bindVpnItem(final RadioButton view, final VpnViewItem item) { view.setOnCheckedChangeListener(null); view.setText(item.profile.getName()); view.setChecked(item.isActive); view.setOnCheckedChangeListener(item); } private void bindVpnState(final ToggleButton view, final VpnViewItem item) { view.setOnCheckedChangeListener(null); VpnState state = item.profile.getState(); view.setChecked(state == VpnState.CONNECTED); view.setEnabled(Utils.isInStableState(item.profile)); view.setOnCheckedChangeListener(item); } private void bindVpnStateMsg(final TextView textView, final VpnViewItem item) { VpnState state = item.profile.getState(); String txt = getStateText(state); textView.setVisibility(TextUtils.isEmpty(txt) ? View.INVISIBLE : View.VISIBLE); textView.setText(txt); } private String getStateText(final VpnState state) { String txt = ""; //$NON-NLS-1$ switch (state) { case CONNECTING: txt = getString(R.string.connecting); break; case DISCONNECTING: txt = getString(R.string.disconnecting); break; } return txt; } } final class VpnViewItem implements OnCheckedChangeListener { VpnProfile profile; boolean isActive; @Override public void onCheckedChanged(final CompoundButton button, final boolean isChecked) { if (button instanceof RadioButton) { onActivationChanged(isChecked); } else if (button instanceof ToggleButton) { toggleState(isChecked); } } private void onActivationChanged(final boolean isChecked) { if (isActive == isChecked) return; isActive = isChecked; if (isActive) { vpnItemActivated(this); } } private void toggleState(final boolean isChecked) { if (isChecked) { connect(profile); } else { disconnect(); } } @Override public String toString() { return profile.getName(); } } }