├── .gitignore ├── .idea ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── gradle.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── debug.keystore ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ ├── gamelist.json │ └── help.html │ ├── java │ ├── com │ │ └── example │ │ │ └── gamebrowser │ │ │ ├── AboutActivity.java │ │ │ ├── FrmBrowser.java │ │ │ ├── FullScreenActivity.java │ │ │ ├── LauncherActivity.java │ │ │ ├── PluginActivity.java │ │ │ ├── SettingActivity.java │ │ │ ├── WebGameBoostEngine.java │ │ │ └── WindowService.java │ └── wei │ │ └── mark │ │ └── standout │ │ ├── StandOutWindow.java │ │ ├── Utils.java │ │ ├── WindowCache.java │ │ ├── constants │ │ └── StandOutFlags.java │ │ └── ui │ │ ├── TouchInfo.java │ │ └── Window.java │ └── res │ ├── drawable-hdpi │ ├── close.png │ ├── corner.png │ ├── hide.png │ ├── ic_menu_capture.png │ └── maximize.png │ ├── drawable-xhdpi │ ├── ic_notification_icon.png │ └── shadow.9.png │ ├── drawable │ ├── bg_btn_close.xml │ ├── bg_btn_cmd.xml │ ├── bg_frame.xml │ └── ic.png │ ├── layout │ ├── about.xml │ ├── activity_plugin.xml │ ├── activity_setting.xml │ ├── adapter_app.xml │ ├── drop_down_list_item.xml │ ├── fullscreen.xml │ ├── fwd.xml │ ├── launcher.xml │ └── system_window_decorators.xml │ └── values │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── plugin_template.apk └── 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 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 安卓页游专用浏览器 2 | 3 | 交流群 QQ:[1039904710](https://jq.qq.com/?_wv=1027&k=59Oskpg) 欢迎来玩 4 | 5 | ###### 手机版请点击下方 View all of README.md展开全文 6 | 7 | - 最大化/最小化隐藏/自由调整大小,像Windows一样使用。(需要悬浮窗权限) 8 | 9 | - 随手研发加速引擎,载入更快,更省流量 10 | 11 | - 全屏启动,沉浸模式,退出确认 12 | 13 | - 半透明窗口功能 14 | 15 | - 分辨率省电功能,亲测省一半左右(但还是没官方APP省电(可能)) 16 | 17 | - 插件功能(请勿滥用) 18 | 19 | - 插件模板在项目仓库中有 20 | 21 | - [点我下载](https://github.com/ZYFDroid/android-webgame-browser/releases) 22 | 23 | ### 更改asset里的gamelist.json即可更改游戏列表 24 | 25 | - 推荐一个游戏 《雀魂》 为了方便,已经内置到gamelist.json里了 26 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | signingConfigs { 5 | defaultRelease { 6 | keyAlias 'androiddebugkey' 7 | keyPassword 'android' 8 | storeFile file('debug.keystore') 9 | storePassword 'android' 10 | } 11 | } 12 | compileSdkVersion 29 13 | buildToolsVersion "29.0.2" 14 | defaultConfig { 15 | applicationId "com.example.gamebrowser" 16 | minSdkVersion 21 17 | targetSdkVersion 28 18 | versionCode 1100 19 | versionName "1.1.0.0" 20 | } 21 | buildTypes { 22 | release { 23 | minifyEnabled true 24 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 25 | signingConfig signingConfigs.defaultRelease 26 | } 27 | debug { 28 | debuggable true 29 | minifyEnabled false 30 | } 31 | } 32 | } 33 | 34 | dependencies { 35 | compile fileTree(include: ['*.jar'], dir: 'libs') 36 | } 37 | -------------------------------------------------------------------------------- /app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZYFDroid/android-webgame-browser/3b21e0439696ee68b6b04856c10f305b23c7e5ec/app/debug.keystore -------------------------------------------------------------------------------- /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 C:\Users\WorldSkills2020\AppData\Local\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 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 39 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/assets/gamelist.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "guid":"gb_about", 4 | "name":"使用说明(内置)", 5 | "url":"file:////android_asset/help.html" 6 | }, 7 | { 8 | "guid":"majsoulcn", 9 | "name":"雀魂麻将", 10 | "url":"https://www.majsoul.com/1/" 11 | }, 12 | { 13 | "guid":"majsouljp", 14 | "name":"雀魂(日服)", 15 | "url":"https://game.mahjongsoul.com/" 16 | }, 17 | { 18 | "guid":"majsoulen", 19 | "name":"雀魂(国际服)", 20 | "url":"https://mahjongsoul.game.yo-star.com/" 21 | } 22 | ] -------------------------------------------------------------------------------- /app/src/main/assets/help.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

欢迎使用电竞级浏览器

4 |

→ 转到 设置 -> 选择游戏

5 | 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/gamebrowser/AboutActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.gamebrowser; 2 | 3 | import android.app.Activity; 4 | import android.app.AlertDialog; 5 | import android.content.Intent; 6 | import android.net.Uri; 7 | import android.os.Bundle; 8 | import android.view.View; 9 | import android.widget.Toast; 10 | 11 | import wei.mark.standout.Utils; 12 | 13 | public class AboutActivity extends Activity { 14 | 15 | @Override 16 | protected void onCreate(Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | setContentView(R.layout.about); 19 | } 20 | 21 | 22 | public void introduce(View view) { 23 | String msg="电竞浏览器\n" + 24 | "\n(窗口模式下,选中文本需要点左上角图标复制)\n" + 25 | "-最大化/最小化隐藏/自由调整大小(需要悬浮窗权限)\n" + 26 | "-随手研发(可能)加速引擎,游戏载入更快,更省流量\n" + 27 | "-半透明窗口功能\n" + 28 | "-全屏启动,沉浸模式,退出确认,客户端级体验(全屏模式下支持分屏)\n" + 29 | "\n" + 30 | "\n" + 31 | "本软件通过系统浏览器实现,部分太老的系统和安卓模拟器可能打不开\n" + 32 | "若您正在玩的游戏有Android版客户端,推荐使用官方客户端而不是浏览器\n" + 33 | "如果你喜欢这个项目,请在github为我点个star(如果你不知道什么是github就算了)"; 34 | AlertDialog ald = new AlertDialog.Builder(this).setTitle("功能介绍").setMessage(msg).setPositiveButton(android.R.string.ok,null).create(); 35 | ald.show(); 36 | Utils.setDialogVersion(this,"firstrun",2); 37 | } 38 | 39 | public void github(View view) { 40 | openUrl("https://github.com/ZYFDroid/android-webgame-browser"); 41 | } 42 | 43 | public void update(View view) { 44 | openUrl("https://github.com/ZYFDroid/android-webgame-browser/releases"); 45 | } 46 | 47 | void openUrl(String url){ 48 | try { 49 | Intent intent = new Intent(Intent.ACTION_VIEW); 50 | Uri uri = Uri.parse(url); 51 | intent.setData(uri); 52 | startActivity(intent); 53 | }catch (Exception ex){ 54 | Toast.makeText(this, "建议先安装一个浏览器", Toast.LENGTH_SHORT).show(); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/gamebrowser/FrmBrowser.java: -------------------------------------------------------------------------------- 1 | package com.example.gamebrowser; 2 | 3 | import android.app.Notification; 4 | import android.app.NotificationChannel; 5 | import android.app.NotificationManager; 6 | import android.app.PendingIntent; 7 | import android.content.ClipData; 8 | import android.content.ClipboardManager; 9 | import android.content.Context; 10 | import android.content.Intent; 11 | import android.graphics.Bitmap; 12 | import android.os.Build; 13 | import android.os.Handler; 14 | import android.util.Log; 15 | import android.view.Gravity; 16 | import android.view.MotionEvent; 17 | import android.view.View; 18 | import android.view.animation.AlphaAnimation; 19 | import android.view.animation.Animation; 20 | import android.view.animation.AnimationSet; 21 | import android.view.animation.ScaleAnimation; 22 | import android.view.animation.TranslateAnimation; 23 | import android.webkit.JavascriptInterface; 24 | import android.webkit.WebView; 25 | import android.widget.FrameLayout; 26 | import android.widget.Toast; 27 | 28 | import java.util.ArrayList; 29 | import java.util.HashMap; 30 | import java.util.List; 31 | 32 | import wei.mark.standout.StandOutWindow; 33 | import wei.mark.standout.Utils; 34 | import wei.mark.standout.constants.StandOutFlags; 35 | import wei.mark.standout.ui.Window; 36 | 37 | /** 38 | * Created by WorldSkills2020 on 10/23/2019. 39 | */ 40 | 41 | public class FrmBrowser extends StandOutWindow { 42 | 43 | public static boolean isRunning = false; 44 | 45 | 46 | public HashMap clipboardFinder = new HashMap<>(); 47 | 48 | public static String baseUrl = SettingActivity.defaultPage; 49 | WebView mWebView; 50 | 51 | @Override 52 | public String getAppName() { 53 | return Utils.getSP(this).getString("windowtext","使用说明")+" - 电竞浏览器"; 54 | } 55 | 56 | @Override 57 | public int getAppIcon() { 58 | return R.drawable.ic; 59 | } 60 | 61 | 62 | private void getSelectedData() { 63 | String js = "(function getSelectedText() {" + 64 | "var txt;" + 65 | "if (window.getSelection) {" + 66 | "txt = window.getSelection().toString();" + 67 | "} else if (window.document.getSelection) {" + 68 | "txt = window.document.getSelection().toString();" + 69 | "} else if (window.document.selection) {" + 70 | "txt = window.document.selection.createRange().text;" + 71 | "}else{txt=\"\";}" + 72 | "CopyInterfaceCallback.onSelectionCallback(txt);" + 73 | "})()"; 74 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 75 | mWebView.evaluateJavascript("javascript:" + js, null); 76 | } else { 77 | mWebView.loadUrl("javascript:" + js); 78 | } 79 | } 80 | 81 | 82 | class SelectionCallback{ 83 | @JavascriptInterface 84 | public void onSelectionCallback(final String text){ 85 | if(text.length()>0){ 86 | new Utils.EditDialog(FrmBrowser.this, "复制文本", text) { 87 | @Override 88 | public void onConfirmText(String text) { 89 | setClipboard(text); 90 | Toast.makeText(FrmBrowser.this, "文本已复制到剪切板", Toast.LENGTH_SHORT).show(); 91 | } 92 | }.show(); 93 | } 94 | else{ 95 | Toast.makeText(FrmBrowser.this, "没有选中文本", Toast.LENGTH_SHORT).show(); 96 | } 97 | } 98 | 99 | } 100 | 101 | 102 | private int thisID = -1; 103 | @Override 104 | public void createAndAttachView(int id, FrameLayout frame) { 105 | baseUrl = Utils.getSP(this).getString("url",SettingActivity.defaultPage); 106 | thisID = id; 107 | isRunning = true; 108 | 109 | this.mWebView = new WebView(this); 110 | 111 | mWebView.addJavascriptInterface(new SelectionCallback(),"CopyInterfaceCallback"); 112 | 113 | frame.addView(mWebView); 114 | renderW = Utils.getSP(this).getInt("rw",1280); 115 | renderH = Utils.getSP(this).getInt("rh",720); 116 | 117 | WebGameBoostEngine.boost(this, mWebView, baseUrl, new WebGameBoostEngine.OnTitleChangedListener() { 118 | @Override 119 | public void onTitleChanged(String title) { 120 | setTitle(thisID,title+" - 电竞浏览器"); 121 | } 122 | 123 | @Override 124 | public void onIconChanged(Bitmap icon) { 125 | setIcon(thisID,icon); 126 | } 127 | }); 128 | 129 | 130 | 131 | FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mWebView.getLayoutParams(); 132 | frame.setClickable(true); 133 | lp.width=renderW; 134 | lp.height = renderH; 135 | mWebView.setLayoutParams(lp); 136 | lp.gravity = Gravity.CENTER; 137 | rootView = frame; 138 | scaleView(); 139 | 140 | mWebView.loadUrl(baseUrl); 141 | 142 | 143 | } 144 | 145 | int renderW=854,renderH=480; 146 | 147 | 148 | public void setClipboard(String text){ 149 | ClipboardManager manager = (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE); 150 | if (manager != null) { 151 | try { 152 | manager.setPrimaryClip(manager.getPrimaryClip()); 153 | manager.setPrimaryClip(ClipData.newPlainText(null, text)); 154 | } catch (Exception e) { 155 | e.printStackTrace(); 156 | } 157 | } 158 | } 159 | FrameLayout rootView; 160 | void scaleView(){ 161 | float pw = rootView.getWidth(); 162 | float ph = rootView.getHeight(); 163 | FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mWebView.getLayoutParams(); 164 | if(pw==0 || ph==0){ 165 | Log.e("SCALE","View not initialized"); 166 | return; 167 | } 168 | if(pw>=ph){ 169 | lp.width=renderW; 170 | lp.height = renderH; 171 | if(pw / renderW * renderH < ph){ 172 | //屏幕更高的场合 173 | mWebView.setScaleX(pw/renderW); 174 | mWebView.setScaleY(pw/renderW); 175 | } 176 | else{ 177 | //屏幕更窄的场合 178 | mWebView.setScaleX(ph/renderH); 179 | mWebView.setScaleY(ph/renderH); 180 | } 181 | mWebView.setRotation(0); 182 | } 183 | else{ 184 | lp.width=renderW; 185 | lp.height = renderH; 186 | 187 | float temp = pw; 188 | pw=ph;ph=temp; 189 | 190 | if(pw / renderW * renderH < ph){ 191 | //屏幕更高的场合 192 | mWebView.setScaleX(pw/renderW); 193 | mWebView.setScaleY(pw/renderW); 194 | } 195 | else{ 196 | //屏幕更窄的场合 197 | mWebView.setScaleX(ph/renderH); 198 | mWebView.setScaleY(ph/renderH); 199 | } 200 | 201 | mWebView.setRotation(reserveGraphic ? -90 : 90); 202 | } 203 | 204 | 205 | mWebView.setLayoutParams(lp); 206 | } 207 | 208 | @Override 209 | public void onResize(int id, Window window, View view, MotionEvent event) { 210 | super.onResize(id, window, view, event); 211 | if(null!=rootView){ 212 | scaleView(); 213 | } 214 | } 215 | 216 | public static String paste(Context context) { 217 | try { 218 | ClipboardManager cmb = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); 219 | if(cmb.getPrimaryClip().getItemCount() > 0) 220 | return cmb.getPrimaryClip().getItemAt(0).getText().toString().trim(); 221 | }catch (Exception ex){ 222 | ex.printStackTrace(); 223 | } 224 | return ""; 225 | } 226 | 227 | Handler hWnd = new Handler(); 228 | 229 | @Override 230 | public Animation getCloseAnimation(int id) { 231 | AnimationSet anims = new AnimationSet(false); 232 | StandOutLayoutParams orignal = getWindow(id).getLayoutParams(); 233 | ScaleAnimation scale = new ScaleAnimation(1,0.75f,1f,0.75f,orignal.width/2,orignal.height/2); 234 | scale.setDuration(400); 235 | scale.setFillAfter(true); 236 | scale.setInterpolator(this,android.R.anim.accelerate_interpolator); 237 | anims.addAnimation(scale); 238 | AlphaAnimation alpha = new AlphaAnimation(1,0); 239 | alpha.setDuration(300); 240 | alpha.setFillAfter(true); 241 | anims.addAnimation(alpha); 242 | isAnimShow = false; 243 | return anims; 244 | } 245 | 246 | @Override 247 | public Animation getShowAnimation(int id) { 248 | if(isAnimShow) { 249 | StandOutLayoutParams orignal = getWindow(id).getLayoutParams(); 250 | int sourceX = orignal.x + orignal.width /2; 251 | int sourceY = orignal.y + orignal.height /2; 252 | int destX = Utils.getSP(this).getInt("bx", 0) + dip2px(this,18); 253 | int destY = Utils.getSP(this).getInt("by", 0) + dip2px(this,18); 254 | 255 | float xscale = (float)dip2px(this,36) / (float)orignal.width; 256 | float yscale = (float)dip2px(this,36) / (float)orignal.height; 257 | 258 | AnimationSet anims = new AnimationSet(false); 259 | 260 | TranslateAnimation translate = new TranslateAnimation(destX-sourceX,0,destY-sourceY,0); 261 | translate.setDuration(400); 262 | translate.setFillAfter(true); 263 | translate.setInterpolator(this,android.R.anim.accelerate_interpolator); 264 | ScaleAnimation scale = new ScaleAnimation(xscale,1f,yscale,1f,orignal.width/2,orignal.height/2); 265 | scale.setDuration(400); 266 | scale.setFillAfter(true); 267 | scale.setInterpolator(this,android.R.anim.accelerate_interpolator); 268 | anims.addAnimation(scale); 269 | anims.addAnimation(translate); 270 | AlphaAnimation alpha = new AlphaAnimation(0,1); 271 | alpha.setDuration(300); 272 | alpha.setFillAfter(true); 273 | alpha.setStartOffset(100); 274 | anims.addAnimation(alpha); 275 | isAnimShow = false; 276 | return anims; 277 | } 278 | else{ 279 | 280 | AnimationSet anims = new AnimationSet(false); 281 | ScaleAnimation scale = new ScaleAnimation(0.75f,1f,0.75f,1f,ScaleAnimation.RELATIVE_TO_SELF,0.5f,ScaleAnimation.RELATIVE_TO_SELF,0.5f); 282 | scale.setDuration(400); 283 | scale.setFillAfter(true); 284 | scale.setInterpolator(this,android.R.anim.decelerate_interpolator); 285 | anims.addAnimation(scale); 286 | AlphaAnimation alpha = new AlphaAnimation(0,1); 287 | alpha.setDuration(300); 288 | alpha.setFillAfter(true); 289 | alpha.setStartOffset(100); 290 | anims.addAnimation(alpha); 291 | isAnimShow = false; 292 | return anims; 293 | } 294 | 295 | } 296 | 297 | @Override 298 | public Animation getHideAnimation(int id) { 299 | if(isAnimHide) { 300 | StandOutLayoutParams orignal = getWindow(id).getLayoutParams(); 301 | int sourceX = orignal.x + orignal.width /2; 302 | int sourceY = orignal.y + orignal.height /2; 303 | int destX = Utils.getSP(this).getInt("bx", 0) + dip2px(this,18); 304 | int destY = Utils.getSP(this).getInt("by", 0) + dip2px(this,18); 305 | 306 | float xscale = (float)dip2px(this,36) / (float)orignal.width; 307 | float yscale = (float)dip2px(this,36) / (float)orignal.height; 308 | 309 | AnimationSet anims = new AnimationSet(false); 310 | 311 | TranslateAnimation translate = new TranslateAnimation(0,destX-sourceX,0,destY-sourceY); 312 | translate.setDuration(400); 313 | translate.setFillAfter(true); 314 | translate.setInterpolator(this,android.R.anim.accelerate_interpolator); 315 | ScaleAnimation scale = new ScaleAnimation(1f,xscale,1f,yscale,orignal.width/2,orignal.height/2); 316 | scale.setDuration(400); 317 | scale.setFillAfter(true); 318 | scale.setInterpolator(this,android.R.anim.accelerate_interpolator); 319 | anims.addAnimation(scale); 320 | anims.addAnimation(translate); 321 | AlphaAnimation alpha = new AlphaAnimation(1,0); 322 | alpha.setDuration(300); 323 | alpha.setFillAfter(true); 324 | anims.addAnimation(alpha); 325 | isAnimHide = false; 326 | return anims; 327 | } 328 | else{ 329 | AnimationSet anims = new AnimationSet(false); 330 | StandOutLayoutParams orignal = getWindow(id).getLayoutParams(); 331 | ScaleAnimation scale = new ScaleAnimation(1,0.75f,1f,0.75f,orignal.width/2,orignal.height/2); 332 | scale.setDuration(400); 333 | scale.setFillAfter(true); 334 | scale.setInterpolator(this,android.R.anim.accelerate_interpolator); 335 | anims.addAnimation(scale); 336 | AlphaAnimation alpha = new AlphaAnimation(1,0); 337 | alpha.setDuration(300); 338 | alpha.setFillAfter(true); 339 | anims.addAnimation(alpha); 340 | isAnimShow = false; 341 | return anims; 342 | } 343 | } 344 | 345 | public static boolean isAnimShow = false; 346 | @Override 347 | public boolean onShow(int id, Window window) { 348 | 349 | StandOutLayoutParams lp = window.getLayoutParams(); 350 | 351 | lp.x = Utils.getSP(this).getInt("wx",100); 352 | lp.y = Utils.getSP(this).getInt("wy",100); 353 | lp.width = Utils.getSP(this).getInt("ww",dip2px(this,240)); 354 | lp.height = Utils.getSP(this).getInt("wh",dip2px(this,200)); 355 | 356 | 357 | window.setLayoutParams(lp); 358 | 359 | hWnd.postDelayed(resizer,300); 360 | 361 | return super.onShow(id, window); 362 | 363 | } 364 | 365 | public static boolean isAnimHide = false; 366 | @Override 367 | public boolean onHide(int id, Window window) { 368 | startService(new Intent(getApplicationContext(),WindowService.class)); 369 | Utils.getSP(this).edit().putInt("wx",window.getLayoutParams().x) 370 | .putInt("wy",window.getLayoutParams().y) 371 | .putInt("ww",window.getLayoutParams().width) 372 | .putInt("wh",window.getLayoutParams().height).commit(); 373 | isAnimHide = true; 374 | return super.onHide(id, window); 375 | } 376 | @Override 377 | public boolean onClose(int id, Window window) { 378 | Utils.getSP(this).edit().putInt("wx",window.getLayoutParams().x) 379 | .putInt("wy",window.getLayoutParams().y) 380 | .putInt("ww",window.getLayoutParams().width) 381 | .putInt("wh",window.getLayoutParams().height).commit(); 382 | mWebView.destroy(); 383 | isRunning = false; 384 | return super.onClose(id, window); 385 | } 386 | @Override 387 | public List getDropDownItems(int id) { 388 | List list = new ArrayList<>(); 389 | list.add(new DropDownListItem(android.R.drawable.ic_menu_rotate,"刷新",new Runnable(){ 390 | @Override 391 | public void run() { 392 | Utils.Confirm(getApplicationContext(), "是否重新载入?", new Runnable() { 393 | @Override 394 | public void run() { 395 | mWebView.reload(); 396 | } 397 | }); 398 | } 399 | })); 400 | list.add(new DropDownListItem(android.R.drawable.ic_menu_view, "隐藏", new Runnable() { 401 | @Override 402 | public void run() { 403 | hide(StandOutWindow.DEFAULT_ID); 404 | } 405 | })); 406 | 407 | 408 | list.add(new DropDownListItem(android.R.drawable.ic_menu_close_clear_cancel, "退出", new Runnable() { 409 | @Override 410 | public void run() { 411 | Utils.Confirm(getApplicationContext(), "是否退出?", new Runnable() { 412 | @Override 413 | public void run() { 414 | close(StandOutWindow.DEFAULT_ID); 415 | } 416 | }); 417 | } 418 | })); 419 | 420 | list.add(new DropDownListItem(android.R.drawable.ic_menu_edit, "复制选中内容", new Runnable() { 421 | @Override 422 | public void run() { 423 | getSelectedData(); 424 | } 425 | })); 426 | 427 | list.add(new DropDownListItem(android.R.drawable.ic_menu_crop, "翻转画面", new Runnable() { 428 | @Override 429 | public void run() { 430 | reserveGraphic=!reserveGraphic; 431 | hWnd.post(resizer); 432 | } 433 | })); 434 | return list; 435 | } 436 | 437 | private boolean reserveGraphic = false; 438 | 439 | @SuppressWarnings("WrongConstant") 440 | @Override 441 | public StandOutLayoutParams getParams(int id, Window window) { 442 | StandOutLayoutParams slp = new StandOutLayoutParams(id, dip2px(this,240), dip2px(this,200), 443 | StandOutLayoutParams.CENTER, StandOutLayoutParams.CENTER,dip2px(this,200), dip2px(this,150)); 444 | slp.type = Utils.getFlagCompat(); 445 | return slp; 446 | } 447 | 448 | public int getFlags(int id) { 449 | int flat = super.getFlags(id) 450 | |StandOutFlags.FLAG_WINDOW_HIDE_ENABLE 451 | |StandOutFlags.FLAG_BODY_MOVE_ENABLE 452 | |StandOutFlags.FLAG_DECORATION_SYSTEM; 453 | 454 | return flat; 455 | } 456 | 457 | @Override 458 | public String getPersistentNotificationMessage(int id) { 459 | return getResources().getString(R.string.app_name)+" 正在使用窗口"; 460 | } 461 | 462 | Runnable resizer = new Runnable() { 463 | @Override 464 | public void run() { 465 | if(null!=rootView){ 466 | if(rootView.getWidth()>0) { 467 | scaleView(); 468 | return; 469 | } 470 | } 471 | hWnd.postDelayed(this,300); 472 | } 473 | }; 474 | 475 | @Override 476 | public void onWindowStateChanged(int id, Window window, View view) { 477 | super.onWindowStateChanged(id, window, view); 478 | hWnd.postDelayed(resizer,300); 479 | } 480 | 481 | @Override 482 | public Notification getPersistentNotification(int id) { 483 | if (Build.VERSION.SDK_INT >= 26) { 484 | NotificationChannel channel = new NotificationChannel("Windows","窗口服务驻留通知", NotificationManager.IMPORTANCE_LOW); 485 | NotificationManager manager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE); 486 | manager.createNotificationChannel(channel); 487 | } 488 | int icon = R.drawable.ic_notification_icon; 489 | long when = System.currentTimeMillis(); 490 | Context c = getApplicationContext(); 491 | String contentTitle = getPersistentNotificationTitle(id); 492 | String contentText = getPersistentNotificationMessage(id); 493 | String tickerText = String.format("%s: %s", contentTitle, contentText); 494 | 495 | // getPersistentNotification() is called for every new window 496 | // so we replace the old notification with a new one that has 497 | // a bigger id 498 | Intent notificationIntent = getPersistentNotificationIntent(id); 499 | 500 | PendingIntent contentIntent = null; 501 | 502 | if (notificationIntent != null) { 503 | contentIntent = PendingIntent.getService(this, 0, 504 | notificationIntent, 505 | // flag updates existing persistent notification 506 | PendingIntent.FLAG_UPDATE_CURRENT); 507 | } 508 | 509 | Notification.Builder notification =null; 510 | 511 | if (Build.VERSION.SDK_INT >= 26) { 512 | notification = new Notification.Builder(c,"Windows"); 513 | } 514 | else{ 515 | notification= new Notification.Builder(c); 516 | } 517 | notification.setSmallIcon(icon); 518 | notification.setTicker(tickerText); 519 | notification.setWhen(when); 520 | 521 | notification.setContentText(contentTitle); 522 | notification.setContentText(contentText); 523 | notification.setContentIntent(contentIntent); 524 | 525 | //notification.setLatestEventInfo(c, contentTitle, contentText,contentIntent); 526 | 527 | Notification noti = notification.build(); 528 | 529 | return noti; 530 | } 531 | 532 | @Override 533 | public boolean onFocusChange(int id, Window window, boolean focus) { 534 | if(focus){ 535 | window.setAlpha(1.0f); 536 | } 537 | else{ 538 | window.setAlpha(0.7f); 539 | } 540 | 541 | return super.onFocusChange(id, window, focus); 542 | } 543 | 544 | @Override 545 | public void onCustomButton1Click(final int id) { 546 | super.onCustomButton1Click(id); 547 | } 548 | 549 | @Override 550 | public Notification getHiddenNotification(int id) { 551 | return null; 552 | } 553 | 554 | @Override 555 | public void onDestroy() { 556 | super.onDestroy(); 557 | rootView.removeAllViews(); 558 | try{ 559 | mWebView.destroy(); 560 | }catch (Exception ex){} 561 | } 562 | 563 | public static int px2dip(Context context, float pxValue) { 564 | final float scale = context.getResources().getDisplayMetrics().density; 565 | return (int) (pxValue / scale + 0.5f); 566 | } 567 | public static int dip2px(Context context, float dpValue) { 568 | float scale = context.getResources().getDisplayMetrics().density; 569 | return (int) (dpValue * scale + 0.5f); 570 | } 571 | 572 | } 573 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/gamebrowser/FullScreenActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.gamebrowser; 2 | 3 | import android.app.Activity; 4 | import android.app.AlertDialog; 5 | import android.content.ClipData; 6 | import android.content.ClipboardManager; 7 | import android.content.Context; 8 | import android.content.DialogInterface; 9 | import android.content.res.Configuration; 10 | import android.os.Build; 11 | import android.os.Bundle; 12 | import android.os.Handler; 13 | import android.util.Log; 14 | import android.view.Gravity; 15 | import android.view.View; 16 | import android.webkit.WebView; 17 | import android.widget.FrameLayout; 18 | 19 | import java.util.HashMap; 20 | import java.util.regex.Matcher; 21 | import java.util.regex.Pattern; 22 | 23 | import wei.mark.standout.Utils; 24 | 25 | 26 | public class FullScreenActivity extends Activity { 27 | public HashMap clipboardFinder = new HashMap<>(); 28 | public static String baseUrl = SettingActivity.defaultPage; 29 | 30 | public static boolean isRunning = false; 31 | 32 | WebView mWebView; 33 | @Override 34 | protected void onCreate(Bundle savedInstanceState) { 35 | super.onCreate(savedInstanceState); 36 | setContentView(R.layout.fullscreen); 37 | isRunning = true; 38 | rootView = findViewById(R.id.rootView); 39 | FrameLayout frame = rootView; 40 | baseUrl = Utils.getSP(this).getString("url",SettingActivity.defaultPage); 41 | 42 | if(null==mWebView) { 43 | this.mWebView = new WebView(this); 44 | WebGameBoostEngine.boost(this,mWebView,baseUrl,null); 45 | } 46 | frame.addView(mWebView); 47 | renderW = Utils.getSP(this).getInt("rw",854); 48 | renderH = Utils.getSP(this).getInt("rh",480); 49 | //this.mWebView.setWebViewClient(new baseUrl(this)); 50 | 51 | hWnd.postDelayed(resizer,1000); 52 | FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mWebView.getLayoutParams(); 53 | frame.setClickable(true); 54 | lp.width=renderW; 55 | lp.height = renderH; 56 | mWebView.setLayoutParams(lp); 57 | lp.gravity = Gravity.CENTER; 58 | rootView = frame; 59 | 60 | 61 | 62 | mWebView.loadUrl(baseUrl); 63 | 64 | } 65 | 66 | int renderW=854,renderH=480; 67 | 68 | 69 | Runnable resizer = new Runnable() { 70 | @Override 71 | public void run() { 72 | hWnd.removeCallbacks(this); 73 | if(null!=rootView){ 74 | if(rootView.getWidth()>0) { 75 | scaleView(); 76 | return; 77 | } 78 | } 79 | hWnd.postDelayed(this,1000); 80 | } 81 | }; 82 | 83 | 84 | FrameLayout rootView; 85 | void scaleView(){ 86 | float pw = rootView.getWidth(); 87 | float ph = rootView.getHeight(); 88 | FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mWebView.getLayoutParams(); 89 | if(pw==0 || ph==0){ 90 | Log.e("SCALE","View not initialized"); 91 | hWnd.postDelayed(resizer,1000); 92 | return; 93 | } 94 | { 95 | lp.width=renderW; 96 | lp.height = renderH; 97 | if(pw / renderW * renderH < ph){ 98 | //屏幕更高的场合 99 | mWebView.setScaleX(pw/renderW); 100 | mWebView.setScaleY(pw/renderW); 101 | } 102 | else{ 103 | //屏幕更窄的场合 104 | mWebView.setScaleX(ph/renderH); 105 | mWebView.setScaleY(ph/renderH); 106 | } 107 | 108 | } 109 | 110 | 111 | 112 | mWebView.setLayoutParams(lp); 113 | } 114 | 115 | 116 | @Override 117 | public void onConfigurationChanged(Configuration newConfig) { 118 | super.onConfigurationChanged(newConfig); 119 | if(null!=rootView){ 120 | hWnd.postDelayed(resizer,1000); 121 | } 122 | } 123 | 124 | 125 | Handler hWnd = new Handler(); 126 | 127 | @Override 128 | protected void onDestroy() { 129 | super.onDestroy(); 130 | isRunning = false; 131 | mWebView.destroy(); 132 | } 133 | 134 | public void onHide() { 135 | //4.1及以上通用flags组合 136 | int flags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE 137 | | View.SYSTEM_UI_FLAG_FULLSCREEN 138 | | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 139 | | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; 140 | 141 | 142 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 143 | getWindow().getDecorView().setSystemUiVisibility( 144 | flags | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 145 | | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY 146 | ); 147 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { 148 | getWindow().getDecorView().setSystemUiVisibility(flags); 149 | } 150 | } 151 | 152 | @Override 153 | public void onWindowFocusChanged(boolean hasFocus) { 154 | if(hasFocus){ 155 | onHide(); 156 | hWnd.postDelayed(resizer,1000); 157 | } 158 | } 159 | 160 | @Override 161 | public void onBackPressed() { 162 | new AlertDialog.Builder(this).setTitle("是否退出?").setPositiveButton("是的", new DialogInterface.OnClickListener() { 163 | @Override 164 | public void onClick(DialogInterface dialog, int which) { 165 | finish(); 166 | } 167 | }).setNegativeButton("不是",null).setNeutralButton("刷新页面", new DialogInterface.OnClickListener() { 168 | @Override 169 | public void onClick(DialogInterface dialog, int which) { 170 | mWebView.loadUrl(baseUrl); 171 | } 172 | }).create().show(); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/gamebrowser/LauncherActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.gamebrowser; 2 | 3 | import android.Manifest; 4 | import android.app.Activity; 5 | import android.content.Intent; 6 | import android.content.pm.PackageManager; 7 | import android.os.Build; 8 | import android.os.Bundle; 9 | 10 | import android.provider.Settings; 11 | import android.view.View; 12 | import android.widget.Toast; 13 | 14 | import wei.mark.standout.StandOutWindow; 15 | import wei.mark.standout.Utils; 16 | 17 | public class LauncherActivity extends Activity { 18 | 19 | public static final int OVERLAY_PERMISSION_REQ_CODE = 4331; 20 | //public static final int STORAGE_PERMISSION_REQ_CODE = 4332; 21 | 22 | 23 | @Override 24 | protected void onCreate(Bundle savedInstanceState) { 25 | super.onCreate(savedInstanceState); 26 | setContentView(R.layout.launcher); 27 | 28 | if(hasFloatWindow()){ 29 | findViewById(R.id.btnFloat).setVisibility(View.GONE); 30 | } 31 | 32 | // if(hasStorage()){ 33 | findViewById(R.id.btnStorage).setVisibility(View.GONE); 34 | // } 35 | 36 | Utils.showDialogVersion(this,"firstrun", 2,"第一次使用,请先阅读 帮助 里的 功能介绍。阅读一次之后该提示将不再显示"); 37 | 38 | } 39 | 40 | public boolean hasFloatWindow(){ 41 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 42 | return Settings.canDrawOverlays(this); 43 | } 44 | return true; 45 | } 46 | 47 | // public boolean hasStorage(){ 48 | // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 49 | // return checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)== PackageManager.PERMISSION_GRANTED; 50 | // } 51 | // return true; 52 | // } 53 | 54 | public void startService(){ 55 | if(Utils.getSP(this).getBoolean("clear",false)) { 56 | Utils.getSP(this).edit() 57 | .remove("wx") 58 | .remove("wy") 59 | .remove("ww") 60 | .remove("wh") 61 | .remove("bx") 62 | .remove("by") 63 | .remove("clear") 64 | .commit(); 65 | } 66 | StandOutWindow.show(this, FrmBrowser.class, StandOutWindow.DEFAULT_ID); 67 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 68 | finishAndRemoveTask(); 69 | } 70 | else{ 71 | finish(); 72 | } 73 | } 74 | 75 | public void start(View view) { 76 | 77 | if(!hasFloatWindow()){ 78 | Toast.makeText(this, "悬浮窗权限没有获取", Toast.LENGTH_SHORT).show(); 79 | return; 80 | } 81 | // if(!hasStorage()) { 82 | // Toast.makeText(this, "存储权限没有获取,无法使用加速功能", Toast.LENGTH_SHORT).show(); 83 | // return; 84 | // } 85 | 86 | if(FullScreenActivity.isRunning){ 87 | Toast.makeText(this, "禁止多开。如果你没有多开,请强行停止本应用以示清白", Toast.LENGTH_SHORT).show(); 88 | return; 89 | } 90 | startAfterPermission(); 91 | } 92 | 93 | void startAfterPermission(){ 94 | startService(); 95 | try { 96 | stopService(new Intent(getApplicationContext(), WindowService.class)); 97 | }catch (Exception ex){} 98 | } 99 | 100 | public void floatwindow(View view) { 101 | if(Build.VERSION.SDK_INT>=23) 102 | { 103 | if(hasFloatWindow()) 104 | { 105 | Toast.makeText(this, "授权成功", Toast.LENGTH_SHORT).show(); 106 | }else{ 107 | try{ 108 | Intent intent=new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION); 109 | startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE); 110 | }catch (Exception e) 111 | { 112 | e.printStackTrace(); 113 | Toast.makeText(this, "手机不兼容,建议换一部", Toast.LENGTH_SHORT).show(); 114 | } 115 | } 116 | } else{ 117 | Toast.makeText(this, "授权成功", Toast.LENGTH_SHORT).show(); 118 | } 119 | } 120 | 121 | public void storage(View view) { 122 | if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.M){ 123 | // if (!hasStorage()){ 124 | // requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},STORAGE_PERMISSION_REQ_CODE); 125 | // }else { 126 | // Toast.makeText(this, "授权成功", Toast.LENGTH_SHORT).show(); 127 | // } 128 | }else { 129 | Toast.makeText(this, "授权成功", Toast.LENGTH_SHORT).show(); 130 | } 131 | } 132 | 133 | public void close(View view) { 134 | finish(); 135 | } 136 | 137 | public void startfull(View view) { 138 | // if(!hasStorage()) { 139 | // Toast.makeText(this, "存储权限没有获取,无法使用加速功能", Toast.LENGTH_SHORT).show(); 140 | // return; 141 | // } 142 | 143 | if(FrmBrowser.isRunning){ 144 | Toast.makeText(this, "禁止多开。如果你没有多开,请强行停止本应用以示清白", Toast.LENGTH_SHORT).show(); 145 | return; 146 | } 147 | 148 | startActivity(new Intent(this,FullScreenActivity.class)); 149 | finish(); 150 | } 151 | 152 | 153 | @Override 154 | public void onWindowFocusChanged(boolean hasFocus) { 155 | super.onWindowFocusChanged(hasFocus); 156 | if(hasFocus){ 157 | if(hasFloatWindow()){ 158 | findViewById(R.id.btnFloat).setVisibility(View.GONE); 159 | } 160 | 161 | // if(hasStorage()){ 162 | // findViewById(R.id.btnStorage).setVisibility(View.GONE); 163 | // } 164 | } 165 | } 166 | 167 | public void setting(View view) { 168 | startActivity(new Intent(this,SettingActivity.class)); 169 | } 170 | 171 | public void help(View view) { 172 | startActivity(new Intent(this,AboutActivity.class)); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/gamebrowser/PluginActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.gamebrowser; 2 | 3 | import android.app.Activity; 4 | import android.app.AlertDialog; 5 | import android.app.ProgressDialog; 6 | import android.content.DialogInterface; 7 | import android.content.Intent; 8 | import android.content.pm.PackageInfo; 9 | import android.content.pm.PackageManager; 10 | import android.content.pm.ResolveInfo; 11 | import android.graphics.drawable.Drawable; 12 | import android.os.AsyncTask; 13 | import android.os.Bundle; 14 | import android.util.Log; 15 | import android.view.LayoutInflater; 16 | import android.view.View; 17 | import android.view.ViewGroup; 18 | import android.widget.BaseAdapter; 19 | import android.widget.CheckBox; 20 | import android.widget.CompoundButton; 21 | import android.widget.ImageView; 22 | import android.widget.LinearLayout; 23 | import android.widget.ListView; 24 | import android.widget.TextView; 25 | import android.widget.Toast; 26 | 27 | import org.json.JSONArray; 28 | import org.json.JSONException; 29 | import org.json.JSONObject; 30 | 31 | import java.io.BufferedReader; 32 | import java.io.ByteArrayOutputStream; 33 | import java.io.File; 34 | import java.io.FileOutputStream; 35 | import java.io.IOException; 36 | import java.io.InputStream; 37 | import java.io.InputStreamReader; 38 | import java.io.OutputStream; 39 | import java.util.ArrayList; 40 | import java.util.Collections; 41 | import java.util.Comparator; 42 | import java.util.LinkedHashSet; 43 | import java.util.List; 44 | import java.util.Set; 45 | import java.util.zip.ZipEntry; 46 | import java.util.zip.ZipFile; 47 | 48 | import wei.mark.standout.Utils; 49 | 50 | import static android.view.View.GONE; 51 | 52 | public class PluginActivity extends Activity { 53 | 54 | @Override 55 | protected void onCreate(Bundle savedInstanceState) { 56 | super.onCreate(savedInstanceState); 57 | setTitle("插件列表"); 58 | setContentView(R.layout.activity_plugin); 59 | new Thread(){ 60 | @Override 61 | public void run() { 62 | Intent intent = new Intent(Intent.ACTION_MAIN,null); 63 | intent.addCategory("com.example.gamebrowser.ADDONS"); 64 | List mApps = getPackageManager().queryIntentActivities(intent, 0); 65 | apps.clear(); 66 | for (ResolveInfo info : mApps) { 67 | try { 68 | PackageInfo pkg = getPackageManager().getPackageInfo(info.activityInfo.packageName,0); 69 | String path = pkg.applicationInfo.sourceDir; 70 | String version = pkg.versionName; 71 | String desc = info.activityInfo.metaData.getString("gb-addons-desc","无描述"); 72 | String author = info.activityInfo.metaData.getString("gb-addons-author","未知"); 73 | apps.add(new AppInfo(info.activityInfo.loadIcon(getPackageManager()),info.activityInfo.loadLabel(getPackageManager()).toString(),path,desc,version,author,pkg.packageName)); 74 | } catch (PackageManager.NameNotFoundException e) { 75 | e.printStackTrace(); 76 | } 77 | 78 | } 79 | Collections.sort(apps,new Comparator() { 80 | @Override 81 | public int compare(AppInfo o1, AppInfo o2) { 82 | return o1.name.compareTo(o2.name); 83 | } 84 | }); 85 | runOnUiThread(new Runnable() { 86 | @Override 87 | public void run() { 88 | initList(); 89 | } 90 | }); 91 | } 92 | }.start(); 93 | 94 | findViewById(R.id.btnPlugin).setEnabled(Utils.getSP(this).getBoolean("accept_plugin2",false)); 95 | } 96 | 97 | List apps = new ArrayList<>(); 98 | 99 | 100 | 101 | class AppListViewHolder 102 | { 103 | ImageView viewImg; 104 | TextView viewName; 105 | TextView viewDesc; 106 | TextView viewVer; 107 | TextView viewAuthor; 108 | CheckBox chkEnabled; 109 | } 110 | class AppInfo{ 111 | Drawable bmp;String name; 112 | String apppath; 113 | String desc,version; 114 | String author; 115 | String pkgName; 116 | public AppInfo(Drawable bmp, String name,String apppath,String desc,String version,String author,String pkgName) { 117 | this.bmp = bmp; 118 | this.name = name; 119 | this.apppath = apppath; 120 | this.desc = desc; 121 | this.version = version; 122 | this.author = author; 123 | this.pkgName=pkgName; 124 | } 125 | } 126 | Set enabledPlugin = new LinkedHashSet<>(); 127 | void initList(){ 128 | enabledPlugin.clear(); 129 | Set temp = Utils.getSP(this).getStringSet("enabled_plugin",null); 130 | if(null!=temp) { 131 | enabledPlugin.addAll(temp); 132 | } 133 | findViewById(R.id.proLoading).setVisibility(GONE); 134 | findViewById(R.id.listPlugin).setVisibility(View.VISIBLE); 135 | ((ListView)findViewById(R.id.listPlugin)).setAdapter(new AppAdapter()); 136 | } 137 | 138 | void saveList(){ 139 | Utils.getSP(this).edit().putStringSet("enabled_plugin",enabledPlugin).apply(); 140 | unsave=true; 141 | } 142 | 143 | class AppAdapter extends BaseAdapter { 144 | public AppAdapter() { 145 | } 146 | @Override 147 | public View getView(int position, View convertView, ViewGroup parent) 148 | { 149 | LinearLayout layout; 150 | AppListViewHolder holder = new AppListViewHolder(); 151 | if(convertView == null) 152 | { 153 | LayoutInflater inflater = getLayoutInflater(); 154 | layout = (LinearLayout) inflater.inflate(R.layout.adapter_app, null); 155 | 156 | holder.viewImg = layout.findViewById(R.id.viewImg); 157 | holder.viewName = layout.findViewById(R.id.viewName); 158 | holder.viewDesc = layout.findViewById(R.id.viewDesc); 159 | holder.viewVer = layout.findViewById(R.id.viewVer); 160 | holder.viewAuthor = layout.findViewById(R.id.viewAuthor); 161 | holder.chkEnabled=layout.findViewById(R.id.chkEnabled); 162 | layout.setTag(holder); 163 | } 164 | else 165 | { 166 | layout = (LinearLayout) convertView; 167 | holder = (AppListViewHolder) layout.getTag(); 168 | } 169 | 170 | AppInfo info = (AppInfo) getItem(position); 171 | holder.viewImg.setImageDrawable(info.bmp); 172 | holder.viewName.setText(info.name); 173 | holder.viewVer.setText(info.version); 174 | holder.viewDesc.setText(info.desc); 175 | holder.viewAuthor.setText("作者:"+info.author); 176 | holder.chkEnabled.setOnCheckedChangeListener(null); 177 | holder.chkEnabled.setChecked(enabledPlugin.contains(info.pkgName)); 178 | holder.chkEnabled.setOnCheckedChangeListener(new PluginEnabler(info.pkgName)); 179 | return layout; 180 | } 181 | 182 | class PluginEnabler implements CompoundButton.OnCheckedChangeListener{ 183 | String pkgName; 184 | 185 | public PluginEnabler(String pkgName) { 186 | this.pkgName = pkgName; 187 | } 188 | 189 | @Override 190 | public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 191 | if(isChecked){ 192 | if(!enabledPlugin.contains(pkgName)){ 193 | enabledPlugin.add(pkgName); 194 | } 195 | } 196 | else{ 197 | if(enabledPlugin.contains(pkgName)){ 198 | enabledPlugin.remove(pkgName); 199 | } 200 | } 201 | saveList(); 202 | } 203 | } 204 | 205 | @Override 206 | public long getItemId(int position) 207 | { 208 | return position; 209 | } 210 | 211 | @Override 212 | public Object getItem(int position) 213 | { 214 | return apps.get(position ); 215 | } 216 | 217 | @Override 218 | public int getCount() 219 | { 220 | return apps.size(); 221 | } 222 | } 223 | 224 | public void pluginhelp(View view) { 225 | String msg="插件说明:\n" + 226 | "\n" + 227 | "第一次使用,每次安装或卸载插件时都需要 载入插件一次\n" + 228 | "\n" + 229 | "注意:\n" + 230 | "由于插件的特殊性质,禁止滥用插件功能,包括但不限于开发,制作,分发,出售,购买,使用任何影响游戏平衡性,解锁付费内容等性质的插件,否则可能会导致账号被封,或者被追究法律责任。\n" + 231 | "解锁插件功能,即表示您已阅读并知晓以上内容,并愿意承担滥用插件功能造成的一切后果。\n" + 232 | "\n\n" + 233 | "警告:\n" + 234 | "请从信任的地方获取插件。一些木马程序可能会伪装成插件,请自行甄别。"; 235 | AlertDialog ald = new AlertDialog.Builder(this).setTitle("功能介绍").setMessage(msg).setPositiveButton("解锁插件功能", new DialogInterface.OnClickListener() { 236 | @Override 237 | public void onClick(DialogInterface dialog, int which) { 238 | findViewById(R.id.btnPlugin).setEnabled(true); 239 | Utils.getSP(PluginActivity.this).edit().putBoolean("accept_plugin2",true).commit(); 240 | } 241 | }).setNegativeButton(android.R.string.cancel,null).create(); 242 | ald.show(); 243 | } 244 | 245 | // Indicates changes of plugin list. 246 | boolean unsave=false; 247 | 248 | @SuppressWarnings("all") 249 | public void loadPlugin(View view) { 250 | new AsyncTask,String,String>(){ 251 | ProgressDialog pdd; 252 | @Override 253 | protected void onPreExecute() { 254 | super.onPreExecute(); 255 | pdd = ProgressDialog.show(PluginActivity.this,"请稍后","正在载入插件...",true,false); 256 | } 257 | @Override 258 | protected void onProgressUpdate(String... values) { 259 | pdd.setMessage(values[0]); 260 | } 261 | @Override 262 | protected void onPostExecute(String s) { 263 | pdd.dismiss(); 264 | Utils.showDialog(PluginActivity.this,s); 265 | unsave=false; 266 | super.onPostExecute(s); 267 | } 268 | 269 | 270 | @Override 271 | protected String doInBackground(List... params) { 272 | StringBuilder result = new StringBuilder("插件加载:\r\n"); 273 | publishProgress("正在清理插件缓存..."); 274 | 275 | 276 | for (SettingActivity.GameEntry server : 277 | SettingActivity.getGameList(PluginActivity.this)) { 278 | try { 279 | cleanDir(getPatchDir(server.uuid)); 280 | cleanDir(getModDir(server.uuid)); 281 | }catch (Exception ex){ 282 | return "插件缓存清理失败"; 283 | } 284 | } 285 | for (AppInfo info: params[0]) { 286 | if(!enabledPlugin.contains(info.pkgName)){continue;} 287 | publishProgress("正在加载 "+info.name); 288 | try{ 289 | ZipFile zipf = new ZipFile(info.apppath); 290 | ZipEntry indexEntry = zipf.getEntry("assets/index.json"); 291 | Log.w("Plugin Install","Reading index file..."); 292 | ByteArrayOutputStream bos = new ByteArrayOutputStream(); 293 | InputStream indexreader = zipf.getInputStream(indexEntry); 294 | copyStream(indexreader,bos); 295 | JSONArray jarr = new JSONArray(new String(bos.toByteArray(),"utf-8")); 296 | indexreader.close(); 297 | bos.close(); 298 | Log.w("Plugin Install","Begining extract..."); 299 | for (int i = 0; i < jarr.length(); i++) { 300 | JSONObject jobj = jarr.getJSONObject(i); 301 | String sourceFile = jobj.getString("file"); 302 | String targetserver = jobj.getString("gameuid"); 303 | String filetype = jobj.getString("type"); 304 | String destPath = jobj.getString("path"); 305 | Log.w("Plugin Install","Extract:"+destPath); 306 | ZipEntry src = zipf.getEntry("assets/"+sourceFile); 307 | File fout =new File(getAddonRootDir(targetserver,filetype)+destPath); 308 | if(!fout.getParentFile().exists()){fout.getParentFile().mkdirs();} 309 | if(fout.exists()){fout.delete();} 310 | fout.createNewFile(); 311 | InputStream zin = zipf.getInputStream(src); 312 | FileOutputStream fos = new FileOutputStream(fout,false); 313 | copyStream(zin,fos); 314 | zin.close(); 315 | fos.close(); 316 | } 317 | zipf.close(); 318 | result.append("加载 "+info.name+" 成功\r\n"); 319 | }catch (Exception ex){ 320 | ex.printStackTrace(); 321 | result.append("加载 "+info.name+" 失败:"+ex.getClass().getName()+":"+ex.getMessage()+"\r\n"); 322 | } 323 | } 324 | return result.toString(); 325 | } 326 | }.execute(apps); 327 | } 328 | 329 | @Override 330 | public void onBackPressed() { 331 | if(unsave && findViewById(R.id.btnPlugin).isEnabled()){ 332 | loadPlugin(null); 333 | return; 334 | } 335 | super.onBackPressed(); 336 | } 337 | 338 | void copyStream(InputStream in, OutputStream out) throws IOException { 339 | byte[] buffer = new byte[1024]; 340 | int len=0; 341 | while ((len=in.read(buffer,0,buffer.length))>0){ 342 | out.write(buffer,0,len); 343 | } 344 | } 345 | 346 | 347 | 348 | String getPatchDir(String cachepref) { 349 | String path = getFilesDir().getAbsolutePath(); 350 | if (!path.endsWith("/")) { 351 | path += "/"; 352 | } 353 | return path +cachepref+ "/patch/"; 354 | } 355 | String getModDir(String cachepref) { 356 | String path = getFilesDir().getAbsolutePath(); 357 | if (!path.endsWith("/")) { 358 | path += "/"; 359 | } 360 | return path +cachepref+ "/mods/"; 361 | } 362 | 363 | String getAddonRootDir(String cachepref,String type) { 364 | String path = getFilesDir().getAbsolutePath(); 365 | if (!path.endsWith("/")) { 366 | path += "/"; 367 | } 368 | return path +cachepref+ "/"+type+"/"; 369 | } 370 | 371 | public static void cleanDir(String dirPath){ 372 | File file = new File(dirPath); 373 | if(!file.exists()){file.mkdirs();} 374 | File[] files = file.listFiles(); 375 | if(files!=null){ 376 | for (File f: 377 | files) { 378 | deleteDir(f.getAbsolutePath()); 379 | } 380 | } 381 | } 382 | 383 | 384 | 385 | public static void deleteDir(String dirPath) 386 | { 387 | Log.w("Plugin Cleanup","Delete: "+dirPath); 388 | File file = new File(dirPath); 389 | if(!file.exists()){return;} 390 | if(file.isFile()) 391 | { 392 | file.delete(); 393 | }else 394 | { 395 | File[] files = file.listFiles(); 396 | if(files == null) 397 | { 398 | file.delete(); 399 | }else 400 | { 401 | for (int i = 0; i < files.length; i++) 402 | { 403 | deleteDir(files[i].getAbsolutePath()); 404 | } 405 | file.delete(); 406 | } 407 | } 408 | } 409 | } 410 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/gamebrowser/SettingActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.gamebrowser; 2 | 3 | import android.Manifest; 4 | import android.app.Activity; 5 | import android.app.AlertDialog; 6 | import android.content.Context; 7 | import android.content.DialogInterface; 8 | import android.content.Intent; 9 | import android.content.pm.PackageManager; 10 | import android.net.Uri; 11 | import android.os.Build; 12 | import android.os.Bundle; 13 | import android.view.View; 14 | import android.view.ViewGroup; 15 | import android.widget.ArrayAdapter; 16 | import android.widget.TextView; 17 | import android.widget.Toast; 18 | 19 | import org.json.JSONArray; 20 | import org.json.JSONException; 21 | import org.json.JSONObject; 22 | 23 | import java.io.BufferedReader; 24 | import java.io.IOException; 25 | import java.io.InputStreamReader; 26 | import java.io.PrintStream; 27 | import java.util.ArrayList; 28 | import java.util.List; 29 | 30 | import wei.mark.standout.StandOutWindow; 31 | import wei.mark.standout.Utils; 32 | 33 | import static wei.mark.standout.Utils.no; 34 | import static wei.mark.standout.Utils.yes; 35 | 36 | public class SettingActivity extends Activity { 37 | public static final String defaultPage = "file:////android_asset/help.html"; 38 | @Override 39 | protected void onCreate(Bundle savedInstanceState) { 40 | super.onCreate(savedInstanceState); 41 | setContentView(R.layout.activity_setting); 42 | } 43 | 44 | // public boolean hasStorage(){ 45 | // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 46 | // return checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)== PackageManager.PERMISSION_GRANTED; 47 | // } 48 | // return true; 49 | // } 50 | 51 | // public void debug(View view) { 52 | // if(!hasStorage()){ 53 | // Toast.makeText(this, "存储权限没有获取", Toast.LENGTH_SHORT).show(); 54 | // return; 55 | // } 56 | // 57 | // try { 58 | // Process ps = Runtime.getRuntime().exec("sh"); 59 | // PrintStream p = new PrintStream(ps.getOutputStream()); 60 | // p.println("logcat -v time > /sdcard/不科学的Logcat.txt"); 61 | // p.println(); 62 | // p.close(); 63 | // view.setEnabled(false); 64 | // } catch (IOException e) { 65 | // e.printStackTrace(); 66 | // Toast.makeText(this, "无法获取Logcat,"+e.getLocalizedMessage(), Toast.LENGTH_SHORT).show(); 67 | // } 68 | // 69 | // } 70 | 71 | public void resetposition(View view) { 72 | 73 | Confirm(this, "是否重置位置?这将会停止当前的游戏。", new Runnable() { 74 | @Override 75 | public void run() { 76 | clearandreset(); 77 | } 78 | }); 79 | } 80 | 81 | public void appinfo(View view) { 82 | Intent mIntent = new Intent(); 83 | mIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 84 | mIntent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS"); 85 | mIntent.setData(Uri.fromParts("package", getPackageName(), null)); 86 | startActivity(mIntent); 87 | } 88 | 89 | void Confirm(Context ctx, String msg, final Runnable action){ 90 | AlertDialog ald = new AlertDialog.Builder(ctx).setMessage(msg).setPositiveButton(yes, new DialogInterface.OnClickListener() { 91 | @Override 92 | public void onClick(DialogInterface dialogInterface, int i) { 93 | action.run(); 94 | } 95 | }).setNegativeButton(no,null).create(); 96 | ald.show(); 97 | } 98 | 99 | void clearandreset(){ 100 | 101 | Utils.getSP(SettingActivity.this).edit().putBoolean("clear",true).commit(); 102 | 103 | try{ 104 | 105 | StandOutWindow.close(this,FrmBrowser.class,StandOutWindow.DEFAULT_ID); 106 | 107 | }catch (Exception ex){} 108 | try { 109 | stopService(new Intent(getApplicationContext(), FrmBrowser.class)); 110 | }catch (Exception ex){} 111 | try { 112 | stopService(new Intent(getApplicationContext(), WindowService.class)); 113 | }catch (Exception ex){} 114 | 115 | 116 | Toast.makeText(this, "重置完成,重启生效", Toast.LENGTH_SHORT).show(); 117 | } 118 | 119 | 120 | 121 | public String[] resolutions={ 122 | "640x360 (最低能玩分辨率)", 123 | "854x480 (480P,省电首选)", 124 | "960x540 (IPhone4)", 125 | "1024x600 (华强北平板电脑)", 126 | "1280x720 (720P)", 127 | "1366x768 (一般笔记本电脑)", 128 | "1600x900 (...)", 129 | "1920x1080 (1080P)", 130 | "2560x1440 (2K)", 131 | "4096x2160 (4K)" 132 | }; 133 | 134 | public void setResolution(View view) { 135 | new AlertDialog.Builder(this).setTitle("设置分辨率").setItems(resolutions, new DialogInterface.OnClickListener() { 136 | @Override 137 | public void onClick(DialogInterface dialogInterface, int i) { 138 | String str = resolutions[i]; 139 | String resstr = str.split("\\s")[0]; 140 | String[] resp = resstr.split("x"); 141 | 142 | Utils.getSP(SettingActivity.this).edit().putInt("rw",Integer.parseInt(resp[0])).putInt("rh",Integer.parseInt(resp[1])).commit(); 143 | 144 | Toast.makeText(SettingActivity.this, "已设置"+resstr, Toast.LENGTH_SHORT).show(); 145 | } 146 | }).create().show(); 147 | } 148 | 149 | public void setServer(View view) { 150 | final List gameList = getGameList(this); 151 | 152 | 153 | new AlertDialog.Builder(this).setTitle("可用游戏列表").setAdapter(new GameAdapter(gameList), new DialogInterface.OnClickListener() { 154 | @Override 155 | public void onClick(DialogInterface dialog, int which) { 156 | setGame(gameList.get(which)); 157 | } 158 | }).create().show(); 159 | 160 | } 161 | 162 | class GameAdapter extends ArrayAdapter{ 163 | public GameAdapter(List objects) { 164 | super(SettingActivity.this,android.R.layout.simple_list_item_1,android.R.id.text1,objects); 165 | } 166 | @Override 167 | public View getView(int position, View convertView, ViewGroup parent) { 168 | View v = super.getView(position, convertView, parent); 169 | ((TextView)v.findViewById(android.R.id.text1)).setText(getItem(position).name); 170 | return v; 171 | } 172 | } 173 | 174 | public void setGame(GameEntry entry){ 175 | Utils.getSP(SettingActivity.this).edit().putString("url",entry.url).putString("tmp",entry.uuid).putString("windowtext",entry.name).commit(); 176 | } 177 | 178 | public static List getGameList(Context ctx){ 179 | ArrayList gameList = new ArrayList<>(); 180 | 181 | try { 182 | BufferedReader reader = new BufferedReader(new InputStreamReader(ctx.getResources().getAssets().open("gamelist.json"))); 183 | String line = ""; 184 | StringBuilder out = new StringBuilder(); 185 | while ((line=reader.readLine())!=null){ 186 | out.append(line).append("\n"); 187 | } 188 | reader.close(); 189 | JSONArray jarr = new JSONArray(out.toString()); 190 | for (int i = 0; i < jarr.length(); i++) { 191 | JSONObject jobj = jarr.getJSONObject(i); 192 | gameList.add(new GameEntry(jobj.getString("guid"),jobj.getString("name"),jobj.getString("url"))); 193 | } 194 | } catch (IOException e) { 195 | e.printStackTrace(); 196 | Toast.makeText(ctx, "读取游戏列表失败", Toast.LENGTH_SHORT).show(); 197 | } catch (JSONException e){ 198 | e.printStackTrace(); 199 | Toast.makeText(ctx, "读取游戏列表失败", Toast.LENGTH_SHORT).show(); 200 | } 201 | return gameList; 202 | } 203 | 204 | public static class GameEntry{ 205 | public String uuid,name,url; 206 | public GameEntry(String uuid, String name, String url) { 207 | this.uuid = uuid; 208 | this.name = name; 209 | this.url = url; 210 | } 211 | } 212 | 213 | public void loadPlugin(View view) { 214 | // if(!hasStorage()){ 215 | // Toast.makeText(this, "存储权限没有获取", Toast.LENGTH_SHORT).show(); 216 | // return; 217 | // } 218 | startActivity(new Intent(this,PluginActivity.class)); 219 | finish(); 220 | } 221 | 222 | } 223 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/gamebrowser/WebGameBoostEngine.java: -------------------------------------------------------------------------------- 1 | package com.example.gamebrowser; 2 | 3 | import android.annotation.TargetApi; 4 | import android.app.Application; 5 | import android.content.Context; 6 | import android.graphics.Bitmap; 7 | import android.net.http.SslError; 8 | import android.os.Build; 9 | import android.util.Log; 10 | import android.webkit.JsResult; 11 | import android.webkit.MimeTypeMap; 12 | import android.webkit.SafeBrowsingResponse; 13 | import android.webkit.SslErrorHandler; 14 | import android.webkit.WebChromeClient; 15 | import android.webkit.WebResourceRequest; 16 | import android.webkit.WebResourceResponse; 17 | import android.webkit.WebSettings; 18 | import android.webkit.WebView; 19 | import android.webkit.WebViewClient; 20 | 21 | import java.io.BufferedReader; 22 | import java.io.ByteArrayInputStream; 23 | import java.io.ByteArrayOutputStream; 24 | import java.io.File; 25 | import java.io.FileInputStream; 26 | import java.io.FileNotFoundException; 27 | import java.io.FileOutputStream; 28 | import java.io.IOException; 29 | import java.io.InputStream; 30 | import java.io.InputStreamReader; 31 | import java.io.OutputStream; 32 | import java.io.PipedInputStream; 33 | import java.io.PipedOutputStream; 34 | import java.io.SequenceInputStream; 35 | import java.net.HttpURLConnection; 36 | import java.net.URL; 37 | import java.util.Map; 38 | import java.util.Vector; 39 | import android.os.Handler; 40 | 41 | import wei.mark.standout.Utils; 42 | 43 | /** 44 | * Created by ZYFDroid on 2020-03-31. 45 | */ 46 | 47 | public class WebGameBoostEngine { 48 | 49 | 50 | public static void boost(final Context ctx,final WebView mWebView, final String baseUrl, final OnTitleChangedListener titleListener){ 51 | final String cachepref = Utils.getSP(ctx).getString("tmp","def"); 52 | String urlRoot = baseUrl; 53 | if(baseUrl.lastIndexOf(".",baseUrl.length())>0){ 54 | if(baseUrl.lastIndexOf(".") > baseUrl.lastIndexOf("/")){ 55 | int slashindex =baseUrl.lastIndexOf("/"); 56 | urlRoot = baseUrl.substring(0,slashindex+1); 57 | } 58 | } 59 | final String baseUrlRoot=urlRoot; 60 | if(null==hWnd){hWnd=new Handler();} 61 | mWebView.setKeepScreenOn(true); 62 | mWebView.setWebViewClient(new WebViewClient() { 63 | @Override 64 | public boolean shouldOverrideUrlLoading(WebView view, String url) { 65 | return !url.startsWith(baseUrl); 66 | } 67 | 68 | 69 | 70 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 71 | @Override 72 | public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { 73 | return !request.getUrl().toString().startsWith(baseUrl); 74 | } 75 | 76 | MimeTypeMap mimt = MimeTypeMap.getSingleton(); 77 | 78 | @Override 79 | public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { 80 | String resUrl = request.getUrl().toString(); 81 | if (resUrl.equals(baseUrl)) { 82 | return super.shouldInterceptRequest(view,request); 83 | } 84 | try { 85 | 86 | if (request.getMethod().equals("GET")) { 87 | if (shouldCache(resUrl)) { 88 | 89 | File cache = new File(urlToLocalPath(resUrl, getBaseDir())); 90 | File patch = new File(urlToLocalPath(resUrl,getPatchDir())); 91 | 92 | String type = "*.*"; 93 | if (mimt.hasExtension(MimeTypeMap.getFileExtensionFromUrl(resUrl))) { 94 | type = mimt.getMimeTypeFromExtension(MimeTypeMap.getFileExtensionFromUrl(resUrl)); 95 | } 96 | if (resUrl.endsWith("/")) { 97 | type = "text/html"; 98 | } 99 | if (patch.exists()) { 100 | 101 | try { 102 | if(BuildConfig.DEBUG) 103 | Log.v("USES_PATCH", resUrl + " -> " + urlToLocalPath(resUrl, getPatchDir())); 104 | 105 | 106 | return new WebResourceResponse(type, null, new FileInputStream(patch)); 107 | } catch (FileNotFoundException e) { 108 | e.printStackTrace(); 109 | } 110 | } 111 | else if(cache.exists()){ 112 | try { 113 | if(BuildConfig.DEBUG) 114 | Log.v("USES_CACHE", resUrl + " -> " + urlToLocalPath(resUrl, getBaseDir())); 115 | return new WebResourceResponse(type, null, new FileInputStream(cache)); 116 | } catch (FileNotFoundException e) { 117 | e.printStackTrace(); 118 | } 119 | } 120 | else { 121 | try { 122 | 123 | String source = resUrl; 124 | String dest = cache.getAbsolutePath(); 125 | 126 | AsyncWebDownloader downer = new AsyncWebDownloader(request,source,dest); 127 | downer.start(); 128 | InputStream cacheIs = downer.getParallelInputStream(); 129 | 130 | 131 | if(BuildConfig.DEBUG) 132 | Log.v("MAKE_CACHE", source + " -> " + dest); 133 | 134 | 135 | return new WebResourceResponse(type, null, cacheIs); 136 | } catch (IOException e) { 137 | e.printStackTrace(); 138 | try { 139 | cache.delete(); 140 | } catch (Exception exxx) { 141 | e.printStackTrace(); 142 | } 143 | } 144 | } 145 | } 146 | } 147 | 148 | } catch (Exception ex) { 149 | ex.printStackTrace(); 150 | } 151 | if(BuildConfig.DEBUG) 152 | Log.v("DIRECTLOAD_NOCACHE", resUrl); 153 | return super.shouldInterceptRequest(view, request); 154 | } 155 | 156 | @Override 157 | public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { 158 | handler.proceed(); 159 | } 160 | 161 | @Override 162 | public void onPageStarted(WebView view, String url, Bitmap favicon) { 163 | super.onPageStarted(view, url, favicon); 164 | 165 | } 166 | 167 | @Override 168 | public void onPageFinished(WebView view, String url) { 169 | super.onPageFinished(view, url); 170 | File patchList = new File(getModDir()); 171 | if(!patchList.exists()){ 172 | patchList.mkdirs(); 173 | } 174 | for (File p : 175 | patchList.listFiles()) { 176 | if(p.getName().endsWith(".js")){ 177 | try { 178 | BufferedReader reader = new BufferedReader(new InputStreamReader((new FileInputStream(p)))); 179 | String line = ""; 180 | StringBuilder out = new StringBuilder(); 181 | while ((line=reader.readLine())!=null){ 182 | out.append(line).append("\n"); 183 | } 184 | reader.close(); 185 | 186 | if(BuildConfig.DEBUG) { 187 | Log.v("LOAD_MOD", p.getAbsolutePath()); 188 | } 189 | 190 | view.evaluateJavascript(out.toString(),null); 191 | } catch (IOException e) { 192 | e.printStackTrace(); 193 | } 194 | } 195 | } 196 | } 197 | 198 | @TargetApi(27) 199 | @Override 200 | public void onSafeBrowsingHit(WebView view, WebResourceRequest request, int threatType, SafeBrowsingResponse callback) { 201 | callback.proceed(false); 202 | } 203 | 204 | boolean shouldCache(String url) { 205 | if (!url.startsWith(baseUrl)) { 206 | return false; 207 | } 208 | if (url.contains("?")) { 209 | return false; 210 | } 211 | 212 | if(url.equals(baseUrl)){return false;} 213 | 214 | if(url.endsWith(".html")){return false;} 215 | if(url.endsWith(".htm")){return false;} 216 | if(url.endsWith(".aspx")){return false;} 217 | if(url.endsWith(".asp")){return false;} 218 | if(url.endsWith(".php")){return false;} 219 | if(url.endsWith(".jsp")){return false;} 220 | if(url.endsWith(".action")){return false;} 221 | if(url.endsWith(".do")){return false;} 222 | 223 | String path = url.replace(baseUrl, ""); 224 | 225 | return true; 226 | } 227 | 228 | String urlToLocalPath(String url, String baseDir) { 229 | File baseFile = new File(baseDir); 230 | if(!baseFile.exists()){ 231 | try{ 232 | baseFile.mkdirs(); 233 | }catch (Exception ex){} 234 | } 235 | 236 | return url.replace(baseUrlRoot, baseDir); 237 | } 238 | 239 | String getBaseDir() { 240 | String path = ctx.getFilesDir().getAbsolutePath(); 241 | if (!path.endsWith("/")) { 242 | path += "/"; 243 | } 244 | return path +cachepref+ "/webres/"; 245 | } 246 | 247 | String getPatchDir() { 248 | String path = ctx.getFilesDir().getAbsolutePath(); 249 | if (!path.endsWith("/")) { 250 | path += "/"; 251 | } 252 | return path +cachepref+ "/patch/"; 253 | } 254 | String getModDir() { 255 | String path = ctx.getFilesDir().getAbsolutePath(); 256 | if (!path.endsWith("/")) { 257 | path += "/"; 258 | } 259 | return path +cachepref+ "/mods/"; 260 | } 261 | 262 | 263 | }); 264 | mWebView.setWebChromeClient(new WebChromeClient() { 265 | @Override 266 | public boolean onJsAlert(WebView view, String url, final String message,final JsResult result) { 267 | hWnd.post(new Runnable() { 268 | @Override 269 | public void run() { 270 | Utils.JsAlert(ctx, message, new Runnable() { 271 | @Override 272 | public void run() { 273 | result.confirm(); 274 | } 275 | }); 276 | } 277 | }); 278 | return true; 279 | } 280 | 281 | @Override 282 | public void onReceivedTitle(WebView view,final String title) { 283 | super.onReceivedTitle(view, title); 284 | hWnd.post(new Runnable() { 285 | @Override 286 | public void run() { 287 | if(null!=titleListener){ 288 | titleListener.onTitleChanged(title); 289 | } 290 | } 291 | }); 292 | } 293 | 294 | @Override 295 | public void onReceivedIcon(WebView view,final Bitmap icon) { 296 | super.onReceivedIcon(view, icon); 297 | hWnd.post(new Runnable() { 298 | @Override 299 | public void run() { 300 | if(null!=titleListener){ 301 | titleListener.onIconChanged(icon); 302 | } 303 | } 304 | }); 305 | } 306 | 307 | }); 308 | WebSettings settings = mWebView.getSettings(); 309 | settings.setJavaScriptEnabled(true); 310 | settings.setAppCachePath(ctx.getCacheDir().getAbsolutePath()); 311 | settings.setCacheMode(WebSettings.LOAD_NO_CACHE); 312 | settings.setAppCacheEnabled(true); 313 | settings.setDomStorageEnabled(true); 314 | settings.setSupportZoom(false); 315 | settings.setDatabaseEnabled(true); 316 | settings.setAllowFileAccess(true); 317 | settings.setMediaPlaybackRequiresUserGesture(false); 318 | String ua = settings.getUserAgentString(); 319 | settings.setUserAgentString(ua+" AndroidGameBrowser/1.0 (Windowed or Fullscreen Immerse browser)"); 320 | 321 | } 322 | private static android.os.Handler hWnd; 323 | 324 | public static interface OnTitleChangedListener{ 325 | void onTitleChanged(String title); 326 | void onIconChanged(Bitmap icon); 327 | } 328 | 329 | } 330 | 331 | 332 | 333 | class AsyncWebDownloader extends Thread{ 334 | private java.io.PipedInputStream pin; 335 | private java.io.PipedOutputStream pout; 336 | private WebResourceRequest req; 337 | private String source; 338 | private String dest; 339 | 340 | private int available = -2; 341 | 342 | public AsyncWebDownloader(WebResourceRequest req, String source, String dest) throws IOException { 343 | this.req = req; 344 | this.source = source; 345 | this.dest = dest; 346 | pin = new PipedInputStream(); 347 | pout = new PipedOutputStream(); 348 | pin.connect(pout); 349 | } 350 | 351 | @Override 352 | public void run(){ 353 | try { 354 | HttpURLConnection conn = null; 355 | InputStream is = null; 356 | byte[] buffer = new byte[4096]; 357 | synchronized (this) { 358 | conn = (HttpURLConnection) new URL(source).openConnection(); 359 | conn.setRequestMethod("GET"); 360 | 361 | boolean downloadFirst = false; 362 | 363 | for (Map.Entry header : 364 | req.getRequestHeaders().entrySet()) { 365 | if (!header.getKey().equals("Range")) { 366 | conn.setRequestProperty(header.getKey(), header.getValue()); 367 | } else { 368 | downloadFirst = true; 369 | } 370 | } 371 | 372 | if(!downloadFirst){available=-3;} 373 | conn.connect(); 374 | is = conn.getInputStream(); 375 | if(downloadFirst) 376 | try { 377 | available = conn.getContentLength(); 378 | } catch (Exception ex) { 379 | available = -1; 380 | } 381 | } 382 | ByteArrayOutputStream os = new ByteArrayOutputStream(); 383 | boolean canWrite = true; 384 | int len = 0; 385 | while ((len = is.read(buffer)) != -1) { 386 | os.write(buffer, 0, len); 387 | os.flush(); 388 | try{ 389 | if(canWrite){ 390 | pout.write(buffer,0,len); 391 | pout.flush(); 392 | } 393 | }catch (Exception ex){ 394 | ex.printStackTrace(); 395 | canWrite = false; 396 | } 397 | } 398 | is.close(); 399 | try{ 400 | pout.close(); 401 | }catch (IOException ex){ 402 | ex.printStackTrace(); 403 | } 404 | File cache = new File(dest); 405 | cache.getParentFile().mkdirs(); 406 | cache.createNewFile(); 407 | OutputStream fos = new FileOutputStream(cache); 408 | os.writeTo(fos); 409 | os.close(); 410 | fos.close(); 411 | conn.disconnect(); 412 | 413 | }catch (IOException ex){ 414 | ex.printStackTrace(); 415 | try { 416 | pout.close(); 417 | } catch (IOException e) { 418 | e.printStackTrace(); 419 | } 420 | } 421 | } 422 | 423 | public InputStream getParallelInputStream(){ 424 | return new WrappedInputStream(pin); 425 | } 426 | 427 | 428 | class WrappedInputStream extends InputStream{ 429 | 430 | public InputStream baseStream; 431 | 432 | public WrappedInputStream(InputStream baseStream) { 433 | this.baseStream = baseStream; 434 | } 435 | 436 | @Override 437 | public int read() throws IOException { 438 | return baseStream.read(); 439 | } 440 | 441 | @Override 442 | public int read(byte[] b) throws IOException { 443 | return baseStream.read(b); 444 | } 445 | 446 | @Override 447 | public int read(byte[] b, int off, int len) throws IOException { 448 | return baseStream.read(b, off, len); 449 | } 450 | 451 | @Override 452 | public int available() throws IOException { 453 | while (available<-1 && available!=-3) { 454 | try{ 455 | Thread.sleep(10); 456 | } catch (InterruptedException e) { 457 | throw new IOException(); 458 | } 459 | } 460 | if(available>=0) { 461 | return available; 462 | } 463 | return baseStream.available(); 464 | } 465 | 466 | @Override 467 | public boolean markSupported() { 468 | return baseStream.markSupported(); 469 | } 470 | 471 | @Override 472 | public long skip(long n) throws IOException { 473 | return baseStream.skip(n); 474 | } 475 | 476 | @Override 477 | public void close() throws IOException { 478 | baseStream.close(); 479 | } 480 | 481 | @Override 482 | public synchronized void mark(int readlimit) { 483 | baseStream.mark(readlimit); 484 | } 485 | 486 | @Override 487 | public synchronized void reset() throws IOException { 488 | baseStream.reset(); 489 | } 490 | } 491 | 492 | 493 | 494 | } 495 | 496 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/gamebrowser/WindowService.java: -------------------------------------------------------------------------------- 1 | package com.example.gamebrowser; 2 | import android.app.*; 3 | import android.os.*; 4 | import android.content.*; 5 | import android.widget.*; 6 | import android.view.*; 7 | import android.graphics.*; 8 | import android.util.*; 9 | 10 | import wei.mark.standout.StandOutWindow; 11 | import wei.mark.standout.Utils; 12 | 13 | public class WindowService extends Service 14 | { 15 | private static final String TAG = "MainService"; 16 | 17 | //要引用的布局文件. 18 | LinearLayout toucherLayout; 19 | //布局参数. 20 | WindowManager.LayoutParams params; 21 | //实例化的WindowManager. 22 | WindowManager windowManager; 23 | static WindowService mInstance; 24 | ImageButton imageButton1; 25 | 26 | //状态栏高度.(接下来会用到) 27 | int statusBarHeight = -1; 28 | @Override 29 | public IBinder onBind(Intent p1) 30 | { 31 | // TODO: Implement this method 32 | return null; 33 | } 34 | 35 | @Override 36 | public void onCreate() 37 | { 38 | // TODO: Implement this method 39 | super.onCreate(); 40 | mInstance=this; 41 | createToucher(); 42 | } 43 | 44 | @SuppressWarnings("WrongConstant") 45 | private void createToucher() 46 | { 47 | params = new WindowManager.LayoutParams(); 48 | windowManager = (WindowManager) getApplication().getSystemService(Context.WINDOW_SERVICE); 49 | params.type = Utils.getFlagCompat(); 50 | params.format = PixelFormat.RGBA_8888; 51 | params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 52 | params.gravity = Gravity.LEFT | Gravity.TOP; 53 | params.x = Utils.getSP(this).getInt("bx",0); 54 | params.y = Utils.getSP(this).getInt("by",0); 55 | blx = params.x; 56 | bly = params.y; 57 | params.width = dip2px(this,36); 58 | params.height = dip2px(this,36); 59 | 60 | LayoutInflater inflater = LayoutInflater.from(getApplication()); 61 | toucherLayout = (LinearLayout) inflater.inflate(R.layout.fwd,null); 62 | windowManager.addView(toucherLayout,params); 63 | toucherLayout.measure(View.MeasureSpec.UNSPECIFIED,View.MeasureSpec.UNSPECIFIED); 64 | int resourceId = getResources().getIdentifier("status_bar_height","dimen","android"); 65 | if (resourceId > 0) 66 | { 67 | statusBarHeight = getResources().getDimensionPixelSize(resourceId); 68 | } 69 | Log.i(TAG,"状态栏高度为:" + statusBarHeight); 70 | 71 | //浮动窗口按钮. 72 | imageButton1 = (ImageButton) toucherLayout.findViewById(R.id.imageButton1); 73 | imageButton1.setOnTouchListener(new View.OnTouchListener() { 74 | boolean isDragged = false; 75 | float dx =0,dy=0; 76 | @Override 77 | public boolean onTouch(View v, MotionEvent event) { 78 | 79 | if(event.getAction()==MotionEvent.ACTION_DOWN){ 80 | isDragged = false; 81 | dx = event.getRawX(); 82 | dy = event.getRawY(); 83 | } 84 | 85 | if(event.getAction()==MotionEvent.ACTION_MOVE) { 86 | 87 | if(!isDragged){ 88 | if(Math.abs(dx-event.getRawX()) + Math.abs(dy-event.getRawY()) > px2dip(getApplicationContext(),75)){ 89 | isDragged = true; 90 | }; 91 | } 92 | else { 93 | params.x = (int) event.getRawX() - dip2px(18); 94 | params.y = (int) event.getRawY() - dip2px(18) - statusBarHeight; 95 | windowManager.updateViewLayout(toucherLayout, params); 96 | blx = params.x; 97 | bly = params.y; 98 | } 99 | } 100 | 101 | if(event.getAction() == MotionEvent.ACTION_UP){ 102 | if(!isDragged){ 103 | FrmBrowser.isAnimShow = true; 104 | 105 | StandOutWindow.show(getApplicationContext(), FrmBrowser.class, StandOutWindow.DEFAULT_ID); 106 | 107 | Utils.getSP(WindowService.this).edit().putInt("bx",blx).putInt("by",bly).commit(); 108 | 109 | stopSelf(); 110 | } 111 | } 112 | 113 | return true; 114 | } 115 | }); 116 | 117 | 118 | 119 | //其他代码... 120 | } 121 | 122 | private int blx=0,bly=0; 123 | 124 | @Override 125 | public void onDestroy() 126 | { 127 | if (imageButton1 != null) 128 | { 129 | windowManager.removeView(toucherLayout); 130 | } 131 | mInstance=null; 132 | super.onDestroy(); 133 | } 134 | 135 | 136 | public static int px2dip(Context context, float pxValue) { 137 | final float scale = context.getResources().getDisplayMetrics().density; 138 | return (int) (pxValue / scale + 0.5f); 139 | } 140 | public static int dip2px(Context context, float dpValue) { 141 | float scale = context.getResources().getDisplayMetrics().density; 142 | return (int) (dpValue * scale + 0.5f); 143 | } 144 | 145 | int dip2px(float dp){return dip2px(this,dp);} 146 | } 147 | -------------------------------------------------------------------------------- /app/src/main/java/wei/mark/standout/Utils.java: -------------------------------------------------------------------------------- 1 | package wei.mark.standout; 2 | 3 | import android.app.Activity; 4 | import android.app.AlertDialog; 5 | import android.app.Dialog; 6 | import android.content.Context; 7 | import android.content.DialogInterface; 8 | import android.content.SharedPreferences; 9 | import android.os.Build; 10 | import android.os.Bundle; 11 | import android.text.InputType; 12 | import android.view.Gravity; 13 | import android.view.View; 14 | import android.view.WindowManager; 15 | import android.widget.Button; 16 | import android.widget.EditText; 17 | import android.widget.LinearLayout; 18 | 19 | public class Utils { 20 | public static boolean isSet(int flags, int flag) { 21 | return (flags & flag) == flag; 22 | } 23 | 24 | public static final String yes="是的"; 25 | public static final String no ="不是"; 26 | public static final String uhh="好"; 27 | 28 | public static void Confirm(Context ctx, String msg, final Runnable action){ 29 | AlertDialog ald = new AlertDialog.Builder(ctx).setMessage(msg).setPositiveButton(yes, new DialogInterface.OnClickListener() { 30 | @Override 31 | public void onClick(DialogInterface dialogInterface, int i) { 32 | action.run(); 33 | } 34 | }).setNegativeButton(no,null).create(); 35 | if(!(ctx instanceof Activity)) 36 | ald.getWindow().setType(getFlagCompat()); 37 | 38 | ald.show(); 39 | } 40 | 41 | public static void JsAlert(Context ctx, String msg, final Runnable action){ 42 | AlertDialog ald = new AlertDialog.Builder(ctx).setMessage(msg).setPositiveButton(yes, new DialogInterface.OnClickListener() { 43 | @Override 44 | public void onClick(DialogInterface dialogInterface, int i) { 45 | action.run(); 46 | } 47 | }).setCancelable(false).create(); 48 | if(!(ctx instanceof Activity)) 49 | ald.getWindow().setType(getFlagCompat()); 50 | 51 | ald.show(); 52 | } 53 | 54 | public static void showDialog(Context ctx, String msg){ 55 | AlertDialog ald = new AlertDialog.Builder(ctx).setMessage(msg).setPositiveButton(uhh,null).create(); 56 | if(!(ctx instanceof Activity)) 57 | ald.getWindow().setType(getFlagCompat()); 58 | ald.show(); 59 | } 60 | 61 | public static int getFlagCompat(){ 62 | if(Build.VERSION.SDK_INT>=26){ 63 | return WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; 64 | } 65 | return WindowManager.LayoutParams.TYPE_SYSTEM_ALERT; 66 | } 67 | 68 | public static void Prompt(Context ctx, String msg, final OnPromptResult action){ 69 | AlertDialog ald = new AlertDialog.Builder(ctx).setMessage(msg).setPositiveButton(yes, new DialogInterface.OnClickListener() { 70 | @Override 71 | public void onClick(DialogInterface dialogInterface, int i) { 72 | action.onResult(true); 73 | } 74 | }).setNegativeButton(no, new DialogInterface.OnClickListener() { 75 | @Override 76 | public void onClick(DialogInterface dialog, int which) { 77 | action.onResult(false); 78 | } 79 | }).setCancelable(false).create(); 80 | if(!(ctx instanceof Activity)) 81 | ald.getWindow().setType(getFlagCompat()); 82 | ald.show(); 83 | } 84 | 85 | public static interface OnPromptResult{ 86 | public void onResult(boolean isYesPressed); 87 | } 88 | 89 | 90 | public static abstract class EditDialog 91 | { 92 | String title; 93 | Context ctx; 94 | String def; 95 | public EditDialog(Context mctx,String mtitle,String defaultValue){ 96 | title=mtitle; 97 | ctx=mctx; 98 | def=defaultValue; 99 | } 100 | 101 | public void show(){ 102 | onCreate(); 103 | if(!(ctx instanceof Activity)) 104 | adbd.getWindow().setType(getFlagCompat()); 105 | adbd.show(); 106 | } 107 | 108 | EditText edt; 109 | AlertDialog adbd; 110 | protected void onCreate() 111 | { 112 | AlertDialog.Builder adb = new AlertDialog.Builder(ctx); 113 | adb.setTitle(title); 114 | LinearLayout base=new LinearLayout(ctx); 115 | base.setOrientation(base.VERTICAL); 116 | base.setGravity(Gravity.CENTER); 117 | edt=new EditText(ctx); 118 | base.addView(edt); 119 | LinearLayout.LayoutParams lyp=(LinearLayout.LayoutParams)edt.getLayoutParams(); 120 | lyp.width=lyp.MATCH_PARENT; 121 | edt.setLayoutParams(lyp); 122 | edt.setInputType(InputType.TYPE_TEXT_FLAG_MULTI_LINE); 123 | edt.setText(def); 124 | LinearLayout btnbase=new LinearLayout(ctx); 125 | btnbase.setOrientation(btnbase.HORIZONTAL); 126 | btnbase.setGravity(Gravity.CENTER); 127 | base.addView(btnbase); 128 | lyp=(LinearLayout.LayoutParams)btnbase.getLayoutParams(); 129 | lyp.width=lyp.MATCH_PARENT; 130 | btnbase.setLayoutParams(lyp); 131 | Button btnCancel=new Button(ctx); 132 | btnCancel.setText("取消"); 133 | btnCancel.setOnClickListener(new Button.OnClickListener(){ 134 | @Override 135 | public void onClick(View p1) 136 | { 137 | adbd.dismiss(); 138 | } 139 | }); 140 | Button btnOk=new Button(ctx); 141 | btnOk.setText("确定"); 142 | btnOk.setOnClickListener(new Button.OnClickListener(){ 143 | @Override 144 | public void onClick(View p1) 145 | { 146 | 147 | onConfirmText(edt.getText().toString()); 148 | adbd.dismiss(); 149 | } 150 | }); 151 | btnbase.addView(btnCancel); 152 | btnbase.addView(btnOk); 153 | adb.setView(base); 154 | adbd = adb.create(); 155 | } 156 | 157 | 158 | public abstract void onConfirmText(String text); 159 | } 160 | 161 | public static SharedPreferences getSP(Context ctx){ 162 | return ctx.getSharedPreferences("0",0); 163 | } 164 | 165 | public static void showDialogVersion(Context ctx,String key,int version,String message){ 166 | if(getSP(ctx).getInt(key,-1) < version){ 167 | Utils.showDialog(ctx,message); 168 | } 169 | } 170 | 171 | public static void setDialogVersion(Context ctx,String key,int version){ 172 | getSP(ctx).edit().putInt(key,version).commit(); 173 | } 174 | 175 | } 176 | 177 | 178 | -------------------------------------------------------------------------------- /app/src/main/java/wei/mark/standout/WindowCache.java: -------------------------------------------------------------------------------- 1 | package wei.mark.standout; 2 | 3 | import java.util.HashMap; 4 | import java.util.HashSet; 5 | import java.util.Map; 6 | import java.util.Set; 7 | 8 | import wei.mark.standout.ui.Window; 9 | 10 | import android.util.SparseArray; 11 | 12 | public class WindowCache { 13 | public Map, SparseArray> sWindows; 14 | 15 | public WindowCache() { 16 | sWindows = new HashMap, SparseArray>(); 17 | } 18 | 19 | /** 20 | * Returns whether the window corresponding to the class and id exists in 21 | * the {@link #sWindows} cache. 22 | * 23 | * @param id 24 | * The id representing the window. 25 | * @param cls 26 | * Class corresponding to the window. 27 | * @return True if the window corresponding to the class and id exists in 28 | * the cache, or false if it does not exist. 29 | */ 30 | public boolean isCached(int id, Class cls) { 31 | return getCache(id, cls) != null; 32 | } 33 | 34 | /** 35 | * Returns the window corresponding to the id from the {@link #sWindows} 36 | * cache. 37 | * 38 | * @param id 39 | * The id representing the window. 40 | * @param cls 41 | * The class of the implementation of the window. 42 | * @return The window corresponding to the id if it exists in the cache, or 43 | * null if it does not. 44 | */ 45 | public Window getCache(int id, Class cls) { 46 | SparseArray l2 = sWindows.get(cls); 47 | if (l2 == null) { 48 | return null; 49 | } 50 | 51 | return l2.get(id); 52 | } 53 | 54 | /** 55 | * Add the window corresponding to the id in the {@link #sWindows} cache. 56 | * 57 | * @param id 58 | * The id representing the window. 59 | * @param cls 60 | * The class of the implementation of the window. 61 | * @param window 62 | * The window to be put in the cache. 63 | */ 64 | public void putCache(int id, Class cls, Window window) { 65 | SparseArray l2 = sWindows.get(cls); 66 | if (l2 == null) { 67 | l2 = new SparseArray(); 68 | sWindows.put(cls, l2); 69 | } 70 | 71 | l2.put(id, window); 72 | } 73 | 74 | /** 75 | * Remove the window corresponding to the id from the {@link #sWindows} 76 | * cache. 77 | * 78 | * @param id 79 | * The id representing the window. 80 | * @param cls 81 | * The class of the implementation of the window. 82 | */ 83 | public void removeCache(int id, Class cls) { 84 | SparseArray l2 = sWindows.get(cls); 85 | if (l2 != null) { 86 | l2.remove(id); 87 | if (l2.size() == 0) { 88 | sWindows.remove(cls); 89 | } 90 | } 91 | } 92 | 93 | /** 94 | * Returns the size of the {@link #sWindows} cache. 95 | * 96 | * @return True if the cache corresponding to this class is empty, false if 97 | * it is not empty. 98 | * @param cls 99 | * The class of the implementation of the window. 100 | */ 101 | public int getCacheSize(Class cls) { 102 | SparseArray l2 = sWindows.get(cls); 103 | if (l2 == null) { 104 | return 0; 105 | } 106 | 107 | return l2.size(); 108 | } 109 | 110 | /** 111 | * Returns the ids in the {@link #sWindows} cache. 112 | * 113 | * @param cls 114 | * The class of the implementation of the window. 115 | * @return The ids representing the cached windows. 116 | */ 117 | public Set getCacheIds(Class cls) { 118 | SparseArray l2 = sWindows.get(cls); 119 | if (l2 == null) { 120 | return new HashSet(); 121 | } 122 | 123 | Set keys = new HashSet(); 124 | for (int i = 0; i < l2.size(); i++) { 125 | keys.add(l2.keyAt(i)); 126 | } 127 | return keys; 128 | } 129 | 130 | public int size() { 131 | return sWindows.size(); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /app/src/main/java/wei/mark/standout/constants/StandOutFlags.java: -------------------------------------------------------------------------------- 1 | package wei.mark.standout.constants; 2 | 3 | import wei.mark.standout.StandOutWindow; 4 | import wei.mark.standout.ui.Window; 5 | import android.view.Gravity; 6 | import android.view.MotionEvent; 7 | import android.view.View; 8 | 9 | /** 10 | * Flags to be returned from {@link StandOutWindow#getFlags(int)}. 11 | * 12 | * @author Mark Wei 13 | * 14 | */ 15 | public class StandOutFlags { 16 | // This counter keeps track of which primary bit to set for each flag 17 | private static int flag_bit = 0; 18 | 19 | /** 20 | * Setting this flag indicates that the window wants the system provided 21 | * window decorations (titlebar, hide/close buttons, resize handle, etc). 22 | */ 23 | public static final int FLAG_DECORATION_SYSTEM = 1 << flag_bit++; 24 | 25 | /** 26 | * Setting this flag indicates that the window decorator should NOT provide 27 | * a close button. 28 | * 29 | *

30 | * This flag also sets {@link #FLAG_DECORATION_SYSTEM}. 31 | */ 32 | public static final int FLAG_DECORATION_CLOSE_DISABLE = FLAG_DECORATION_SYSTEM 33 | | 1 << flag_bit++; 34 | 35 | /** 36 | * Setting this flag indicates that the window decorator should NOT provide 37 | * a resize handle. 38 | * 39 | *

40 | * This flag also sets {@link #FLAG_DECORATION_SYSTEM}. 41 | */ 42 | public static final int FLAG_DECORATION_RESIZE_DISABLE = FLAG_DECORATION_SYSTEM 43 | | 1 << flag_bit++; 44 | 45 | /** 46 | * Setting this flag indicates that the window decorator should NOT provide 47 | * a resize handle. 48 | * 49 | *

50 | * This flag also sets {@link #FLAG_DECORATION_SYSTEM}. 51 | */ 52 | public static final int FLAG_DECORATION_MAXIMIZE_DISABLE = FLAG_DECORATION_SYSTEM 53 | | 1 << flag_bit++; 54 | 55 | /** 56 | * Setting this flag indicates that the window decorator should NOT provide 57 | * a resize handle. 58 | * 59 | *

60 | * This flag also sets {@link #FLAG_DECORATION_SYSTEM}. 61 | */ 62 | public static final int FLAG_DECORATION_MOVE_DISABLE = FLAG_DECORATION_SYSTEM 63 | | 1 << flag_bit++; 64 | 65 | /** 66 | * Setting this flag indicates that the window can be moved by dragging the 67 | * body. 68 | * 69 | *

70 | * Note that if {@link #FLAG_DECORATION_SYSTEM} is set, the window can 71 | * always be moved by dragging the titlebar regardless of this flag. 72 | */ 73 | public static final int FLAG_BODY_MOVE_ENABLE = 1 << flag_bit++; 74 | 75 | /** 76 | * Setting this flag indicates that windows are able to be hidden, that 77 | * {@link StandOutWindow#getHiddenIcon(int)}, 78 | * {@link StandOutWindow#getHiddenTitle(int)}, and 79 | * {@link StandOutWindow#getHiddenMessage(int)} are implemented, and that 80 | * the system window decorator should provide a hide button if 81 | * {@link #FLAG_DECORATION_SYSTEM} is set. 82 | */ 83 | public static final int FLAG_WINDOW_HIDE_ENABLE = 1 << flag_bit++; 84 | 85 | /** 86 | * Setting this flag indicates that the window should be brought to the 87 | * front upon user interaction. 88 | * 89 | *

90 | * Note that if you set this flag, there is a noticeable flashing of the 91 | * window during {@link MotionEvent#ACTION_UP}. This the hack that allows 92 | * the system to bring the window to the front. 93 | */ 94 | public static final int FLAG_WINDOW_BRING_TO_FRONT_ON_TOUCH = 1 << flag_bit++; 95 | 96 | /** 97 | * Setting this flag indicates that the window should be brought to the 98 | * front upon user tap. 99 | * 100 | *

101 | * Note that if you set this flag, there is a noticeable flashing of the 102 | * window during {@link MotionEvent#ACTION_UP}. This the hack that allows 103 | * the system to bring the window to the front. 104 | */ 105 | public static final int FLAG_WINDOW_BRING_TO_FRONT_ON_TAP = 1 << flag_bit++; 106 | 107 | /** 108 | * Setting this flag indicates that the system should keep the window's 109 | * position within the edges of the screen. If this flag is not set, the 110 | * window will be able to be dragged off of the screen. 111 | * 112 | *

113 | * If this flag is set, the window's {@link Gravity} is recommended to be 114 | * {@link Gravity#TOP} | {@link Gravity#LEFT}. If the gravity is anything 115 | * other than TOP|LEFT, then even though the window will be displayed within 116 | * the edges, it will behave as if the user can drag it off the screen. 117 | * 118 | */ 119 | public static final int FLAG_WINDOW_EDGE_LIMITS_ENABLE = 1 << flag_bit++; 120 | 121 | /** 122 | * Setting this flag indicates that the system should keep the window's 123 | * aspect ratio constant when resizing. 124 | * 125 | *

126 | * The aspect ratio will only be enforced in 127 | * {@link StandOutWindow#onTouchHandleResize(int, Window, View, MotionEvent)} 128 | * . The aspect ratio will not be enforced if you set the width or height of 129 | * the window's LayoutParams manually. 130 | * 131 | * @see StandOutWindow#onTouchHandleResize(int, Window, View, MotionEvent) 132 | */ 133 | public static final int FLAG_WINDOW_ASPECT_RATIO_ENABLE = 1 << flag_bit++; 134 | 135 | /** 136 | * Setting this flag indicates that the system should resize the window when 137 | * it detects a pinch-to-zoom gesture. 138 | * 139 | * @see Window#onInterceptTouchEvent(MotionEvent) 140 | */ 141 | public static final int FLAG_WINDOW_PINCH_RESIZE_ENABLE = 1 << flag_bit++; 142 | 143 | /** 144 | * Setting this flag indicates that the window does not need focus. If this 145 | * flag is set, the system will not take care of setting and unsetting the 146 | * focus of windows based on user touch and key events. 147 | * 148 | *

149 | * You will most likely need focus if your window contains any of the 150 | * following: Button, ListView, EditText. 151 | * 152 | *

153 | * The benefit of disabling focus is that your window will not consume any 154 | * key events. Normally, focused windows will consume the Back and Menu 155 | * keys. 156 | * 157 | * @see {@link StandOutWindow#focus(int)} 158 | * @see {@link StandOutWindow#unfocus(int)} 159 | * 160 | */ 161 | public static final int FLAG_WINDOW_FOCUSABLE_DISABLE = 1 << flag_bit++; 162 | 163 | /** 164 | * Setting this flag indicates that the system should not change the 165 | * window's visual state when focus is changed. If this flag is set, the 166 | * implementation can choose to change the visual state in 167 | * {@link StandOutWindow#onFocusChange(int, Window, boolean)}. 168 | * 169 | * @see {@link Window#onFocus(boolean)} 170 | * 171 | */ 172 | //public static final int FLAG_WINDOW_FOCUS_INDICATOR_DISABLE = 1 << flag_bit++; 173 | 174 | /** 175 | * Setting this flag indicates that the system should disable all 176 | * compatibility workarounds. The default behavior is to run 177 | * {@link Window#fixCompatibility(View, int)} on the view returned by the 178 | * implementation. 179 | * 180 | * @see {@link Window#fixCompatibility(View, int)} 181 | */ 182 | public static final int FLAG_FIX_COMPATIBILITY_ALL_DISABLE = 1 << flag_bit++; 183 | 184 | /** 185 | * Setting this flag indicates that the system should disable all additional 186 | * functionality. The default behavior is to run 187 | * {@link Window#addFunctionality(View, int)} on the view returned by the 188 | * implementation. 189 | * 190 | * @see {@link StandOutWindow#addFunctionality(View, int)} 191 | */ 192 | public static final int FLAG_ADD_FUNCTIONALITY_ALL_DISABLE = 1 << flag_bit++; 193 | 194 | /** 195 | * Setting this flag indicates that the system should disable adding the 196 | * resize handle additional functionality to a custom View R.id.corner. 197 | * 198 | *

199 | * If {@link #FLAG_DECORATION_SYSTEM} is set, the user will always be able 200 | * to resize the window with the default corner. 201 | * 202 | * @see {@link Window#addFunctionality(View, int)} 203 | */ 204 | public static final int FLAG_ADD_FUNCTIONALITY_RESIZE_DISABLE = 1 << flag_bit++; 205 | 206 | /** 207 | * Setting this flag indicates that the system should disable adding the 208 | * drop down menu additional functionality to a custom View 209 | * R.id.window_icon. 210 | * 211 | *

212 | * If {@link #FLAG_DECORATION_SYSTEM} is set, the user will always be able 213 | * to show the drop down menu with the default window icon. 214 | * 215 | * @see {@link Window#addFunctionality(View, int)} 216 | */ 217 | public static final int FLAG_ADD_FUNCTIONALITY_DROP_DOWN_DISABLE = 1 << flag_bit++; 218 | 219 | 220 | } -------------------------------------------------------------------------------- /app/src/main/java/wei/mark/standout/ui/TouchInfo.java: -------------------------------------------------------------------------------- 1 | package wei.mark.standout.ui; 2 | 3 | import java.util.Locale; 4 | 5 | /** 6 | * This class holds temporal touch and gesture information. Mainly used to hold 7 | * temporary data for onTouchEvent(MotionEvent). 8 | * 9 | * @author Mark Wei 10 | * 11 | */ 12 | public class TouchInfo { 13 | /** 14 | * The state of the window. 15 | */ 16 | public int firstX, firstY, lastX, lastY; 17 | public double dist, scale, firstWidth, firstHeight; 18 | public float ratio; 19 | 20 | /** 21 | * Whether we're past the move threshold already. 22 | */ 23 | public boolean moving; 24 | 25 | @Override 26 | public String toString() { 27 | return String 28 | .format(Locale.US, 29 | "WindowTouchInfo { firstX=%d, firstY=%d,lastX=%d, lastY=%d, firstWidth=%d, firstHeight=%d }", 30 | firstX, firstY, lastX, lastY, firstWidth, firstHeight); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/wei/mark/standout/ui/Window.java: -------------------------------------------------------------------------------- 1 | package wei.mark.standout.ui; 2 | 3 | import java.util.LinkedList; 4 | import java.util.Queue; 5 | 6 | import com.example.gamebrowser.R; 7 | import wei.mark.standout.StandOutWindow; 8 | import wei.mark.standout.StandOutWindow.StandOutLayoutParams; 9 | import wei.mark.standout.Utils; 10 | import wei.mark.standout.constants.StandOutFlags; 11 | import android.content.Context; 12 | import android.os.Bundle; 13 | import android.util.DisplayMetrics; 14 | import android.util.Log; 15 | import android.view.Gravity; 16 | import android.view.KeyEvent; 17 | import android.view.LayoutInflater; 18 | import android.view.MotionEvent; 19 | import android.view.View; 20 | import android.view.ViewGroup; 21 | import android.widget.FrameLayout; 22 | import android.widget.ImageView; 23 | import android.widget.PopupWindow; 24 | import android.widget.TextView; 25 | 26 | /** 27 | * Special view that represents a floating window. 28 | * 29 | * @author Mark Wei 30 | * 31 | */ 32 | public class Window extends FrameLayout { 33 | public static final int VISIBILITY_GONE = 0; 34 | public static final int VISIBILITY_VISIBLE = 1; 35 | public static final int VISIBILITY_TRANSITION = 2; 36 | 37 | static final String TAG = "Window"; 38 | 39 | /** 40 | * Class of the window, indicating which application the window belongs to. 41 | */ 42 | public Class cls; 43 | /** 44 | * Id of the window. 45 | */ 46 | public int id; 47 | 48 | /** 49 | * Whether the window is shown, hidden/closed, or in transition. 50 | */ 51 | public int visibility; 52 | 53 | /** 54 | * Whether the window is focused. 55 | */ 56 | public boolean focused; 57 | 58 | /** 59 | * Original params from {@link StandOutWindow#getParams(int, Window)}. 60 | */ 61 | public StandOutLayoutParams originalParams; 62 | /** 63 | * Original flags from {@link StandOutWindow#getFlags(int)}. 64 | */ 65 | public int flags; 66 | 67 | /** 68 | * Touch information of the window. 69 | */ 70 | public TouchInfo touchInfo; 71 | 72 | /** 73 | * Data attached to the window. 74 | */ 75 | public Bundle data; 76 | 77 | /** 78 | * Width and height of the screen. 79 | */ 80 | int displayWidth, displayHeight; 81 | 82 | 83 | View content; 84 | 85 | FrameLayout body; 86 | /** 87 | * Context of the window. 88 | */ 89 | private final StandOutWindow mContext; 90 | private LayoutInflater mLayoutInflater; 91 | 92 | public Window(Context context) { 93 | super(context); 94 | mContext = null; 95 | } 96 | 97 | public Window(final StandOutWindow context, final int id) { 98 | super(context); 99 | context.setTheme(context.getThemeStyle()); 100 | 101 | mContext = context; 102 | mLayoutInflater = LayoutInflater.from(context); 103 | 104 | this.cls = context.getClass(); 105 | this.id = id; 106 | this.originalParams = context.getParams(id, this); 107 | this.flags = context.getFlags(id); 108 | this.touchInfo = new TouchInfo(); 109 | touchInfo.ratio = (float) originalParams.width / originalParams.height; 110 | this.data = new Bundle(); 111 | DisplayMetrics metrics = mContext.getResources() 112 | .getDisplayMetrics(); 113 | displayWidth = metrics.widthPixels; 114 | displayHeight = (int) (metrics.heightPixels - 25 * metrics.density); 115 | 116 | // create the window contents 117 | 118 | 119 | if (Utils.isSet(flags, StandOutFlags.FLAG_DECORATION_SYSTEM)) { 120 | // requested system window decorations 121 | content = getSystemDecorations(); 122 | body = (FrameLayout) content.findViewById(R.id.body); 123 | } else { 124 | // did not request decorations. will provide own implementation 125 | content = new FrameLayout(context); 126 | content.setId(R.id.content); 127 | body = (FrameLayout) content; 128 | } 129 | 130 | addView(content); 131 | 132 | body.setOnTouchListener(new OnTouchListener() { 133 | 134 | @Override 135 | public boolean onTouch(View v, MotionEvent event) { 136 | // pass all touch events to the implementation 137 | boolean consumed = false; 138 | 139 | // handle move and bring to front 140 | consumed = context.onTouchHandleMove(id, Window.this, v, event) 141 | || consumed; 142 | 143 | // alert implementation 144 | consumed = context.onTouchBody(id, Window.this, v, event) 145 | || consumed; 146 | 147 | return consumed; 148 | } 149 | }); 150 | 151 | // attach the view corresponding to the id from the 152 | // implementation 153 | context.createAndAttachView(id, body); 154 | 155 | // make sure the implementation attached the view 156 | if (body.getChildCount() == 0) { 157 | throw new RuntimeException( 158 | "You must attach your view to the given frame in createAndAttachView()"); 159 | } 160 | 161 | // implement StandOut specific workarounds 162 | if (!Utils.isSet(flags, 163 | StandOutFlags.FLAG_FIX_COMPATIBILITY_ALL_DISABLE)) { 164 | fixCompatibility(body); 165 | } 166 | // implement StandOut specific additional functionality 167 | if (!Utils.isSet(flags, 168 | StandOutFlags.FLAG_ADD_FUNCTIONALITY_ALL_DISABLE)) { 169 | addFunctionality(body); 170 | } 171 | 172 | // attach the existing tag from the frame to the window 173 | setTag(body.getTag()); 174 | } 175 | 176 | @Override 177 | public boolean onInterceptTouchEvent(MotionEvent event) { 178 | StandOutLayoutParams params = getLayoutParams(); 179 | 180 | // focus window 181 | if (event.getAction() == MotionEvent.ACTION_DOWN) { 182 | if (mContext.getFocusedWindow() != this) { 183 | mContext.focus(id); 184 | } 185 | } 186 | 187 | // multitouch 188 | if (event.getPointerCount() >= 2 189 | && Utils.isSet(flags, 190 | StandOutFlags.FLAG_WINDOW_PINCH_RESIZE_ENABLE) 191 | && (event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_DOWN) { 192 | touchInfo.scale = 1; 193 | touchInfo.dist = -1; 194 | touchInfo.firstWidth = params.width; 195 | touchInfo.firstHeight = params.height; 196 | return true; 197 | } 198 | 199 | return false; 200 | } 201 | 202 | @Override 203 | public boolean onTouchEvent(MotionEvent event) { 204 | // handle touching outside 205 | switch (event.getAction()) { 206 | case MotionEvent.ACTION_OUTSIDE: 207 | // unfocus window 208 | if (mContext.getFocusedWindow() == this) { 209 | mContext.unfocus(this); 210 | } 211 | 212 | // notify implementation that ACTION_OUTSIDE occurred 213 | mContext.onTouchBody(id, this, this, event); 214 | break; 215 | } 216 | 217 | // handle multitouch 218 | if (event.getPointerCount() >= 2 219 | && Utils.isSet(flags, 220 | StandOutFlags.FLAG_WINDOW_PINCH_RESIZE_ENABLE)) { 221 | // 2 fingers or more 222 | 223 | float x0 = event.getX(0); 224 | float y0 = event.getY(0); 225 | float x1 = event.getX(1); 226 | float y1 = event.getY(1); 227 | 228 | double dist = Math 229 | .sqrt(Math.pow(x0 - x1, 2) + Math.pow(y0 - y1, 2)); 230 | 231 | switch (event.getAction() & MotionEvent.ACTION_MASK) { 232 | case MotionEvent.ACTION_MOVE: 233 | if (touchInfo.dist == -1) { 234 | touchInfo.dist = dist; 235 | } 236 | touchInfo.scale *= dist / touchInfo.dist; 237 | touchInfo.dist = dist; 238 | 239 | // scale the window with anchor point set to middle 240 | edit().setAnchorPoint(.5f, .5f) 241 | .setSize( 242 | (int) (touchInfo.firstWidth * touchInfo.scale), 243 | (int) (touchInfo.firstHeight * touchInfo.scale)) 244 | .commit(); 245 | break; 246 | } 247 | mContext.onResize(id, this, this,event); 248 | } 249 | 250 | return true; 251 | } 252 | 253 | @Override 254 | public boolean dispatchKeyEvent(KeyEvent event) { 255 | if (mContext.onKeyEvent(id, this, event)) { 256 | Log.d(TAG, "Window " + id + " key event " + event 257 | + " cancelled by implementation."); 258 | return false; 259 | } 260 | 261 | if (event.getAction() == KeyEvent.ACTION_UP) { 262 | switch (event.getKeyCode()) { 263 | case KeyEvent.KEYCODE_BACK: 264 | mContext.unfocus(this); 265 | return true; 266 | } 267 | } 268 | 269 | return super.dispatchKeyEvent(event); 270 | } 271 | 272 | /** 273 | * Request or remove the focus from this window. 274 | * 275 | * @param focus 276 | * Whether we want to gain or lose focus. 277 | * @return True if focus changed successfully, false if it failed. 278 | */ 279 | public boolean onFocus(boolean focus) { 280 | if (!Utils.isSet(flags, StandOutFlags.FLAG_WINDOW_FOCUSABLE_DISABLE)) { 281 | // window is focusable 282 | 283 | if (focus == focused) { 284 | // window already focused/unfocused 285 | return false; 286 | } 287 | 288 | focused = focus; 289 | 290 | // alert callbacks and cancel if instructed 291 | if (mContext.onFocusChange(id, this, focus)) { 292 | Log.d(TAG, "Window " + id + " focus change " 293 | + (focus ? "(true)" : "(false)") 294 | + " cancelled by implementation."); 295 | focused = !focus; 296 | return false; 297 | } 298 | 299 | 300 | 301 | // set window manager params 302 | StandOutLayoutParams params = getLayoutParams(); 303 | params.setFocusFlag(focus); 304 | mContext.updateViewLayout(id, params); 305 | 306 | if (focus) { 307 | mContext.setFocusedWindow(this); 308 | } else { 309 | if (mContext.getFocusedWindow() == this) { 310 | mContext.setFocusedWindow(null); 311 | } 312 | } 313 | 314 | return true; 315 | } 316 | return false; 317 | } 318 | 319 | @Override 320 | public void setLayoutParams(ViewGroup.LayoutParams params) { 321 | if (params instanceof StandOutLayoutParams) { 322 | super.setLayoutParams(params); 323 | } else { 324 | throw new IllegalArgumentException( 325 | "Window" 326 | + id 327 | + ": LayoutParams must be an instance of StandOutLayoutParams."); 328 | } 329 | } 330 | 331 | /** 332 | * Convenience method to start editting the size and position of this 333 | * window. Make sure you call {@link Editor#commit()} when you are done to 334 | * update the window. 335 | * 336 | * @return The Editor associated with this window. 337 | */ 338 | public Editor edit() { 339 | return new Editor(); 340 | } 341 | 342 | @Override 343 | public StandOutLayoutParams getLayoutParams() { 344 | StandOutLayoutParams params = (StandOutLayoutParams) super 345 | .getLayoutParams(); 346 | if (params == null) { 347 | params = originalParams; 348 | } 349 | return params; 350 | } 351 | 352 | public View btnCam; 353 | 354 | private int getDecorationStyle(){ 355 | 356 | return R.layout.system_window_decorators; 357 | } 358 | 359 | /** 360 | * Returns the system window decorations if the implementation sets 361 | * . 362 | * 363 | *

364 | * The system window decorations support hiding, closing, moving, and 365 | * resizing. 366 | * 367 | * @return The frame view containing the system window decorations. 368 | */ 369 | private View getSystemDecorations() { 370 | final View decorations = mLayoutInflater.inflate( 371 | getDecorationStyle(), null); 372 | 373 | // icon 374 | final ImageView icon = (ImageView) decorations 375 | .findViewById(R.id.window_icon); 376 | icon.setImageResource(mContext.getAppIcon()); 377 | icon.setOnClickListener(new OnClickListener() { 378 | 379 | @Override 380 | public void onClick(View v) { 381 | PopupWindow dropDown = mContext.getDropDown(id); 382 | if (dropDown != null) { 383 | dropDown.showAsDropDown(icon); 384 | } 385 | } 386 | }); 387 | 388 | // title 389 | TextView title = (TextView) decorations.findViewById(R.id.title); 390 | title.setText(mContext.getTitle(id)); 391 | 392 | // hide 393 | View hide = decorations.findViewById(R.id.hide); 394 | hide.setOnClickListener(new OnClickListener() { 395 | 396 | @Override 397 | public void onClick(View v) { 398 | mContext.hide(id); 399 | } 400 | }); 401 | hide.setVisibility(View.GONE); 402 | 403 | // maximize 404 | View maximize = decorations.findViewById(R.id.maximize); 405 | maximize.setOnClickListener(new OnClickListener() { 406 | 407 | @Override 408 | public void onClick(View v) { 409 | StandOutLayoutParams params = getLayoutParams(); 410 | boolean isMaximized = data 411 | .getBoolean(WindowDataKeys.IS_MAXIMIZED); 412 | if (isMaximized && params.width == displayWidth 413 | && params.height == displayHeight && params.x == 0 414 | && params.y == 0) { 415 | data.putBoolean(WindowDataKeys.IS_MAXIMIZED, false); 416 | int oldWidth = data.getInt( 417 | WindowDataKeys.WIDTH_BEFORE_MAXIMIZE, -1); 418 | int oldHeight = data.getInt( 419 | WindowDataKeys.HEIGHT_BEFORE_MAXIMIZE, -1); 420 | int oldX = data 421 | .getInt(WindowDataKeys.X_BEFORE_MAXIMIZE, -1); 422 | int oldY = data 423 | .getInt(WindowDataKeys.Y_BEFORE_MAXIMIZE, -1); 424 | edit().setSize(oldWidth, oldHeight).setPosition(oldX, oldY) 425 | .commit(); 426 | } else { 427 | data.putBoolean(WindowDataKeys.IS_MAXIMIZED, true); 428 | data.putInt(WindowDataKeys.WIDTH_BEFORE_MAXIMIZE, 429 | params.width); 430 | data.putInt(WindowDataKeys.HEIGHT_BEFORE_MAXIMIZE, 431 | params.height); 432 | data.putInt(WindowDataKeys.X_BEFORE_MAXIMIZE, params.x); 433 | data.putInt(WindowDataKeys.Y_BEFORE_MAXIMIZE, params.y); 434 | edit().setSize(1f, 1f).setPosition(0, 0).commit(); 435 | } 436 | 437 | mContext.onWindowStateChanged(mContext.getUniqueId(),Window.this,body); 438 | } 439 | }); 440 | 441 | // close 442 | 443 | 444 | 445 | View close = decorations.findViewById(R.id.close); 446 | close.setOnClickListener(new OnClickListener() { 447 | 448 | @Override 449 | public void onClick(View v) { 450 | 451 | Utils.Confirm(mContext, "是否退出?", new Runnable() { 452 | @Override 453 | public void run() { 454 | mContext.close(id); 455 | } 456 | }); 457 | } 458 | }); 459 | 460 | 461 | 462 | 463 | View custom = decorations.findViewById(R.id.custom); 464 | custom.setOnClickListener(new OnClickListener() { 465 | 466 | @Override 467 | public void onClick(View v) { 468 | mContext.onCustomButton1Click(id); 469 | } 470 | }); 471 | 472 | 473 | // move 474 | View titlebar = decorations.findViewById(R.id.titlebar); 475 | titlebar.setOnTouchListener(new OnTouchListener() { 476 | 477 | @Override 478 | public boolean onTouch(View v, MotionEvent event) { 479 | // handle dragging to move 480 | boolean consumed = mContext.onTouchHandleMove(id, Window.this, 481 | v, event); 482 | return consumed; 483 | } 484 | }); 485 | 486 | // resize 487 | View corner = decorations.findViewById(R.id.corner); 488 | corner.setOnTouchListener(new OnTouchListener() { 489 | 490 | @Override 491 | public boolean onTouch(View v, MotionEvent event) { 492 | // handle dragging to move 493 | boolean consumed = mContext.onTouchHandleResize(id, 494 | Window.this, v, event); 495 | 496 | return consumed; 497 | } 498 | }); 499 | 500 | // set window appearance and behavior based on flags 501 | if (Utils.isSet(flags, StandOutFlags.FLAG_WINDOW_HIDE_ENABLE)) { 502 | hide.setVisibility(View.VISIBLE); 503 | } 504 | if (Utils.isSet(flags, StandOutFlags.FLAG_DECORATION_MAXIMIZE_DISABLE)) { 505 | maximize.setVisibility(View.GONE); 506 | } 507 | if (Utils.isSet(flags, StandOutFlags.FLAG_DECORATION_CLOSE_DISABLE)) { 508 | close.setVisibility(View.GONE); 509 | } 510 | if (Utils.isSet(flags, StandOutFlags.FLAG_DECORATION_MOVE_DISABLE)) { 511 | titlebar.setOnTouchListener(null); 512 | } 513 | if (Utils.isSet(flags, StandOutFlags.FLAG_DECORATION_RESIZE_DISABLE)) { 514 | corner.setVisibility(View.GONE); 515 | } 516 | 517 | return decorations; 518 | } 519 | 520 | 521 | 522 | 523 | 524 | /** 525 | * Implement StandOut specific additional functionalities. 526 | * 527 | *

528 | * Currently, this method does the following: 529 | * 530 | *

531 | * Attach resize handles: For every View found to have id R.id.corner, 532 | * attach an OnTouchListener that implements resizing the window. 533 | * 534 | * @param root 535 | * The view hierarchy that is part of the window. 536 | */ 537 | void addFunctionality(View root) { 538 | // corner for resize 539 | if (!Utils.isSet(flags, 540 | StandOutFlags.FLAG_ADD_FUNCTIONALITY_RESIZE_DISABLE)) { 541 | View corner = root.findViewById(R.id.corner); 542 | if (corner != null) { 543 | corner.setOnTouchListener(new OnTouchListener() { 544 | 545 | @Override 546 | public boolean onTouch(View v, MotionEvent event) { 547 | // handle dragging to move 548 | boolean consumed = mContext.onTouchHandleResize(id, 549 | Window.this, v, event); 550 | 551 | return consumed; 552 | } 553 | }); 554 | } 555 | } 556 | 557 | // window_icon for drop down 558 | if (!Utils.isSet(flags, 559 | StandOutFlags.FLAG_ADD_FUNCTIONALITY_DROP_DOWN_DISABLE)) { 560 | final View icon = root.findViewById(R.id.window_icon); 561 | if (icon != null) { 562 | icon.setOnClickListener(new OnClickListener() { 563 | 564 | @Override 565 | public void onClick(View v) { 566 | PopupWindow dropDown = mContext.getDropDown(id); 567 | if (dropDown != null) { 568 | dropDown.showAsDropDown(icon); 569 | } 570 | } 571 | }); 572 | } 573 | } 574 | } 575 | 576 | /** 577 | * Iterate through each View in the view hiearchy and implement StandOut 578 | * specific compatibility workarounds. 579 | * 580 | *

581 | * Currently, this method does the following: 582 | * 583 | *

584 | * Nothing yet. 585 | * 586 | * @param root 587 | * The root view hierarchy to iterate through and check. 588 | */ 589 | void fixCompatibility(View root) { 590 | Queue queue = new LinkedList(); 591 | queue.add(root); 592 | 593 | View view = null; 594 | while ((view = queue.poll()) != null) { 595 | // do nothing yet 596 | 597 | // iterate through children 598 | if (view instanceof ViewGroup) { 599 | ViewGroup group = (ViewGroup) view; 600 | for (int i = 0; i < group.getChildCount(); i++) { 601 | queue.add(group.getChildAt(i)); 602 | } 603 | } 604 | } 605 | } 606 | 607 | /** 608 | * Convenient way to resize or reposition a Window. The Editor allows you to 609 | * easily resize and reposition the window around anchor points. 610 | * 611 | * @author Mark Wei 612 | * 613 | */ 614 | public class Editor { 615 | /** 616 | * Special value for width, height, x, or y positions that represents 617 | * that the value should not be changed. 618 | */ 619 | public static final int UNCHANGED = Integer.MIN_VALUE; 620 | 621 | /** 622 | * Layout params of the window associated with this Editor. 623 | */ 624 | StandOutLayoutParams mParams; 625 | 626 | /** 627 | * The position of the anchor point as a percentage of the window's 628 | * width/height. The anchor point is only used by the {@link Editor}. 629 | * 630 | *

631 | * The anchor point effects the following methods: 632 | * 633 | *

634 | * {@link #setSize(float, float)}, {@link #setSize(int, int)}, 635 | * {@link #setPosition(int, int)}, {@link #setPosition(int, int)}. 636 | * 637 | * The window will move, expand, or shrink around the anchor point. 638 | * 639 | *

640 | * Values must be between 0 and 1, inclusive. 0 means the left/top, 0.5 641 | * is the center, 1 is the right/bottom. 642 | */ 643 | float anchorX, anchorY; 644 | 645 | public Editor() { 646 | mParams = getLayoutParams(); 647 | anchorX = anchorY = 0; 648 | } 649 | 650 | public Editor setAnchorPoint(float x, float y) { 651 | if (x < 0 || x > 1 || y < 0 || y > 1) { 652 | throw new IllegalArgumentException( 653 | "Anchor point must be between 0 and 1, inclusive."); 654 | } 655 | 656 | anchorX = x; 657 | anchorY = y; 658 | 659 | return this; 660 | } 661 | 662 | /** 663 | * Set the size of this window as percentages of max screen size. The 664 | * window will expand and shrink around the top-left corner, unless 665 | * you've set a different anchor point with 666 | * {@link #setAnchorPoint(float, float)}. 667 | * 668 | * Changes will not applied until you {@link #commit()}. 669 | * 670 | * @param percentWidth 671 | * @param percentHeight 672 | * @return The same Editor, useful for method chaining. 673 | */ 674 | public Editor setSize(float percentWidth, float percentHeight) { 675 | return setSize((int) (displayWidth * percentWidth), 676 | (int) (displayHeight * percentHeight)); 677 | } 678 | 679 | /** 680 | * Set the size of this window in absolute pixels. The window will 681 | * expand and shrink around the top-left corner, unless you've set a 682 | * different anchor point with {@link #setAnchorPoint(float, float)}. 683 | * 684 | * Changes will not applied until you {@link #commit()}. 685 | * 686 | * @param width 687 | * @param height 688 | * @return The same Editor, useful for method chaining. 689 | */ 690 | public Editor setSize(int width, int height) { 691 | return setSize(width, height, false); 692 | } 693 | 694 | /** 695 | * Set the size of this window in absolute pixels. The window will 696 | * expand and shrink around the top-left corner, unless you've set a 697 | * different anchor point with {@link #setAnchorPoint(float, float)}. 698 | * 699 | * Changes will not applied until you {@link #commit()}. 700 | * 701 | * @param width 702 | * @param height 703 | * @param skip 704 | * Don't call {@link #setPosition(int, int)} to avoid stack 705 | * overflow. 706 | * @return The same Editor, useful for method chaining. 707 | */ 708 | private Editor setSize(int width, int height, boolean skip) { 709 | if (mParams != null) { 710 | if (anchorX < 0 || anchorX > 1 || anchorY < 0 || anchorY > 1) { 711 | throw new IllegalStateException( 712 | "Anchor point must be between 0 and 1, inclusive."); 713 | } 714 | 715 | int lastWidth = mParams.width; 716 | int lastHeight = mParams.height; 717 | 718 | if (width != UNCHANGED) { 719 | mParams.width = width; 720 | } 721 | if (height != UNCHANGED) { 722 | mParams.height = height; 723 | } 724 | 725 | // set max width/height 726 | int maxWidth = mParams.maxWidth; 727 | int maxHeight = mParams.maxHeight; 728 | 729 | if (Utils.isSet(flags, 730 | StandOutFlags.FLAG_WINDOW_EDGE_LIMITS_ENABLE)) { 731 | maxWidth = (int) Math.min(maxWidth, displayWidth); 732 | maxHeight = (int) Math.min(maxHeight, displayHeight); 733 | } 734 | 735 | // keep window between min and max 736 | mParams.width = Math.min( 737 | Math.max(mParams.width, mParams.minWidth), maxWidth); 738 | mParams.height = Math.min( 739 | Math.max(mParams.height, mParams.minHeight), maxHeight); 740 | 741 | // keep window in aspect ratio 742 | if (Utils.isSet(flags, 743 | StandOutFlags.FLAG_WINDOW_ASPECT_RATIO_ENABLE)) { 744 | int ratioWidth = (int) (mParams.height * touchInfo.ratio); 745 | int ratioHeight = (int) (mParams.width / touchInfo.ratio); 746 | if (ratioHeight >= mParams.minHeight 747 | && ratioHeight <= mParams.maxHeight) { 748 | // width good adjust height 749 | mParams.height = ratioHeight; 750 | } else { 751 | // height good adjust width 752 | mParams.width = ratioWidth; 753 | } 754 | } 755 | 756 | if (!skip) { 757 | // set position based on anchor point 758 | setPosition((int) (mParams.x + lastWidth * anchorX), 759 | (int) (mParams.y + lastHeight * anchorY)); 760 | } 761 | } 762 | 763 | return this; 764 | } 765 | 766 | /** 767 | * Set the position of this window as percentages of max screen size. 768 | * The window's top-left corner will be positioned at the given x and y, 769 | * unless you've set a different anchor point with 770 | * {@link #setAnchorPoint(float, float)}. 771 | * 772 | * Changes will not applied until you {@link #commit()}. 773 | * 774 | * @param percentWidth 775 | * @param percentHeight 776 | * @return The same Editor, useful for method chaining. 777 | */ 778 | public Editor setPosition(float percentWidth, float percentHeight) { 779 | return setPosition((int) (displayWidth * percentWidth), 780 | (int) (displayHeight * percentHeight)); 781 | } 782 | 783 | /** 784 | * Set the position of this window in absolute pixels. The window's 785 | * top-left corner will be positioned at the given x and y, unless 786 | * you've set a different anchor point with 787 | * {@link #setAnchorPoint(float, float)}. 788 | * 789 | * Changes will not applied until you {@link #commit()}. 790 | * 791 | * @param x 792 | * @param y 793 | * @return The same Editor, useful for method chaining. 794 | */ 795 | public Editor setPosition(int x, int y) { 796 | return setPosition(x, y, false); 797 | } 798 | 799 | /** 800 | * Set the position of this window in absolute pixels. The window's 801 | * top-left corner will be positioned at the given x and y, unless 802 | * you've set a different anchor point with 803 | * {@link #setAnchorPoint(float, float)}. 804 | * 805 | * Changes will not applied until you {@link #commit()}. 806 | * 807 | * @param x 808 | * @param y 809 | * @param skip 810 | * Don't call {@link #setPosition(int, int)} and 811 | * {@link #setSize(int, int)} to avoid stack overflow. 812 | * @return The same Editor, useful for method chaining. 813 | */ 814 | private Editor setPosition(int x, int y, boolean skip) { 815 | if (mParams != null) { 816 | if (anchorX < 0 || anchorX > 1 || anchorY < 0 || anchorY > 1) { 817 | throw new IllegalStateException( 818 | "Anchor point must be between 0 and 1, inclusive."); 819 | } 820 | 821 | // sets the x and y correctly according to anchorX and 822 | // anchorY 823 | if (x != UNCHANGED) { 824 | mParams.x = (int) (x - mParams.width * anchorX); 825 | } 826 | if (y != UNCHANGED) { 827 | mParams.y = (int) (y - mParams.height * anchorY); 828 | } 829 | 830 | if (Utils.isSet(flags, 831 | StandOutFlags.FLAG_WINDOW_EDGE_LIMITS_ENABLE)) { 832 | // if gravity is not TOP|LEFT throw exception 833 | if (mParams.gravity != (Gravity.TOP | Gravity.LEFT)) { 834 | throw new IllegalStateException( 835 | "The window " 836 | + id 837 | + " gravity must be TOP|LEFT if FLAG_WINDOW_EDGE_LIMITS_ENABLE or FLAG_WINDOW_EDGE_TILE_ENABLE is set."); 838 | } 839 | 840 | // keep window inside edges 841 | mParams.x = Math.min(Math.max(mParams.x, 0), displayWidth 842 | - mParams.width); 843 | mParams.y = Math.min(Math.max(mParams.y, 0), displayHeight 844 | - mParams.height); 845 | } 846 | } 847 | 848 | return this; 849 | } 850 | 851 | /** 852 | * Commit the changes to this window. Updates the layout. This Editor 853 | * cannot be used after you commit. 854 | */ 855 | public void commit() { 856 | if (mParams != null) { 857 | mContext.updateViewLayout(id, mParams); 858 | mParams = null; 859 | } 860 | } 861 | } 862 | 863 | public static class WindowDataKeys { 864 | public static final String IS_MAXIMIZED = "isMaximized"; 865 | public static final String WIDTH_BEFORE_MAXIMIZE = "widthBeforeMaximize"; 866 | public static final String HEIGHT_BEFORE_MAXIMIZE = "heightBeforeMaximize"; 867 | public static final String X_BEFORE_MAXIMIZE = "xBeforeMaximize"; 868 | public static final String Y_BEFORE_MAXIMIZE = "yBeforeMaximize"; 869 | } 870 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZYFDroid/android-webgame-browser/3b21e0439696ee68b6b04856c10f305b23c7e5ec/app/src/main/res/drawable-hdpi/close.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/corner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZYFDroid/android-webgame-browser/3b21e0439696ee68b6b04856c10f305b23c7e5ec/app/src/main/res/drawable-hdpi/corner.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/hide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZYFDroid/android-webgame-browser/3b21e0439696ee68b6b04856c10f305b23c7e5ec/app/src/main/res/drawable-hdpi/hide.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_menu_capture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZYFDroid/android-webgame-browser/3b21e0439696ee68b6b04856c10f305b23c7e5ec/app/src/main/res/drawable-hdpi/ic_menu_capture.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/maximize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZYFDroid/android-webgame-browser/3b21e0439696ee68b6b04856c10f305b23c7e5ec/app/src/main/res/drawable-hdpi/maximize.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_notification_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZYFDroid/android-webgame-browser/3b21e0439696ee68b6b04856c10f305b23c7e5ec/app/src/main/res/drawable-xhdpi/ic_notification_icon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/shadow.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZYFDroid/android-webgame-browser/3b21e0439696ee68b6b04856c10f305b23c7e5ec/app/src/main/res/drawable-xhdpi/shadow.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_btn_close.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_btn_cmd.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_frame.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZYFDroid/android-webgame-browser/3b21e0439696ee68b6b04856c10f305b23c7e5ec/app/src/main/res/drawable/ic.png -------------------------------------------------------------------------------- /app/src/main/res/layout/about.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 12 | 13 | 21 | 22 | 28 | 29 | 30 |