package fq.router2; import android.app.*; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.Color; import android.net.Uri; import android.net.VpnService; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.preference.PreferenceManager; import android.support.v4.app.ActivityCompat; import android.support.v4.app.NotificationCompat; import android.support.v4.view.GestureDetectorCompat; import android.view.*; import android.webkit.CookieSyncManager; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.Button; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import com.google.analytics.tracking.android.EasyTracker; import com.google.analytics.tracking.android.GoogleAnalytics; import com.google.analytics.tracking.android.Tracker; import fq.router2.feedback.*; import fq.router2.life_cycle.*; import fq.router2.utils.*; import java.io.File; import java.lang.reflect.Method; public class MainActivity extends Activity implements LaunchedIntent.Handler, UpdateFoundIntent.Handler, ExitedIntent.Handler, DownloadingIntent.Handler, DownloadedIntent.Handler, DownloadFailedIntent.Handler, HandleFatalErrorIntent.Handler, DnsPollutedIntent.Handler, HandleAlertIntent.Handler, ExitingIntent.Handler, LaunchingIntent.Handler, SocksVpnConnectedIntent.Handler { public final static int SHOW_AS_ACTION_IF_ROOM = 1; private final static int ITEM_ID_EXIT = 1; private final static int ITEM_ID_REPORT_ERROR = 2; private final static int ITEM_ID_SETTINGS = 3; private final static int ITEM_ID_UPGRADE_MANUALLY = 4; private final static int ITEM_ID_CLEAN_DNS = 5; private final static int ITEM_ID_ABOUT = 6; private final static int ITEM_ID_OPEN_FULL_GOOGLE_PLAY = 7; private final static int ASK_VPN_PERMISSION = 1; public static boolean isReady; private Handler handler = new Handler(); private String upgradeUrl; private boolean downloaded; private static boolean dnsPollutionAcked = false; static { IOUtils.createCommonDirs(); } private GestureDetectorCompat gestureDetector; private String shareUrl; private Tracker gaTracker; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); PreferenceManager.setDefaultValues(this, R.xml.preferences, true); setTitle("fqrouter " + LaunchService.getMyVersion(this)); LaunchedIntent.register(this); LaunchingIntent.register(this); UpdateFoundIntent.register(this); ExitedIntent.register(this); DownloadingIntent.register(this); DownloadedIntent.register(this); DownloadFailedIntent.register(this); HandleFatalErrorIntent.register(this); DnsPollutedIntent.register(this); HandleAlertIntent.register(this); ExitingIntent.register(this); SocksVpnConnectedIntent.register(this); gestureDetector = new GestureDetectorCompat(this, new MyGestureDetector()); Button fullPowerButton = (Button) findViewById(R.id.fullPowerButton); GoogleAnalytics gaInstance = GoogleAnalytics.getInstance(MainActivity.this); gaTracker = gaInstance.getTracker("UA-37740383-2"); CookieSyncManager.createInstance(this); fullPowerButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { gaTracker.sendEvent("more-power", "click", "", new Long(0)); if (Build.VERSION.SDK_INT < 14) { Uri uri = Uri.parse("http://127.0.0.1:" + ConfigUtils.getHttpManagerPort()); startActivity(new Intent(Intent.ACTION_VIEW, uri)); } else { showWebView(); } } }); if (isReady) { onReady(); showWebView(); } else { LaunchService.execute(this); } } @Override public boolean onTouchEvent(MotionEvent event) { this.gestureDetector.onTouchEvent(event); return super.onTouchEvent(event); } @Override public void onStart() { super.onStart(); EasyTracker.getInstance().activityStart(this); } @Override public void onStop() { super.onStop(); EasyTracker.getInstance().activityStop(this); } @Override protected void onPause() { super.onPause(); if (isReady) { WebView webView = (WebView) findViewById(R.id.webView); webView.loadUrl("javascript:onPause()"); CookieSyncManager.getInstance().sync(); } } @Override protected void onResume() { super.onResume(); if (isReady) { CheckDnsPollutionService.execute(this); WebView webView = (WebView) findViewById(R.id.webView); webView.loadUrl("javascript:onResume()"); showWebView(); } } private void loadWebView() { if (Build.VERSION.SDK_INT < 14) { return; } WebView webView = (WebView) findViewById(R.id.webView); webView.getSettings().setJavaScriptEnabled(true); webView.getSettings().setAppCacheEnabled(false); webView.loadUrl("http://127.0.0.1:" + ConfigUtils.getHttpManagerPort() + "/home"); webView.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { LogUtils.i("url: " + url); startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url))); return true; } @Override public void onPageFinished(WebView view, String url) { CookieSyncManager.getInstance().sync(); } }); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { try { if (ASK_VPN_PERMISSION == requestCode) { if (resultCode == RESULT_OK) { if (LaunchService.SOCKS_VPN_SERVICE_CLASS == null) { onHandleFatalError("vpn class not loaded"); } else { updateStatus(_(R.string.status_launch_vpn), 80); stopService(new Intent(this, LaunchService.SOCKS_VPN_SERVICE_CLASS)); startService(new Intent(this, LaunchService.SOCKS_VPN_SERVICE_CLASS)); } } else { onHandleFatalError(_(R.string.status_vpn_rejected)); LogUtils.e("failed to start vpn service: " + resultCode); } } else { super.onActivityResult(requestCode, resultCode, data); } } catch (Exception e) { LogUtils.e("failed to handle onActivityResult", e); } } @Override public boolean onCreateOptionsMenu(Menu menu) { if (isReady) { menu.add(Menu.NONE, ITEM_ID_SETTINGS, Menu.NONE, R.string.menu_settings); menu.add(Menu.NONE, ITEM_ID_CLEAN_DNS, Menu.NONE, R.string.menu_clean_dns); menu.add(Menu.NONE, ITEM_ID_OPEN_FULL_GOOGLE_PLAY, Menu.NONE, R.string.menu_open_full_google_play); } if (upgradeUrl != null) { menu.add(Menu.NONE, ITEM_ID_UPGRADE_MANUALLY, Menu.NONE, R.string.menu_upgrade_manually); } menu.add(Menu.NONE, ITEM_ID_REPORT_ERROR, Menu.NONE, R.string.menu_report_error); menu.add(Menu.NONE, ITEM_ID_ABOUT, Menu.NONE, R.string.menu_about); addMenuItem(menu, ITEM_ID_EXIT, _(R.string.menu_exit)); return super.onCreateOptionsMenu(menu); } private MenuItem addMenuItem(Menu menu, int menuItemId, String caption) { MenuItem menuItem = menu.add(Menu.NONE, menuItemId, Menu.NONE, caption); try { Method method = MenuItem.class.getMethod("setShowAsAction", int.class); try { method.invoke(menuItem, SHOW_AS_ACTION_IF_ROOM); } catch (Exception e) { } } catch (NoSuchMethodException e) { } return menuItem; } @Override public boolean onMenuItemSelected(int featureId, MenuItem item) { if (ITEM_ID_EXIT == item.getItemId()) { exit(); } else if (ITEM_ID_REPORT_ERROR == item.getItemId()) { new ErrorReportEmail(this).send(); } else if (ITEM_ID_SETTINGS == item.getItemId()) { startActivity(new Intent(this, MainSettingsActivity.class)); } else if (ITEM_ID_UPGRADE_MANUALLY == item.getItemId()) { startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(upgradeUrl))); } else if (ITEM_ID_CLEAN_DNS == item.getItemId()) { showDnsPollutedAlert(); } else if (ITEM_ID_ABOUT == item.getItemId()) { openAbout(); } else if (ITEM_ID_OPEN_FULL_GOOGLE_PLAY == item.getItemId()) { openFullGooglePlay(); } return super.onMenuItemSelected(featureId, item); } private void openAbout() { WebView web = new WebView(this); web.loadUrl(_(R.string.about_page)); web.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url))); return true; } }); new AlertDialog.Builder(this) .setTitle(String.format(_(R.string.about_info_title), LaunchService.getMyVersion(this))) .setCancelable(false) .setPositiveButton(R.string.about_info_share, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { Intent intent = new Intent(android.content.Intent.ACTION_SEND); intent.setType("text/plain"); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); intent.putExtra(Intent.EXTRA_SUBJECT, _(R.string.share_subject)); if (null == shareUrl || shareUrl.trim().isEmpty()) { shareUrl = "https://s3-ap-southeast-1.amazonaws.com/fqrouter/fqrouter-latest.apk"; } intent.putExtra(Intent.EXTRA_TEXT, String.format(_(R.string.share_content), shareUrl)); startActivity(Intent.createChooser(intent, _(R.string.share_channel))); } }) .setNegativeButton(R.string.about_info_close, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { dialogInterface.cancel(); } }) .setView(web) .create() .show(); new Thread(new Runnable() { @Override public void run() { try { shareUrl = DnsUtils.resolveTXT(_(R.string.share_url_domain)); } catch (Exception e) { LogUtils.e("failed to resolve share url"); } } }).start(); } public static void displayNotification(Context context, String text) { if (!PreferenceManager.getDefaultSharedPreferences(context).getBoolean("NotificationEnabled", true)) { clearNotification(context); return; } if (LaunchService.isVpnRunning(context)) { clearNotification(context); return; } try { Intent openIntent = new Intent(context, MainActivity.class); Notification notification = new NotificationCompat.Builder(context) .setSmallIcon(R.drawable.icon) .setContentTitle(context.getResources().getString(R.string.notification_title)) .setContentText(text) .setContentIntent(PendingIntent.getActivity(context, 0, openIntent, 0)) .addAction( android.R.drawable.ic_menu_close_clear_cancel, context.getResources().getString(R.string.menu_exit), PendingIntent.getService(context, 0, new Intent(context, ExitService.class), 0)) .addAction( android.R.drawable.ic_menu_manage, context.getResources().getString(R.string.menu_status), PendingIntent.getActivity(context, 0, new Intent(context, MainActivity.class), 0)) .build(); NotificationManager notificationManager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE); notification.flags |= Notification.FLAG_ONGOING_EVENT; notificationManager.notify(1983, notification); } catch (Exception e) { LogUtils.e("failed to display notification " + text, e); } } public static void clearNotification(Context context) { try { NotificationManager notificationManager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE); notificationManager.cancel(1983); } catch (Exception e) { LogUtils.e("failed to clear notification", e); } } public void updateStatus(String status, int progress) { LogUtils.i(status); TextView textView = (TextView) findViewById(R.id.statusTextView); textView.setText(status); ProgressBar progressBar = (ProgressBar) findViewById(R.id.progressBar); progressBar.setProgress(progress); } public void exit() { if (LaunchService.isVpnRunning(this)) { Toast.makeText(this, R.string.vpn_exit_hint, 5000).show(); return; } ExitService.execute(this); displayNotification(this, _(R.string.status_exiting)); } private String _(int id) { return getResources().getString(id); } @Override public void onLaunched(boolean isVpnMode) { ActivityCompat.invalidateOptionsMenu(this); if (isVpnMode) { updateStatus(_(R.string.status_acquire_vpn_permission), 75); clearNotification(this); if (LaunchService.isVpnRunning(this)) { onReady(); } else { startVpn(); } } else { onReady(); } } public void onReady() { isReady = true; ActivityCompat.invalidateOptionsMenu(this); updateStatus(_(R.string.status_ready), 100); displayNotification(this, _(R.string.status_ready)); findViewById(R.id.progressBar).setVisibility(View.GONE); findViewById(R.id.hintTextView).setVisibility(View.VISIBLE); findViewById(R.id.fullPowerButton).setVisibility(View.VISIBLE); loadWebView(); checkUpdate(); } private void checkUpdate() { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); boolean AutoUpdateEnabled = preferences.getBoolean("AutoUpdateEnabled", true); if (AutoUpdateEnabled && upgradeUrl == null) { CheckUpdateService.execute(this); } } @Override public void onUpdateFound(String latestVersion, final String upgradeUrl) { final String downloadTo = "/sdcard/fqrouter-latest.apk"; if (downloaded) { onDownloaded(upgradeUrl, downloadTo); return; } this.upgradeUrl = upgradeUrl; ActivityCompat.invalidateOptionsMenu(this); new AlertDialog.Builder(this) .setIcon(android.R.drawable.ic_dialog_alert) .setTitle(R.string.new_version_alert_title) .setMessage(R.string.new_version_alert_message) .setPositiveButton(R.string.new_version_alert_yes, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { updateStatus(_(R.string.status_downloading) + " " + Uri.parse(upgradeUrl).getLastPathSegment(), 5); DownloadService.execute( MainActivity.this, upgradeUrl, downloadTo); } }) .setNegativeButton(R.string.new_version_alert_no, null) .show(); } @Override public void onExited() { clearNotification(this); finish(); } @Override public void onDownloadFailed(final String url, String downloadTo) { ActivityCompat.invalidateOptionsMenu(this); onHandleFatalError(_(R.string.status_download_failed) + " " + Uri.parse(url).getLastPathSegment()); Toast.makeText(this, R.string.upgrade_via_browser_hint, 3000).show(); handler.postDelayed(new Runnable() { @Override public void run() { startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url))); } }, 3000); } @Override public void onDownloaded(String url, String downloadTo) { downloaded = true; ActivityCompat.invalidateOptionsMenu(this); updateStatus(_(R.string.status_downloaded) + " " + Uri.parse(url).getLastPathSegment(), 5); onExiting(); try { ManagerProcess.kill(); } catch (Exception e) { LogUtils.e("failed to kill manager", e); } ApkUtils.install(this, downloadTo); } @Override public void onDownloading(String url, String downloadTo, int percent) { if (System.currentTimeMillis() % (2 * 1000) == 0) { displayNotification(this, _(R.string.status_downloading) + " " + Uri.parse(url).getLastPathSegment() + ": " + percent + "%"); } TextView textView = (TextView) findViewById(R.id.statusTextView); textView.setText(_(R.string.status_downloaded) + " " + percent + "%"); } private void startVpn() { if (LaunchService.isVpnRunning(this)) { LogUtils.e("vpn is already running, do not start it again"); return; } String[] fds = new File("/proc/self/fd").list(); if (null == fds) { LogUtils.e("failed to list /proc/self/fd"); onHandleFatalError(_(R.string.status_vpn_rejected)); return; } if (fds.length > 500) { LogUtils.e("too many fds before start: " + fds.length); onHandleFatalError(_(R.string.status_vpn_rejected)); return; } Intent intent = VpnService.prepare(MainActivity.this); if (intent == null) { onActivityResult(ASK_VPN_PERMISSION, RESULT_OK, null); } else { startActivityForResult(intent, ASK_VPN_PERMISSION); } } @Override public void onHandleFatalError(String message) { LogUtils.e("fatal error: " + message); findViewById(R.id.progressBar).setVisibility(View.GONE); TextView statusTextView = (TextView) findViewById(R.id.statusTextView); statusTextView.setTextColor(Color.RED); statusTextView.setText(message); checkUpdate(); } @Override public void onDnsPolluted(final long pollutedAt) { if (!dnsPollutionAcked) { showDnsPollutedAlert(); } } private void showDnsPollutedAlert() { new AlertDialog.Builder(MainActivity.this) .setIcon(android.R.drawable.ic_dialog_alert) .setTitle(R.string.dns_polluted_alert_title) .setMessage(R.string.dns_polluted_alert_message) .setPositiveButton(R.string.dns_polluted_alert_fix, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { dnsPollutionAcked = true; new Thread(new Runnable() { @Override public void run() { try { AirplaneModeUtils.toggle(MainActivity.this); showToast(R.string.dns_polluted_alert_toggle_succeed); } catch (Exception e) { LogUtils.e("failed to toggle airplane mode", e); showToast(R.string.dns_polluted_alert_toggle_failed); } } }).start(); } }) .setNegativeButton(R.string.dns_polluted_alert_ack, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { dnsPollutionAcked = true; } }) .show(); } private void showToast(final int message) { handler.postDelayed(new Runnable() { @Override public void run() { Toast.makeText(MainActivity.this, message, 5000).show(); } }, 0); } @Override public void onExiting() { displayNotification(this, _(R.string.status_exiting)); isReady = false; ActivityCompat.invalidateOptionsMenu(this); findViewById(R.id.webView).setVisibility(View.GONE); findViewById(R.id.progressBar).setVisibility(View.GONE); findViewById(R.id.hintTextView).setVisibility(View.GONE); findViewById(R.id.fullPowerButton).setVisibility(View.GONE); findViewById(R.id.statusTextView).setVisibility(View.VISIBLE); TextView statusTextView = (TextView) findViewById(R.id.statusTextView); statusTextView.setText(_(R.string.status_exiting)); } private class MyGestureDetector extends GestureDetector.SimpleOnGestureListener { private static final int SWIPE_MIN_DISTANCE = 120; private static final int SWIPE_MAX_OFF_PATH = 250; private static final int SWIPE_THRESHOLD_VELOCITY = 200; @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { try { if (Math.abs(e1.getY() - e2.getY()) > SWIPE_MAX_OFF_PATH) return false; // right to left swipe if (e1.getX() - e2.getX() > SWIPE_MIN_DISTANCE && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) { gaTracker.sendEvent("more-power", "swipe", "", new Long(0)); showWebView(); } } catch (Exception e) { LogUtils.e("failed to swipe", e); } return false; } } private void showWebView() { if (Build.VERSION.SDK_INT < 14) { return; } if (isReady) { final TextView statusTextView = (TextView) findViewById(R.id.statusTextView); statusTextView.setText(R.string.status_loading_page); statusTextView.setVisibility(View.VISIBLE); handler.postDelayed(new Runnable() { @Override public void run() { statusTextView.setVisibility(View.GONE); } }, 2000); findViewById(R.id.progressBar).setVisibility(View.GONE); findViewById(R.id.hintTextView).setVisibility(View.GONE); findViewById(R.id.fullPowerButton).setVisibility(View.GONE); findViewById(R.id.webView).setVisibility(View.VISIBLE); } } private void openFullGooglePlay() { if (ShellUtils.isRooted()) { new AlertDialog.Builder(this) .setIcon(android.R.drawable.ic_dialog_info) .setTitle(R.string.full_google_play_title) .setMessage(R.string.full_google_play_message) .setPositiveButton(R.string.full_google_play_open, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { killGooglePlay(); openGooglePlay(); } }) .setNegativeButton(R.string.full_google_play_clean_and_open, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { killGooglePlay(); try { ShellUtils.sudo("/data/data/fq.router2/busybox", "rm", "-rf", "/data/data/com.android.vending/*"); } catch (Exception e) { LogUtils.e("failed to clear google play data", e); showToast(R.string.failed_to_open_google_play); return; } openGooglePlay(); } }) .show(); } else { new AlertDialog.Builder(this) .setIcon(android.R.drawable.ic_dialog_info) .setTitle(R.string.stop_google_play_title) .setMessage(R.string.stop_google_play_message) .setPositiveButton(R.string.stop_google_play_done, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { openGooglePlay(); } }) .setNegativeButton(R.string.stop_google_play_do, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { ApkUtils.showInstalledAppDetails(MainActivity.this, "com.android.vending"); } }) .show(); } } private void killGooglePlay() { try { ShellUtils.sudo("/data/data/fq.router2/busybox", "killall", "com.android.vending"); } catch (Exception e) { LogUtils.e("failed to stop google play", e); } } private void openGooglePlay() { new Thread(new Runnable() { @Override public void run() { try { HttpUtils.post("http://127.0.0.1:" + ConfigUtils.getHttpManagerPort() + "/force-us-ip"); Intent LaunchIntent = getPackageManager().getLaunchIntentForPackage("com.android.vending"); startActivity(LaunchIntent); } catch (Exception e) { LogUtils.e("failed to open google play", e); showToast(R.string.failed_to_open_google_play); } } }).start(); } }