├── .gitignore ├── .idea ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── gradle.xml ├── misc.xml ├── modules.xml └── runConfigurations.xml ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── mogic │ │ └── toad │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── mogic │ │ │ └── toad │ │ │ ├── MainActivity.java │ │ │ ├── ToadBaseWebActivity.java │ │ │ ├── ToadHosts.java │ │ │ ├── ToadJavascriptInterface.java │ │ │ ├── ToadStringMap.java │ │ │ ├── ToadStringSet.java │ │ │ ├── ToadUrlInterceptor.java │ │ │ ├── ToadWebActivity.java │ │ │ ├── ToadWebViewProxyUtil.java │ │ │ └── proxyhandler │ │ │ ├── ConnectionHandler.java │ │ │ ├── IProxyListener.java │ │ │ ├── ProxyServer.java │ │ │ └── SocketConnect.java │ └── res │ │ ├── drawable-hdpi │ │ ├── toad_back_button.png │ │ ├── toad_forward_button.png │ │ ├── toad_left_button.png │ │ └── toad_right_button.png │ │ ├── drawable-mdpi │ │ ├── toad_back_button.png │ │ ├── toad_forward_button.png │ │ ├── toad_left_button.png │ │ └── toad_right_button.png │ │ ├── drawable-xhdpi │ │ ├── toad_back_button.png │ │ ├── toad_forward_button.png │ │ ├── toad_left_button.png │ │ └── toad_right_button.png │ │ ├── drawable-xxhdpi │ │ ├── toad_back_button.png │ │ ├── toad_forward_button.png │ │ ├── toad_left_button.png │ │ └── toad_right_button.png │ │ ├── layout │ │ └── toad_web_activity.xml │ │ ├── menu │ │ ├── toad_left_menu.xml │ │ └── toad_right_menu.xml │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── mogic │ └── toad │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion "25.0.3" 6 | useLibrary 'org.apache.http.legacy' 7 | defaultConfig { 8 | applicationId "mogic.toad" 9 | minSdkVersion 9 10 | targetSdkVersion 25 11 | versionCode 2 12 | versionName "1.0.1" 13 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 14 | } 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | } 22 | 23 | dependencies { 24 | compile fileTree(dir: 'libs', include: ['*.jar']) 25 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 26 | exclude group: 'com.android.support', module: 'support-annotations' 27 | }) 28 | compile 'com.android.support:appcompat-v7:25.3.1' 29 | compile 'com.android.support:design:25.3.1' 30 | compile 'com.android.support.constraint:constraint-layout:1.0.0-alpha7' 31 | testCompile 'junit:junit:4.12' 32 | compile 'com.just.agentweb:agentweb:4.0.2' 33 | compile 'com.just.agentweb:download:4.0.2' 34 | compile 'com.just.agentweb:filechooser:4.0.2' 35 | } 36 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in E:\Android\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /app/src/androidTest/java/mogic/toad/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package mogic.toad; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("mogic.toad", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/java/mogic/toad/MainActivity.java: -------------------------------------------------------------------------------- 1 | package mogic.toad; 2 | 3 | public class MainActivity extends ToadWebActivity { 4 | } 5 | -------------------------------------------------------------------------------- /app/src/main/java/mogic/toad/ToadBaseWebActivity.java: -------------------------------------------------------------------------------- 1 | package mogic.toad; 2 | 3 | import android.os.Bundle; 4 | import android.os.Build; 5 | import android.content.Context; 6 | import android.support.design.widget.CoordinatorLayout; 7 | import android.support.design.widget.AppBarLayout; 8 | import android.support.v7.app.AppCompatActivity; 9 | import android.support.v7.widget.Toolbar; 10 | import android.support.v7.widget.AppCompatImageButton; 11 | import android.support.v7.widget.PopupMenu; 12 | import android.support.v4.view.ViewCompat; 13 | import android.widget.TextView; 14 | import android.widget.Toast; 15 | import android.view.MenuItem; 16 | import android.view.View; 17 | import android.view.KeyEvent; 18 | import android.view.WindowManager; 19 | import android.view.Display; 20 | import android.graphics.Point; 21 | import android.graphics.Color; 22 | import android.util.DisplayMetrics; 23 | import android.webkit.WebView; 24 | import android.webkit.WebViewClient; 25 | import android.webkit.WebChromeClient; 26 | import android.webkit.WebSettings; 27 | 28 | import com.just.agentweb.AgentWeb; 29 | import com.just.agentweb.NestedScrollAgentWebView; 30 | 31 | public class ToadBaseWebActivity extends AppCompatActivity implements View.OnClickListener, PopupMenu.OnMenuItemClickListener { 32 | protected CoordinatorLayout mCoordinatorLayout; 33 | protected AppBarLayout mAppBarLayout; 34 | protected Toolbar mToolbar; 35 | protected TextView mTitleTextView; 36 | protected AppCompatImageButton mLeftButton; 37 | protected AppCompatImageButton mRightButton; 38 | protected AppCompatImageButton mForwardButton; 39 | protected AppCompatImageButton mBackButton; 40 | protected PopupMenu mLeftPopupMenu; 41 | protected PopupMenu mRightPopupMenu; 42 | 43 | protected int mIndicatorColor; 44 | protected int mIndicatorHeight; 45 | 46 | protected AgentWeb mAgentWeb; 47 | protected AgentWeb.CommonBuilder mAgentWebBuilder; 48 | 49 | protected ToadWebView mWebView; 50 | 51 | protected WebSettings mWebSettings; 52 | protected String mDefaultUserAgent; 53 | protected boolean mIsFullscreen; 54 | 55 | public class ToadWebView extends NestedScrollAgentWebView { 56 | public ToadWebView(Context context) { 57 | super(context); 58 | } 59 | } 60 | 61 | public class ToadWebViewClient extends WebViewClient { 62 | } 63 | 64 | public class ToadWebChromeClient extends WebChromeClient { 65 | @Override 66 | public void onReceivedTitle(WebView view, String title) { 67 | super.onReceivedTitle(view, title); 68 | mTitleTextView.setText(title); 69 | } 70 | } 71 | 72 | protected ToadWebView createWebView(Context context) { 73 | return new ToadWebView(context); 74 | } 75 | 76 | protected ToadWebViewClient createWebViewClient() { 77 | return new ToadWebViewClient(); 78 | } 79 | 80 | protected ToadWebChromeClient createWebChromeClient() { 81 | return new ToadWebChromeClient(); 82 | } 83 | 84 | @Override 85 | protected void onCreate(Bundle savedInstanceState) { 86 | super.onCreate(savedInstanceState); 87 | setContentView(R.layout.toad_web_activity); 88 | initView(); 89 | createAgentWeb(); 90 | } 91 | 92 | protected void initView() { 93 | mCoordinatorLayout = (CoordinatorLayout) findViewById(R.id.main); 94 | mAppBarLayout = (AppBarLayout) findViewById(R.id.app_bar); 95 | mToolbar = (Toolbar) findViewById(R.id.toolbar); 96 | mTitleTextView = (TextView) findViewById(R.id.title); 97 | mTitleTextView.setMaxWidth(getDisplayWidth() - dpToPx(220)); 98 | mLeftButton = (AppCompatImageButton) findViewById(R.id.left_button); 99 | mRightButton = (AppCompatImageButton) findViewById(R.id.right_button); 100 | mForwardButton = (AppCompatImageButton) findViewById(R.id.forward_button); 101 | mBackButton = (AppCompatImageButton) findViewById(R.id.back_button); 102 | mLeftButton.setOnClickListener(this); 103 | mRightButton.setOnClickListener(this); 104 | mForwardButton.setOnClickListener(this); 105 | mBackButton.setOnClickListener(this); 106 | mIndicatorColor = Color.parseColor("#ff0000"); 107 | mIndicatorHeight = 2; 108 | 109 | if (mLeftPopupMenu == null) { 110 | mLeftPopupMenu = new PopupMenu(this, mLeftButton); 111 | mLeftPopupMenu.inflate(R.menu.toad_left_menu); 112 | mLeftPopupMenu.setOnMenuItemClickListener(this); 113 | } 114 | if (mRightPopupMenu == null) { 115 | mRightPopupMenu = new PopupMenu(this, mRightButton); 116 | mRightPopupMenu.inflate(R.menu.toad_right_menu); 117 | mRightPopupMenu.setOnMenuItemClickListener(this); 118 | } 119 | } 120 | 121 | protected void beforeCreateAgentWeb() { 122 | mWebView = createWebView(this); 123 | 124 | mWebSettings = mWebView.getSettings(); 125 | mDefaultUserAgent = mWebSettings.getUserAgentString(); 126 | 127 | CoordinatorLayout.LayoutParams params = new CoordinatorLayout.LayoutParams(-1, -1); 128 | params.setBehavior(new AppBarLayout.ScrollingViewBehavior()); 129 | 130 | mAgentWebBuilder = AgentWeb.with(this) 131 | .setAgentWebParent(mCoordinatorLayout, 1, params) 132 | .useDefaultIndicator(mIndicatorColor, mIndicatorHeight) 133 | .setWebView(mWebView) 134 | .setWebViewClient(createWebViewClient()) 135 | .setWebChromeClient(createWebChromeClient()); 136 | } 137 | 138 | protected void createAgentWeb() { 139 | beforeCreateAgentWeb(); 140 | mAgentWeb = mAgentWebBuilder.createAgentWeb().ready().go(null); 141 | mWebSettings.setUserAgentString(mDefaultUserAgent); 142 | afterCreateAgentWeb(); 143 | } 144 | 145 | protected void afterCreateAgentWeb() { 146 | } 147 | 148 | public void enterFullscreen() { 149 | mWebView.setNestedScrollingEnabled(false); 150 | mAppBarLayout.setExpanded(false, true); 151 | mIsFullscreen = true; 152 | } 153 | 154 | public void exitFullScreen() { 155 | mAppBarLayout.setExpanded(true, true); 156 | mWebView.setNestedScrollingEnabled(true); 157 | mIsFullscreen = false; 158 | } 159 | 160 | public void enableDesktopMode() { 161 | mWebSettings.setUserAgentString(mDefaultUserAgent.replace("Android", "").replace("Mobile", "")); 162 | } 163 | 164 | public void disableDesktopMode() { 165 | mWebSettings.setUserAgentString(mDefaultUserAgent); 166 | } 167 | 168 | public void copyUrlToClipboard() { 169 | String url = mWebView.getUrl(); 170 | setClipboard(getApplicationContext(), url); 171 | Toast.makeText(getApplicationContext(), url, Toast.LENGTH_SHORT).show(); 172 | } 173 | 174 | public void exit() { 175 | System.exit(0); 176 | } 177 | 178 | @Override 179 | protected void onDestroy() { 180 | super.onDestroy(); 181 | mAgentWeb.getWebLifeCycle().onDestroy(); 182 | } 183 | 184 | @Override 185 | public void onClick(View view) { 186 | switch (view.getId()) { 187 | case R.id.back_button: 188 | if (mAgentWeb.getWebCreator().getWebView().canGoBack()) { 189 | mAgentWeb.back(); 190 | } 191 | break; 192 | case R.id.forward_button: 193 | if (mAgentWeb.getWebCreator().getWebView().canGoForward()) { 194 | mAgentWeb.getWebCreator().getWebView().goForward(); 195 | } 196 | break; 197 | case R.id.left_button: 198 | mLeftPopupMenu.show(); 199 | break; 200 | case R.id.right_button: 201 | mRightPopupMenu.show(); 202 | break; 203 | default: 204 | break; 205 | } 206 | } 207 | 208 | @Override 209 | public boolean onMenuItemClick(MenuItem item) { 210 | switch (item.getItemId()) { 211 | case R.id.menu_item_refresh: 212 | mAgentWeb.getUrlLoader().reload(); 213 | return true; 214 | case R.id.menu_item_fullscreen: 215 | enterFullscreen(); 216 | return true; 217 | case R.id.menu_item_desktop_mode: 218 | if (!item.isChecked()) { 219 | enableDesktopMode(); 220 | item.setChecked(true); 221 | } else { 222 | disableDesktopMode(); 223 | item.setChecked(false); 224 | } 225 | return true; 226 | case R.id.menu_item_copy_url: 227 | copyUrlToClipboard(); 228 | return true; 229 | case R.id.menu_item_exit: 230 | exit(); 231 | return true; 232 | default: 233 | return false; 234 | } 235 | } 236 | 237 | @Override 238 | public boolean onKeyDown(int keyCode, KeyEvent event) { 239 | if (keyCode == KeyEvent.KEYCODE_BACK) { 240 | if (mIsFullscreen == true) { 241 | exitFullScreen(); 242 | return true; 243 | } 244 | } 245 | if (mAgentWeb.handleKeyEvent(keyCode, event)) { 246 | return true; 247 | } 248 | return super.onKeyDown(keyCode, event); 249 | } 250 | 251 | @Override 252 | protected void onPause() { 253 | mAgentWeb.getWebLifeCycle().onPause(); 254 | super.onPause(); 255 | } 256 | 257 | @Override 258 | protected void onResume() { 259 | mAgentWeb.getWebLifeCycle().onResume(); 260 | super.onResume(); 261 | } 262 | 263 | private void setClipboard(Context context, String text) { 264 | if(android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB) { 265 | android.text.ClipboardManager clipboard = (android.text.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); 266 | clipboard.setText(text); 267 | } else { 268 | android.content.ClipboardManager clipboard = (android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); 269 | android.content.ClipData clip = android.content.ClipData.newPlainText("Copied Text", text); 270 | clipboard.setPrimaryClip(clip); 271 | } 272 | } 273 | 274 | private DisplayMetrics getDisplayMetrics() { 275 | Context context = this.getApplicationContext(); 276 | return context.getResources().getDisplayMetrics(); 277 | } 278 | 279 | private int dpToPx(int dp) { 280 | return (int) (dp * getDisplayMetrics().density + 0.5f); 281 | } 282 | 283 | private int pxToDp(int px) { 284 | return (int) (px / getDisplayMetrics().density + 0.5f); 285 | } 286 | 287 | private Display getDefaultDisplay() { 288 | Context context = this.getApplicationContext(); 289 | WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 290 | return windowManager.getDefaultDisplay(); 291 | } 292 | 293 | private int getDisplayWidth() { 294 | Display display = getDefaultDisplay(); 295 | if (Build.VERSION.SDK_INT >= 13) { 296 | Point size = new Point(); 297 | display.getSize(size); 298 | return size.x; 299 | } else { 300 | return display.getWidth(); 301 | } 302 | } 303 | 304 | private int getDisplayHeight() { 305 | Display display = getDefaultDisplay(); 306 | if (Build.VERSION.SDK_INT >= 13) { 307 | Point size = new Point(); 308 | display.getSize(size); 309 | return size.y; 310 | } else { 311 | return display.getHeight(); 312 | } 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /app/src/main/java/mogic/toad/ToadHosts.java: -------------------------------------------------------------------------------- 1 | package mogic.toad; 2 | 3 | public class ToadHosts { 4 | private static ToadStringMap hosts = new ToadStringMap(); 5 | 6 | public static String lookup(String host) { 7 | return hosts.lookup(host); 8 | } 9 | 10 | public static String get(String key) { 11 | return hosts.get(key); 12 | } 13 | 14 | public static void set(String key, String value) { 15 | hosts.set(key, value); 16 | } 17 | 18 | public static int size() { 19 | return hosts.size(); 20 | } 21 | 22 | public static void remove(String key) { 23 | hosts.remove(key); 24 | } 25 | 26 | public static void removeMatched(String host) { 27 | hosts.removeMatched(host); 28 | } 29 | 30 | public static void clear() { 31 | hosts.clear(); 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/mogic/toad/ToadJavascriptInterface.java: -------------------------------------------------------------------------------- 1 | package mogic.toad; 2 | 3 | import android.webkit.JavascriptInterface; 4 | 5 | public class ToadJavascriptInterface { 6 | public static class BaseObject { 7 | protected ToadWebActivity mActivity; 8 | 9 | public BaseObject(ToadWebActivity activity) { 10 | mActivity = activity; 11 | } 12 | 13 | public boolean securityCheck() { 14 | return mActivity.isTrustedPage(); 15 | } 16 | } 17 | 18 | public static class UrlWhitelist extends BaseObject { 19 | public UrlWhitelist(ToadWebActivity activity) { 20 | super(activity); 21 | } 22 | 23 | @JavascriptInterface 24 | public boolean match(String url) { 25 | if (!securityCheck()) 26 | return false; 27 | return ToadUrlInterceptor.Whitelist.match(url); 28 | } 29 | 30 | @JavascriptInterface 31 | public boolean contain(String key) { 32 | if (!securityCheck()) 33 | return false; 34 | return ToadUrlInterceptor.Whitelist.contain(key); 35 | } 36 | 37 | @JavascriptInterface 38 | public void add(String key) { 39 | if (!securityCheck()) 40 | return; 41 | ToadUrlInterceptor.Whitelist.add(key); 42 | } 43 | 44 | @JavascriptInterface 45 | public int size() { 46 | if (!securityCheck()) 47 | return 0; 48 | return ToadUrlInterceptor.Whitelist.size(); 49 | } 50 | 51 | @JavascriptInterface 52 | public void remove(String key) { 53 | if (!securityCheck()) 54 | return; 55 | ToadUrlInterceptor.Whitelist.remove(key); 56 | } 57 | 58 | @JavascriptInterface 59 | public void removeUrls(String url) { 60 | if (!securityCheck()) 61 | return; 62 | ToadUrlInterceptor.Whitelist.removeMatched(url); 63 | } 64 | 65 | @JavascriptInterface 66 | public void clear() { 67 | if (!securityCheck()) 68 | return; 69 | ToadUrlInterceptor.Whitelist.clear(); 70 | } 71 | } 72 | 73 | public static class UrlBlacklist extends BaseObject { 74 | public UrlBlacklist(ToadWebActivity activity) { 75 | super(activity); 76 | } 77 | 78 | @JavascriptInterface 79 | public boolean match(String url) { 80 | if (!securityCheck()) 81 | return false; 82 | return ToadUrlInterceptor.Blacklist.match(url); 83 | } 84 | 85 | @JavascriptInterface 86 | public boolean contain(String key) { 87 | if (!securityCheck()) 88 | return false; 89 | return ToadUrlInterceptor.Blacklist.contain(key); 90 | } 91 | 92 | @JavascriptInterface 93 | public void add(String key) { 94 | if (!securityCheck()) 95 | return; 96 | ToadUrlInterceptor.Blacklist.add(key); 97 | } 98 | 99 | @JavascriptInterface 100 | public int size() { 101 | if (!securityCheck()) 102 | return 0; 103 | return ToadUrlInterceptor.Blacklist.size(); 104 | } 105 | 106 | @JavascriptInterface 107 | public void remove(String key) { 108 | if (!securityCheck()) 109 | return; 110 | ToadUrlInterceptor.Blacklist.remove(key); 111 | } 112 | 113 | @JavascriptInterface 114 | public void removeUrls(String url) { 115 | if (!securityCheck()) 116 | return; 117 | ToadUrlInterceptor.Blacklist.removeMatched(url); 118 | } 119 | 120 | @JavascriptInterface 121 | public void clear() { 122 | if (!securityCheck()) 123 | return; 124 | ToadUrlInterceptor.Blacklist.clear(); 125 | } 126 | } 127 | 128 | public static class Hosts extends BaseObject { 129 | public Hosts(ToadWebActivity activity) { 130 | super(activity); 131 | } 132 | 133 | @JavascriptInterface 134 | public String lookup(String host) { 135 | if (!securityCheck()) 136 | return null; 137 | return ToadHosts.lookup(host); 138 | } 139 | 140 | @JavascriptInterface 141 | public String get(String key) { 142 | if (!securityCheck()) 143 | return null; 144 | return ToadHosts.get(key); 145 | } 146 | 147 | @JavascriptInterface 148 | public void set(String key, String value) { 149 | if (!securityCheck()) 150 | return; 151 | ToadHosts.set(key, value); 152 | } 153 | 154 | @JavascriptInterface 155 | public int size() { 156 | if (!securityCheck()) 157 | return 0; 158 | return ToadHosts.size(); 159 | } 160 | 161 | @JavascriptInterface 162 | public void remove(String key) { 163 | if (!securityCheck()) 164 | return; 165 | ToadHosts.remove(key); 166 | } 167 | 168 | @JavascriptInterface 169 | public void removeHosts(String host) { 170 | if (!securityCheck()) 171 | return; 172 | ToadHosts.removeMatched(host); 173 | } 174 | 175 | @JavascriptInterface 176 | public void clear() { 177 | if (!securityCheck()) 178 | return; 179 | ToadHosts.clear(); 180 | } 181 | } 182 | 183 | public static class Menu extends BaseObject { 184 | public Menu(ToadWebActivity activity) { 185 | super(activity); 186 | } 187 | 188 | @JavascriptInterface 189 | public void showItem(int id) { 190 | if (!securityCheck()) 191 | return; 192 | mActivity.setCustomMenuItemVisible(id, true); 193 | } 194 | 195 | @JavascriptInterface 196 | public void hideItem(int id) { 197 | if (!securityCheck()) 198 | return; 199 | mActivity.setCustomMenuItemVisible(id, false); 200 | } 201 | 202 | @JavascriptInterface 203 | public void setItem(int id, String text, String url) { 204 | if (!securityCheck()) 205 | return; 206 | mActivity.setCustomMenuItem(id, text, url); 207 | } 208 | 209 | @JavascriptInterface 210 | public void displayItem(int id, String text, String url) { 211 | if (!securityCheck()) 212 | return; 213 | mActivity.displayCustomMenuItem(id, text, url); 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /app/src/main/java/mogic/toad/ToadStringMap.java: -------------------------------------------------------------------------------- 1 | package mogic.toad; 2 | 3 | import java.util.concurrent.ConcurrentHashMap; 4 | import java.util.Iterator; 5 | 6 | public class ToadStringMap { 7 | private ConcurrentHashMap mHashMap; 8 | 9 | public ToadStringMap() { 10 | mHashMap = new ConcurrentHashMap(); 11 | } 12 | 13 | public String get(String key) { 14 | String value = mHashMap.get(key); 15 | if (value == null) 16 | return mHashMap.containsKey(key) ? "" : null; 17 | return value; 18 | } 19 | 20 | public void set(String key, String value) { 21 | if (key == null) 22 | return; 23 | mHashMap.put(key, value); 24 | } 25 | 26 | public void remove(String key) { 27 | mHashMap.remove(key); 28 | } 29 | 30 | public void clear() { 31 | mHashMap.clear(); 32 | } 33 | 34 | public int size() { 35 | return mHashMap.size(); 36 | } 37 | 38 | public String lookup(String fuzzyString) { 39 | if (fuzzyString == null) 40 | return null; 41 | String key; 42 | for (Iterator it = mHashMap.keySet().iterator(); it.hasNext();) { 43 | key = it.next(); 44 | if (fuzzyString.matches(key)) 45 | return get(key); 46 | } 47 | return null; 48 | } 49 | 50 | public void removeMatched(String fuzzyString) { 51 | if (fuzzyString == null) 52 | return; 53 | String key; 54 | for (Iterator it = mHashMap.keySet().iterator(); it.hasNext();) { 55 | key = it.next(); 56 | if (fuzzyString.matches(key)) 57 | mHashMap.remove(key); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/mogic/toad/ToadStringSet.java: -------------------------------------------------------------------------------- 1 | package mogic.toad; 2 | 3 | public class ToadStringSet { 4 | private ToadStringMap mMap; 5 | 6 | public ToadStringSet() { 7 | mMap = new ToadStringMap(); 8 | } 9 | 10 | public boolean contain(String key) { 11 | if (mMap.get(key) != null) 12 | return true; 13 | return false; 14 | } 15 | 16 | public void add(String key) { 17 | mMap.set(key, ""); 18 | } 19 | 20 | public void remove(String key) { 21 | mMap.remove(key); 22 | } 23 | 24 | public void clear() { 25 | mMap.clear(); 26 | } 27 | 28 | public int size() { 29 | return mMap.size(); 30 | } 31 | 32 | public boolean match(String fuzzyString) { 33 | String result = mMap.lookup(fuzzyString); 34 | if (result != null) 35 | return true; 36 | return false; 37 | } 38 | 39 | public void removeMatched(String fuzzyString) { 40 | mMap.removeMatched(fuzzyString); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/mogic/toad/ToadUrlInterceptor.java: -------------------------------------------------------------------------------- 1 | package mogic.toad; 2 | 3 | public class ToadUrlInterceptor { 4 | private static ToadStringSet whitelist = new ToadStringSet(); 5 | private static ToadStringSet blacklist = new ToadStringSet(); 6 | 7 | public static class Whitelist { 8 | public static boolean match(String url) { 9 | return whitelist.match(url); 10 | } 11 | 12 | public static boolean contain(String key) { 13 | return whitelist.contain(key); 14 | } 15 | 16 | public static void add(String key) { 17 | whitelist.add(key); 18 | } 19 | 20 | public static int size() { 21 | return whitelist.size(); 22 | } 23 | 24 | public static void remove(String key) { 25 | whitelist.remove(key); 26 | } 27 | 28 | public static void removeMatched(String url) { 29 | whitelist.removeMatched(url); 30 | } 31 | 32 | public static void clear() { 33 | whitelist.clear(); 34 | } 35 | } 36 | 37 | public static class Blacklist { 38 | public static boolean match(String url) { 39 | return blacklist.match(url); 40 | } 41 | 42 | public static boolean contain(String key) { 43 | return blacklist.contain(key); 44 | } 45 | 46 | public static void add(String key) { 47 | blacklist.add(key); 48 | } 49 | 50 | public static int size() { 51 | return blacklist.size(); 52 | } 53 | 54 | public static void remove(String key) { 55 | blacklist.remove(key); 56 | } 57 | 58 | public static void removeMatched(String url) { 59 | blacklist.removeMatched(url); 60 | } 61 | 62 | public static void clear() { 63 | blacklist.clear(); 64 | } 65 | } 66 | 67 | public static boolean intercept(String url) { 68 | if (whitelist.match(url)) 69 | return false; 70 | return blacklist.match(url); 71 | } 72 | } -------------------------------------------------------------------------------- /app/src/main/java/mogic/toad/ToadWebActivity.java: -------------------------------------------------------------------------------- 1 | package mogic.toad; 2 | 3 | import android.os.Build; 4 | import android.os.Handler; 5 | import android.os.Looper; 6 | import android.text.TextUtils; 7 | import android.view.Menu; 8 | import android.view.MenuItem; 9 | import android.webkit.WebView; 10 | import android.webkit.WebResourceRequest; 11 | import android.webkit.WebResourceResponse; 12 | import android.widget.Toast; 13 | import android.graphics.Bitmap; 14 | 15 | import mogic.toad.proxyhandler.ProxyServer; 16 | import mogic.toad.proxyhandler.IProxyListener; 17 | import mogic.toad.proxyhandler.ConnectionHandler; 18 | 19 | public class ToadWebActivity extends ToadBaseWebActivity implements IProxyListener { 20 | protected ProxyServer mProxyServer; 21 | protected MenuItem mCustomMenuItems[]; 22 | protected String mCustomMenuItemUrls[]; 23 | protected boolean mIsTrustedPage; 24 | protected ToadStringSet mTrustedUrls; 25 | 26 | protected String getInitialUrl() { 27 | return "https://hahamama.github.io/"; 28 | } 29 | 30 | protected void setDefaultCustomMenuItems() { 31 | displayCustomMenuItem(1, "起始页", getInitialUrl()); 32 | displayCustomMenuItem(2, "膜乎", "https://www.mohu.club/"); 33 | displayCustomMenuItem(3, "辱乎", "https://www.ruhu.ml/"); 34 | displayCustomMenuItem(4, "品葱", "https://www.pin-cong.com/"); 35 | displayCustomMenuItem(9, "无法正常使用?", "https://toadbucket.bitbucket.io/"); 36 | displayCustomMenuItem(10, "关于...", "https://github.com/toadapp/toad-android"); 37 | } 38 | 39 | protected void setDefaultHosts() { 40 | ToadHosts.set("(.+?)\\.netlify\\.com", "netlify.com"); 41 | ToadHosts.set("(.+?)\\.bitbucket\\.io", "bitbucket.io"); 42 | ToadHosts.set("(.+?)\\.github\\.io", "raw.githubusercontent.com"); 43 | ToadHosts.set("(.*\\.|)mohu\\.club", "1.0.0.1"); 44 | ToadHosts.set("(.*\\.|)ruhu\\.ml", "1.0.0.1"); 45 | ToadHosts.set("(.*\\.|)pin-cong\\.com", "1.0.0.1"); 46 | 47 | ToadHosts.set("(.*\\.|)googlesyndication\\.com", "0.0.0.0"); 48 | ToadHosts.set("(.*\\.|)google-analytics\\.com", "0.0.0.0"); 49 | ToadHosts.set("(.*\\.|)51\\.la", "0.0.0.0"); 50 | ToadHosts.set("(.*\\.|)51yes\\.com", "0.0.0.0"); 51 | ToadHosts.set("(.*\\.|)cnzz\\.com", "0.0.0.0"); 52 | ToadHosts.set("(.*\\.|)baidu\\.com", "0.0.0.0"); 53 | ToadHosts.set("cpro\\.baidustatic\\.com", "0.0.0.0"); 54 | } 55 | 56 | protected void setDefaultInterceptorUrls() { 57 | ToadUrlInterceptor.Blacklist.add("https://(www\\.)?mohu\\d?\\..+/matomo/piwik\\.js"); 58 | } 59 | 60 | protected void setDefaultTrustedUrls() { 61 | mTrustedUrls.add("https://toadbucket\\.bitbucket\\.io/.*"); 62 | mTrustedUrls.add("https://hahamama\\.github\\.io/.*"); 63 | mTrustedUrls.add("https://toadapp\\.github\\.io/.*"); 64 | } 65 | 66 | @Override 67 | protected void beforeCreateAgentWeb() { 68 | super.beforeCreateAgentWeb(); 69 | mIsTrustedPage = false; 70 | mTrustedUrls = new ToadStringSet(); 71 | initCustomMenuItems(); 72 | ConnectionHandler.setForceHttps(true); 73 | setDefaultHosts(); 74 | setDefaultInterceptorUrls(); 75 | setDefaultTrustedUrls(); 76 | } 77 | 78 | @Override 79 | protected void afterCreateAgentWeb() { 80 | mProxyServer = new ProxyServer(); 81 | mProxyServer.setCallback(this); 82 | mProxyServer.startServer(); 83 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { 84 | mWebView.addJavascriptInterface(new ToadJavascriptInterface.Hosts(this), "ToadHosts"); 85 | mWebView.addJavascriptInterface(new ToadJavascriptInterface.UrlWhitelist(this), "ToadUrlWhitelist"); 86 | mWebView.addJavascriptInterface(new ToadJavascriptInterface.UrlBlacklist(this), "ToadUrlBlacklist"); 87 | mWebView.addJavascriptInterface(new ToadJavascriptInterface.Menu(this), "ToadMenu"); 88 | } 89 | } 90 | 91 | @Override 92 | public void onReportProxyPort(int port) { 93 | ToadWebViewProxyUtil.setProxy(mWebView, "localhost", port); 94 | mWebView.loadUrl(getInitialUrl()); 95 | 96 | setDefaultCustomMenuItems(); 97 | } 98 | 99 | @Override 100 | protected ToadWebViewClient createWebViewClient() { 101 | return new ToadWebViewClient() { 102 | @Override 103 | public void onPageStarted(WebView view, String url, Bitmap favicon) { 104 | verifyPageUrl(url); 105 | super.onPageStarted(view, url, favicon); 106 | } 107 | 108 | @Deprecated 109 | @Override 110 | public WebResourceResponse shouldInterceptRequest(WebView view, String url) { 111 | if (!TextUtils.isEmpty(url) && ToadUrlInterceptor.intercept(url)) { 112 | return new WebResourceResponse(null, null, null); 113 | } 114 | return super.shouldInterceptRequest(view, url); 115 | } 116 | 117 | @Override 118 | public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { 119 | if (request.getUrl() != null) { 120 | String url = request.getUrl().toString(); 121 | if (!TextUtils.isEmpty(url) && ToadUrlInterceptor.intercept(url)) { 122 | return new WebResourceResponse(null, null, null); 123 | } 124 | } 125 | return super.shouldInterceptRequest(view, request); 126 | } 127 | }; 128 | } 129 | 130 | protected void verifyPageUrl(String url) { 131 | mIsTrustedPage = mTrustedUrls.match(url); 132 | } 133 | 134 | protected boolean isTrustedPage() { 135 | return mIsTrustedPage; 136 | } 137 | 138 | @Override 139 | public boolean onMenuItemClick(MenuItem item) { 140 | int id = fromCustomMenuItemId(item.getItemId()); 141 | if (id != -1) { 142 | customMenuItemClicked(id); 143 | return true; 144 | } 145 | return super.onMenuItemClick(item); 146 | } 147 | 148 | protected void customMenuItemClicked(int id) { 149 | if (id < 1 || id > 10) 150 | return; 151 | String url = mCustomMenuItemUrls[id - 1]; 152 | if (!TextUtils.isEmpty(url)) 153 | mWebView.loadUrl(url); 154 | } 155 | 156 | protected void initCustomMenuItems() { 157 | Menu rightMenu = mRightPopupMenu.getMenu(); 158 | mCustomMenuItems = new MenuItem[10]; 159 | mCustomMenuItemUrls = new String[10]; 160 | for (int i = 0; i < 10; i++) { 161 | mCustomMenuItems[i] = rightMenu.findItem(toCustomMenuItemId(i + 1)); 162 | mCustomMenuItemUrls[i] = null; 163 | } 164 | } 165 | 166 | protected void displayCustomMenuItem(int id, String text, String url) { 167 | setCustomMenuItem(id, text, url); 168 | setCustomMenuItemVisible(id, true); 169 | } 170 | 171 | protected void setCustomMenuItem(int id, String text, String url) { 172 | if (id < 1 || id > 10) 173 | return; 174 | mCustomMenuItems[id - 1].setTitle(text); 175 | mCustomMenuItemUrls[id - 1] = url; 176 | } 177 | 178 | protected void setCustomMenuItemVisible(int id, Boolean visible) { 179 | if (id < 1 || id > 10) 180 | return; 181 | mCustomMenuItems[id - 1].setVisible(visible); 182 | } 183 | 184 | protected int toCustomMenuItemId(int num) { 185 | switch (num) { 186 | case 1: 187 | return R.id.menu_item_custom_1; 188 | case 2: 189 | return R.id.menu_item_custom_2; 190 | case 3: 191 | return R.id.menu_item_custom_3; 192 | case 4: 193 | return R.id.menu_item_custom_4; 194 | case 5: 195 | return R.id.menu_item_custom_5; 196 | case 6: 197 | return R.id.menu_item_custom_6; 198 | case 7: 199 | return R.id.menu_item_custom_7; 200 | case 8: 201 | return R.id.menu_item_custom_8; 202 | case 9: 203 | return R.id.menu_item_custom_9; 204 | case 10: 205 | return R.id.menu_item_custom_10; 206 | } 207 | return -1; 208 | } 209 | 210 | protected int fromCustomMenuItemId(int menuItemId) { 211 | switch (menuItemId) { 212 | case R.id.menu_item_custom_1: 213 | return 1; 214 | case R.id.menu_item_custom_2: 215 | return 2; 216 | case R.id.menu_item_custom_3: 217 | return 3; 218 | case R.id.menu_item_custom_4: 219 | return 4; 220 | case R.id.menu_item_custom_5: 221 | return 5; 222 | case R.id.menu_item_custom_6: 223 | return 6; 224 | case R.id.menu_item_custom_7: 225 | return 7; 226 | case R.id.menu_item_custom_8: 227 | return 8; 228 | case R.id.menu_item_custom_9: 229 | return 9; 230 | case R.id.menu_item_custom_10: 231 | return 10; 232 | } 233 | return -1; 234 | } 235 | 236 | } -------------------------------------------------------------------------------- /app/src/main/java/mogic/toad/ToadWebViewProxyUtil.java: -------------------------------------------------------------------------------- 1 | package mogic.toad; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.net.Proxy; 7 | import android.os.Build; 8 | import android.os.Parcelable; 9 | import android.util.ArrayMap; 10 | import android.util.Log; 11 | import android.webkit.WebView; 12 | import org.apache.http.HttpHost; 13 | 14 | import java.io.PrintWriter; 15 | import java.io.StringWriter; 16 | import java.lang.reflect.Constructor; 17 | import java.lang.reflect.Field; 18 | import java.lang.reflect.InvocationTargetException; 19 | import java.lang.reflect.Method; 20 | 21 | public class ToadWebViewProxyUtil { 22 | public static final String LOG_TAG = "WebViewProxyUtil"; 23 | 24 | public static boolean setProxy(WebView webview, String host, int port) { 25 | // 3.2 (HC) or lower 26 | if (Build.VERSION.SDK_INT <= 13) { 27 | return setProxyUpToHC(webview, host, port); 28 | } 29 | // ICS: 4.0 30 | else if (Build.VERSION.SDK_INT <= 15) { 31 | return setProxyICS(webview, host, port); 32 | } 33 | // 4.1-4.3 (JB) 34 | else if (Build.VERSION.SDK_INT <= 18) { 35 | return setProxyJB(webview, host, port); 36 | } 37 | // 4.4 (KK) & 5.0 (Lollipop) 38 | else { 39 | return setProxyKKPlus(webview, host, port, "android.app.Application"); 40 | } 41 | } 42 | 43 | /** 44 | * Set Proxy for Android 3.2 and below. 45 | */ 46 | @SuppressWarnings("all") 47 | private static boolean setProxyUpToHC(WebView webview, String host, int port) { 48 | Log.d(LOG_TAG, "Setting proxy with <= 3.2 API."); 49 | 50 | HttpHost proxyServer = new HttpHost(host, port); 51 | // Getting network 52 | Class networkClass = null; 53 | Object network = null; 54 | try { 55 | networkClass = Class.forName("android.webkit.Network"); 56 | if (networkClass == null) { 57 | Log.e(LOG_TAG, "failed to get class for android.webkit.Network"); 58 | return false; 59 | } 60 | Method getInstanceMethod = networkClass.getMethod("getInstance", Context.class); 61 | if (getInstanceMethod == null) { 62 | Log.e(LOG_TAG, "failed to get getInstance method"); 63 | } 64 | network = getInstanceMethod.invoke(networkClass, new Object[]{webview.getContext()}); 65 | } catch (Exception ex) { 66 | Log.e(LOG_TAG, "error getting network: " + ex); 67 | return false; 68 | } 69 | if (network == null) { 70 | Log.e(LOG_TAG, "error getting network: network is null"); 71 | return false; 72 | } 73 | Object requestQueue = null; 74 | try { 75 | Field requestQueueField = networkClass 76 | .getDeclaredField("mRequestQueue"); 77 | requestQueue = getFieldValueSafely(requestQueueField, network); 78 | } catch (Exception ex) { 79 | Log.e(LOG_TAG, "error getting field value"); 80 | return false; 81 | } 82 | if (requestQueue == null) { 83 | Log.e(LOG_TAG, "Request queue is null"); 84 | return false; 85 | } 86 | Field proxyHostField = null; 87 | try { 88 | Class requestQueueClass = Class.forName("android.net.http.RequestQueue"); 89 | proxyHostField = requestQueueClass 90 | .getDeclaredField("mProxyHost"); 91 | } catch (Exception ex) { 92 | Log.e(LOG_TAG, "error getting proxy host field"); 93 | return false; 94 | } 95 | 96 | boolean temp = proxyHostField.isAccessible(); 97 | try { 98 | proxyHostField.setAccessible(true); 99 | proxyHostField.set(requestQueue, proxyServer); 100 | } catch (Exception ex) { 101 | Log.e(LOG_TAG, "error setting proxy host"); 102 | } finally { 103 | proxyHostField.setAccessible(temp); 104 | } 105 | 106 | Log.d(LOG_TAG, "Setting proxy with <= 3.2 API successful!"); 107 | return true; 108 | } 109 | 110 | @SuppressWarnings("all") 111 | private static boolean setProxyICS(WebView webview, String host, int port) { 112 | try 113 | { 114 | Log.d(LOG_TAG, "Setting proxy with 4.0 API."); 115 | 116 | Class jwcjb = Class.forName("android.webkit.JWebCoreJavaBridge"); 117 | Class params[] = new Class[1]; 118 | params[0] = Class.forName("android.net.ProxyProperties"); 119 | Method updateProxyInstance = jwcjb.getDeclaredMethod("updateProxy", params); 120 | 121 | Class wv = Class.forName("android.webkit.WebView"); 122 | Field mWebViewCoreField = wv.getDeclaredField("mWebViewCore"); 123 | Object mWebViewCoreFieldInstance = getFieldValueSafely(mWebViewCoreField, webview); 124 | 125 | Class wvc = Class.forName("android.webkit.WebViewCore"); 126 | Field mBrowserFrameField = wvc.getDeclaredField("mBrowserFrame"); 127 | Object mBrowserFrame = getFieldValueSafely(mBrowserFrameField, mWebViewCoreFieldInstance); 128 | 129 | Class bf = Class.forName("android.webkit.BrowserFrame"); 130 | Field sJavaBridgeField = bf.getDeclaredField("sJavaBridge"); 131 | Object sJavaBridge = getFieldValueSafely(sJavaBridgeField, mBrowserFrame); 132 | 133 | Class ppclass = Class.forName("android.net.ProxyProperties"); 134 | Class pparams[] = new Class[3]; 135 | pparams[0] = String.class; 136 | pparams[1] = int.class; 137 | pparams[2] = String.class; 138 | Constructor ppcont = ppclass.getConstructor(pparams); 139 | 140 | updateProxyInstance.invoke(sJavaBridge, ppcont.newInstance(host, port, null)); 141 | 142 | Log.d(LOG_TAG, "Setting proxy with 4.0 API successful!"); 143 | return true; 144 | } 145 | catch (Exception ex) 146 | { 147 | Log.e(LOG_TAG, "failed to set HTTP proxy: " + ex); 148 | return false; 149 | } 150 | } 151 | 152 | /** 153 | * Set Proxy for Android 4.1 - 4.3. 154 | */ 155 | @SuppressWarnings("all") 156 | private static boolean setProxyJB(WebView webview, String host, int port) { 157 | Log.d(LOG_TAG, "Setting proxy with 4.1 - 4.3 API."); 158 | 159 | try { 160 | Class wvcClass = Class.forName("android.webkit.WebViewClassic"); 161 | Class wvParams[] = new Class[1]; 162 | wvParams[0] = Class.forName("android.webkit.WebView"); 163 | Method fromWebView = wvcClass.getDeclaredMethod("fromWebView", wvParams); 164 | Object webViewClassic = fromWebView.invoke(null, webview); 165 | 166 | Class wv = Class.forName("android.webkit.WebViewClassic"); 167 | Field mWebViewCoreField = wv.getDeclaredField("mWebViewCore"); 168 | Object mWebViewCoreFieldInstance = getFieldValueSafely(mWebViewCoreField, webViewClassic); 169 | 170 | Class wvc = Class.forName("android.webkit.WebViewCore"); 171 | Field mBrowserFrameField = wvc.getDeclaredField("mBrowserFrame"); 172 | Object mBrowserFrame = getFieldValueSafely(mBrowserFrameField, mWebViewCoreFieldInstance); 173 | 174 | Class bf = Class.forName("android.webkit.BrowserFrame"); 175 | Field sJavaBridgeField = bf.getDeclaredField("sJavaBridge"); 176 | Object sJavaBridge = getFieldValueSafely(sJavaBridgeField, mBrowserFrame); 177 | 178 | Class ppclass = Class.forName("android.net.ProxyProperties"); 179 | Class pparams[] = new Class[3]; 180 | pparams[0] = String.class; 181 | pparams[1] = int.class; 182 | pparams[2] = String.class; 183 | Constructor ppcont = ppclass.getConstructor(pparams); 184 | 185 | Class jwcjb = Class.forName("android.webkit.JWebCoreJavaBridge"); 186 | Class params[] = new Class[1]; 187 | params[0] = Class.forName("android.net.ProxyProperties"); 188 | Method updateProxyInstance = jwcjb.getDeclaredMethod("updateProxy", params); 189 | 190 | updateProxyInstance.invoke(sJavaBridge, ppcont.newInstance(host, port, null)); 191 | } catch (Exception ex) { 192 | Log.e(LOG_TAG,"Setting proxy with >= 4.1 API failed with error: " + ex.getMessage()); 193 | return false; 194 | } 195 | 196 | Log.d(LOG_TAG, "Setting proxy with 4.1 - 4.3 API successful!"); 197 | return true; 198 | } 199 | 200 | // from https://stackoverflow.com/questions/19979578/android-webview-set-proxy-programatically-kitkat 201 | @SuppressLint("NewApi") 202 | @SuppressWarnings("all") 203 | private static boolean setProxyKKPlus(WebView webView, String host, int port, String applicationClassName) { 204 | Log.d(LOG_TAG, "Setting proxy with >= 4.4 API."); 205 | 206 | Context appContext = webView.getContext().getApplicationContext(); 207 | System.setProperty("http.proxyHost", host); 208 | System.setProperty("http.proxyPort", port + ""); 209 | System.setProperty("https.proxyHost", host); 210 | System.setProperty("https.proxyPort", port + ""); 211 | try { 212 | Class applictionCls = Class.forName(applicationClassName); 213 | Field loadedApkField = applictionCls.getField("mLoadedApk"); 214 | loadedApkField.setAccessible(true); 215 | Object loadedApk = loadedApkField.get(appContext); 216 | Class loadedApkCls = Class.forName("android.app.LoadedApk"); 217 | Field receiversField = loadedApkCls.getDeclaredField("mReceivers"); 218 | receiversField.setAccessible(true); 219 | ArrayMap receivers = (ArrayMap) receiversField.get(loadedApk); 220 | for (Object receiverMap : receivers.values()) { 221 | for (Object rec : ((ArrayMap) receiverMap).keySet()) { 222 | Class clazz = rec.getClass(); 223 | if (clazz.getName().contains("ProxyChangeListener")) { 224 | Method onReceiveMethod = clazz.getDeclaredMethod("onReceive", Context.class, Intent.class); 225 | Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION); 226 | 227 | onReceiveMethod.invoke(rec, appContext, intent); 228 | } 229 | } 230 | } 231 | 232 | Log.d(LOG_TAG, "Setting proxy with >= 4.4 API successful!"); 233 | return true; 234 | } catch (ClassNotFoundException e) { 235 | StringWriter sw = new StringWriter(); 236 | e.printStackTrace(new PrintWriter(sw)); 237 | String exceptionAsString = sw.toString(); 238 | Log.v(LOG_TAG, e.getMessage()); 239 | Log.v(LOG_TAG, exceptionAsString); 240 | } catch (NoSuchFieldException e) { 241 | StringWriter sw = new StringWriter(); 242 | e.printStackTrace(new PrintWriter(sw)); 243 | String exceptionAsString = sw.toString(); 244 | Log.v(LOG_TAG, e.getMessage()); 245 | Log.v(LOG_TAG, exceptionAsString); 246 | } catch (IllegalAccessException e) { 247 | StringWriter sw = new StringWriter(); 248 | e.printStackTrace(new PrintWriter(sw)); 249 | String exceptionAsString = sw.toString(); 250 | Log.v(LOG_TAG, e.getMessage()); 251 | Log.v(LOG_TAG, exceptionAsString); 252 | } catch (IllegalArgumentException e) { 253 | StringWriter sw = new StringWriter(); 254 | e.printStackTrace(new PrintWriter(sw)); 255 | String exceptionAsString = sw.toString(); 256 | Log.v(LOG_TAG, e.getMessage()); 257 | Log.v(LOG_TAG, exceptionAsString); 258 | } catch (NoSuchMethodException e) { 259 | StringWriter sw = new StringWriter(); 260 | e.printStackTrace(new PrintWriter(sw)); 261 | String exceptionAsString = sw.toString(); 262 | Log.v(LOG_TAG, e.getMessage()); 263 | Log.v(LOG_TAG, exceptionAsString); 264 | } catch (InvocationTargetException e) { 265 | StringWriter sw = new StringWriter(); 266 | e.printStackTrace(new PrintWriter(sw)); 267 | String exceptionAsString = sw.toString(); 268 | Log.v(LOG_TAG, e.getMessage()); 269 | Log.v(LOG_TAG, exceptionAsString); 270 | } 271 | return false; 272 | } 273 | 274 | private static Object getFieldValueSafely(Field field, Object classInstance) throws IllegalArgumentException, IllegalAccessException { 275 | boolean oldAccessibleValue = field.isAccessible(); 276 | field.setAccessible(true); 277 | Object result = field.get(classInstance); 278 | field.setAccessible(oldAccessibleValue); 279 | return result; 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /app/src/main/java/mogic/toad/proxyhandler/ConnectionHandler.java: -------------------------------------------------------------------------------- 1 | package mogic.toad.proxyhandler; 2 | 3 | import android.util.Log; 4 | 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.io.OutputStream; 8 | import java.net.ServerSocket; 9 | import java.net.Socket; 10 | import java.net.SocketException; 11 | import java.net.URI; 12 | import java.net.URISyntaxException; 13 | 14 | import mogic.toad.ToadHosts; 15 | 16 | public class ConnectionHandler { 17 | private static final String TAG = "ProxyServer"; 18 | 19 | private static boolean mForceHttps = false; 20 | 21 | public static boolean getForceHttps() { 22 | return mForceHttps; 23 | } 24 | 25 | public static void setForceHttps(boolean value) { 26 | mForceHttps = value; 27 | } 28 | 29 | public static void handleConnection(Socket connection) { 30 | try { 31 | String requestLine = getLine(connection.getInputStream()); 32 | String[] splitLine = requestLine.split(" "); 33 | if (splitLine.length < 3) { 34 | connection.close(); 35 | return; 36 | } 37 | String requestType = splitLine[0]; 38 | String urlString = splitLine[1]; 39 | String httpVersion = splitLine[2]; 40 | 41 | URI url = null; 42 | String host; 43 | int port; 44 | boolean isHttpConnectMethod = requestType.equals("CONNECT"); 45 | 46 | if (isHttpConnectMethod) { 47 | String[] hostPortSplit = urlString.split(":"); 48 | host = hostPortSplit[0]; 49 | // Use default SSL port if not specified. Parse it otherwise 50 | if (hostPortSplit.length < 2) { 51 | port = 443; 52 | } else { 53 | try { 54 | port = Integer.parseInt(hostPortSplit[1]); 55 | } catch (NumberFormatException nfe) { 56 | connection.close(); 57 | return; 58 | } 59 | } 60 | } else { 61 | try { 62 | url = new URI(urlString); 63 | host = url.getHost(); 64 | port = url.getPort(); 65 | if (port < 0) { 66 | port = 80; 67 | } 68 | } catch (URISyntaxException e) { 69 | connection.close(); 70 | return; 71 | } 72 | } 73 | 74 | String newHost = ToadHosts.lookup(host); 75 | if (newHost != null) { 76 | if (newHost == "0.0.0.0") { 77 | connection.close(); 78 | return; 79 | } 80 | if (!isHttpConnectMethod && port == 80 && mForceHttps) { 81 | sendLine(connection, "HTTP/1.1 301 Moved Permanently"); 82 | sendLine(connection, "Location: https://" + host + getAbsolutePathFromAbsoluteURI(url)); 83 | sendLine(connection, "Content-Type: text/html"); 84 | sendLine(connection, "Content-Length: 0"); 85 | sendLine(connection, ""); 86 | connection.close(); 87 | return; 88 | } 89 | host = newHost; 90 | } 91 | 92 | Socket server = new Socket(host, port); 93 | if (isHttpConnectMethod) { 94 | skipToRequestBody(connection); 95 | sendLine(connection, "HTTP/1.1 200 Connection Established"); 96 | sendLine(connection, ""); 97 | } else { 98 | // Proxying the request directly to the origin server. 99 | sendAugmentedRequestToHost(connection, server, requestType, url, httpVersion); 100 | } 101 | 102 | // Pass data back and forth until complete. 103 | SocketConnect.connect(connection, server); 104 | 105 | } catch (Exception e) { 106 | Log.d(TAG, "Problem Proxying", e); 107 | } 108 | 109 | try { 110 | connection.close(); 111 | } catch (IOException ioe) { 112 | // Do nothing 113 | } 114 | } 115 | 116 | /** 117 | * Sends HTTP request-line (i.e. the first line in the request) 118 | * that contains absolute path of a given absolute URI. 119 | * 120 | * @param server server to send the request to. 121 | * @param requestType type of the request, a.k.a. HTTP method. 122 | * @param absoluteUri absolute URI which absolute path should be extracted. 123 | * @param httpVersion version of HTTP, e.g. HTTP/1.1. 124 | * @throws IOException if the request-line cannot be sent. 125 | */ 126 | private static void sendRequestLineWithPath(Socket server, String requestType, 127 | URI absoluteUri, String httpVersion) throws IOException { 128 | 129 | String absolutePath = getAbsolutePathFromAbsoluteURI(absoluteUri); 130 | String outgoingRequestLine = String.format("%s %s %s", 131 | requestType, absolutePath, httpVersion); 132 | sendLine(server, outgoingRequestLine); 133 | } 134 | 135 | /** 136 | * Extracts absolute path form a given URI. E.g., passing 137 | * http://google.com:80/execute?query=cat#top 138 | * will result in /execute?query=cat#top. 139 | * 140 | * @param uri URI which absolute path has to be extracted, 141 | * @return the absolute path of the URI, 142 | */ 143 | private static String getAbsolutePathFromAbsoluteURI(URI uri) { 144 | String rawPath = uri.getRawPath(); 145 | String rawQuery = uri.getRawQuery(); 146 | String rawFragment = uri.getRawFragment(); 147 | StringBuilder absolutePath = new StringBuilder(); 148 | 149 | if (rawPath != null) { 150 | absolutePath.append(rawPath); 151 | } else { 152 | absolutePath.append("/"); 153 | } 154 | if (rawQuery != null) { 155 | absolutePath.append("?").append(rawQuery); 156 | } 157 | if (rawFragment != null) { 158 | absolutePath.append("#").append(rawFragment); 159 | } 160 | return absolutePath.toString(); 161 | } 162 | 163 | private static String getLine(InputStream inputStream) throws IOException { 164 | StringBuilder buffer = new StringBuilder(); 165 | int byteBuffer = inputStream.read(); 166 | if (byteBuffer < 0) return ""; 167 | do { 168 | if (byteBuffer != '\r') { 169 | buffer.append((char)byteBuffer); 170 | } 171 | byteBuffer = inputStream.read(); 172 | } while ((byteBuffer != '\n') && (byteBuffer >= 0)); 173 | 174 | return buffer.toString(); 175 | } 176 | 177 | private static void sendLine(Socket socket, String line) throws IOException { 178 | OutputStream os = socket.getOutputStream(); 179 | os.write(line.getBytes()); 180 | os.write('\r'); 181 | os.write('\n'); 182 | os.flush(); 183 | } 184 | 185 | /** 186 | * Reads from socket until an empty line is read which indicates the end of HTTP headers. 187 | * 188 | * @param socket socket to read from. 189 | * @throws IOException if an exception took place during the socket read. 190 | */ 191 | private static void skipToRequestBody(Socket socket) throws IOException { 192 | while (getLine(socket.getInputStream()).length() != 0); 193 | } 194 | 195 | /** 196 | * Sends an augmented request to the final host (DIRECT connection). 197 | * 198 | * @param src socket to read HTTP headers from.The socket current position should point 199 | * to the beginning of the HTTP header section. 200 | * @param dst socket to write the augmented request to. 201 | * @param httpMethod original request http method. 202 | * @param uri original request absolute URI. 203 | * @param httpVersion original request http version. 204 | * @throws IOException if an exception took place during socket reads or writes. 205 | */ 206 | private static void sendAugmentedRequestToHost(Socket src, Socket dst, 207 | String httpMethod, URI uri, String httpVersion) throws IOException { 208 | 209 | sendRequestLineWithPath(dst, httpMethod, uri, httpVersion); 210 | filterAndForwardRequestHeaders(src, dst); 211 | 212 | // Currently the proxy does not support keep-alive connections; therefore, 213 | // the proxy has to request the destination server to close the connection 214 | // after the destination server sent the response. 215 | sendLine(dst, "Connection: close"); 216 | 217 | // Sends and empty line that indicates termination of the header section. 218 | sendLine(dst, ""); 219 | } 220 | 221 | /** 222 | * Forwards original request headers filtering out the ones that have to be removed. 223 | * 224 | * @param src source socket that contains original request headers. 225 | * @param dst destination socket to send the filtered headers to. 226 | * @throws IOException if the data cannot be read from or written to the sockets. 227 | */ 228 | private static void filterAndForwardRequestHeaders(Socket src, Socket dst) throws IOException { 229 | String line; 230 | do { 231 | line = getLine(src.getInputStream()); 232 | if (line.length() > 0 && !shouldRemoveHeaderLine(line)) { 233 | sendLine(dst, line); 234 | } 235 | } while (line.length() > 0); 236 | } 237 | 238 | /** 239 | * Returns true if a given header line has to be removed from the original request. 240 | * 241 | * @param line header line that should be analysed. 242 | * @return true if the header line should be removed and not forwarded to the destination. 243 | */ 244 | private static boolean shouldRemoveHeaderLine(String line) { 245 | int colIndex = line.indexOf(":"); 246 | if (colIndex != -1) { 247 | String headerName = line.substring(0, colIndex).trim(); 248 | if (headerNameMatches(headerName, "connection") || 249 | headerNameMatches(headerName, "proxy-connection")) { 250 | return true; 251 | } 252 | } 253 | return false; 254 | } 255 | 256 | private static boolean headerNameMatches(String headerName, String pattern) { 257 | return headerName.regionMatches(true, 0, pattern, 0, pattern.length()); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /app/src/main/java/mogic/toad/proxyhandler/IProxyListener.java: -------------------------------------------------------------------------------- 1 | package mogic.toad.proxyhandler; 2 | 3 | public interface IProxyListener { 4 | void onReportProxyPort(int port); 5 | } -------------------------------------------------------------------------------- /app/src/main/java/mogic/toad/proxyhandler/ProxyServer.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013, The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package mogic.toad.proxyhandler; 17 | 18 | import android.util.Log; 19 | 20 | import android.os.Handler; 21 | import android.os.Looper; 22 | 23 | import java.io.IOException; 24 | import java.net.ServerSocket; 25 | import java.net.Socket; 26 | import java.net.SocketException; 27 | import java.util.concurrent.ExecutorService; 28 | import java.util.concurrent.Executors; 29 | 30 | /** 31 | * @hide 32 | */ 33 | public class ProxyServer extends Thread { 34 | 35 | private static final String TAG = "ProxyServer"; 36 | 37 | private ExecutorService threadExecutor; 38 | 39 | private boolean mIsRunning = false; 40 | 41 | private ServerSocket mServerSocket; 42 | private int mPort; 43 | private IProxyListener mCallback; 44 | 45 | private class ProxyConnection implements Runnable { 46 | private Socket connection; 47 | 48 | private ProxyConnection(Socket connection) { 49 | this.connection = connection; 50 | } 51 | 52 | @Override 53 | public void run() { 54 | ConnectionHandler.handleConnection(connection); 55 | } 56 | } 57 | 58 | public ProxyServer() { 59 | threadExecutor = Executors.newCachedThreadPool(); 60 | mPort = -1; 61 | mCallback = null; 62 | } 63 | 64 | @Override 65 | public void run() { 66 | try { 67 | mServerSocket = new ServerSocket(0); 68 | 69 | reportPort(mServerSocket.getLocalPort()); 70 | 71 | while (mIsRunning) { 72 | try { 73 | Socket socket = mServerSocket.accept(); 74 | // Only receive local connections. 75 | if (socket.getInetAddress().isLoopbackAddress()) { 76 | ProxyConnection parser = new ProxyConnection(socket); 77 | 78 | threadExecutor.execute(parser); 79 | } else { 80 | socket.close(); 81 | } 82 | } catch (IOException e) { 83 | e.printStackTrace(); 84 | } 85 | } 86 | } catch (SocketException e) { 87 | Log.e(TAG, "Failed to start proxy server", e); 88 | } catch (IOException e1) { 89 | Log.e(TAG, "Failed to start proxy server", e1); 90 | } 91 | 92 | mIsRunning = false; 93 | } 94 | 95 | protected synchronized void reportPort(int port) { 96 | mPort = port; 97 | if (mCallback != null) { 98 | new Handler(Looper.getMainLooper()).post(new Runnable() { 99 | @Override 100 | public void run() { 101 | mCallback.onReportProxyPort(mPort); 102 | } 103 | }); 104 | } 105 | } 106 | 107 | public synchronized void setCallback(IProxyListener callback) { 108 | mCallback = callback; 109 | } 110 | 111 | public synchronized void startServer() { 112 | mIsRunning = true; 113 | start(); 114 | } 115 | 116 | public synchronized void stopServer() { 117 | mIsRunning = false; 118 | if (mServerSocket != null) { 119 | try { 120 | mServerSocket.close(); 121 | mServerSocket = null; 122 | } catch (IOException e) { 123 | e.printStackTrace(); 124 | } 125 | } 126 | } 127 | 128 | public boolean isBound() { 129 | return (mPort != -1); 130 | } 131 | 132 | public int getPort() { 133 | return mPort; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /app/src/main/java/mogic/toad/proxyhandler/SocketConnect.java: -------------------------------------------------------------------------------- 1 | package mogic.toad.proxyhandler; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.io.OutputStream; 6 | import java.net.Socket; 7 | 8 | /** 9 | * @hide 10 | */ 11 | public class SocketConnect extends Thread { 12 | 13 | private InputStream from; 14 | private OutputStream to; 15 | 16 | public SocketConnect(Socket from, Socket to) throws IOException { 17 | this.from = from.getInputStream(); 18 | this.to = to.getOutputStream(); 19 | start(); 20 | } 21 | 22 | @Override 23 | public void run() { 24 | final byte[] buffer = new byte[512]; 25 | 26 | try { 27 | while (true) { 28 | int r = from.read(buffer); 29 | if (r < 0) { 30 | break; 31 | } 32 | to.write(buffer, 0, r); 33 | } 34 | from.close(); 35 | to.close(); 36 | } catch (IOException io) { 37 | 38 | } 39 | } 40 | 41 | public static void connect(Socket first, Socket second) { 42 | try { 43 | SocketConnect sc1 = new SocketConnect(first, second); 44 | SocketConnect sc2 = new SocketConnect(second, first); 45 | try { 46 | sc1.join(); 47 | } catch (InterruptedException e) { 48 | } 49 | try { 50 | sc2.join(); 51 | } catch (InterruptedException e) { 52 | } 53 | } catch (IOException e) { 54 | e.printStackTrace(); 55 | } 56 | } 57 | 58 | 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/toad_back_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toadapp/toad-android/4e2e5eb4484262fce37f60e11d5aaf3e0e42dc5e/app/src/main/res/drawable-hdpi/toad_back_button.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/toad_forward_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toadapp/toad-android/4e2e5eb4484262fce37f60e11d5aaf3e0e42dc5e/app/src/main/res/drawable-hdpi/toad_forward_button.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/toad_left_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toadapp/toad-android/4e2e5eb4484262fce37f60e11d5aaf3e0e42dc5e/app/src/main/res/drawable-hdpi/toad_left_button.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/toad_right_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toadapp/toad-android/4e2e5eb4484262fce37f60e11d5aaf3e0e42dc5e/app/src/main/res/drawable-hdpi/toad_right_button.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/toad_back_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toadapp/toad-android/4e2e5eb4484262fce37f60e11d5aaf3e0e42dc5e/app/src/main/res/drawable-mdpi/toad_back_button.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/toad_forward_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toadapp/toad-android/4e2e5eb4484262fce37f60e11d5aaf3e0e42dc5e/app/src/main/res/drawable-mdpi/toad_forward_button.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/toad_left_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toadapp/toad-android/4e2e5eb4484262fce37f60e11d5aaf3e0e42dc5e/app/src/main/res/drawable-mdpi/toad_left_button.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/toad_right_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toadapp/toad-android/4e2e5eb4484262fce37f60e11d5aaf3e0e42dc5e/app/src/main/res/drawable-mdpi/toad_right_button.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/toad_back_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toadapp/toad-android/4e2e5eb4484262fce37f60e11d5aaf3e0e42dc5e/app/src/main/res/drawable-xhdpi/toad_back_button.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/toad_forward_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toadapp/toad-android/4e2e5eb4484262fce37f60e11d5aaf3e0e42dc5e/app/src/main/res/drawable-xhdpi/toad_forward_button.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/toad_left_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toadapp/toad-android/4e2e5eb4484262fce37f60e11d5aaf3e0e42dc5e/app/src/main/res/drawable-xhdpi/toad_left_button.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/toad_right_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toadapp/toad-android/4e2e5eb4484262fce37f60e11d5aaf3e0e42dc5e/app/src/main/res/drawable-xhdpi/toad_right_button.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/toad_back_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toadapp/toad-android/4e2e5eb4484262fce37f60e11d5aaf3e0e42dc5e/app/src/main/res/drawable-xxhdpi/toad_back_button.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/toad_forward_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toadapp/toad-android/4e2e5eb4484262fce37f60e11d5aaf3e0e42dc5e/app/src/main/res/drawable-xxhdpi/toad_forward_button.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/toad_left_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toadapp/toad-android/4e2e5eb4484262fce37f60e11d5aaf3e0e42dc5e/app/src/main/res/drawable-xxhdpi/toad_left_button.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/toad_right_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toadapp/toad-android/4e2e5eb4484262fce37f60e11d5aaf3e0e42dc5e/app/src/main/res/drawable-xxhdpi/toad_right_button.png -------------------------------------------------------------------------------- /app/src/main/res/layout/toad_web_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | 22 | 23 | 26 | 27 | 36 | 37 | 48 | 49 | 57 | 58 | 69 | 70 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /app/src/main/res/menu/toad_left_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/menu/toad_right_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 56 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toadapp/toad-android/4e2e5eb4484262fce37f60e11d5aaf3e0e42dc5e/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toadapp/toad-android/4e2e5eb4484262fce37f60e11d5aaf3e0e42dc5e/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #333333 4 | #282828 5 | #ffffff 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Toad 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 24 | 28 | 29 | 32 | 33 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/test/java/mogic/toad/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package mogic.toad; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() throws Exception { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:2.3.3' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | } 19 | } 20 | 21 | task clean(type: Delete) { 22 | delete rootProject.buildDir 23 | } 24 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toadapp/toad-android/4e2e5eb4484262fce37f60e11d5aaf3e0e42dc5e/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Jul 28 00:49:11 UTC 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------