diff --git a/README.md b/README.md index 896a0a6..75923ac 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,56 @@ # android_virtual_cam -xposed安卓虚拟摄像头 -## 感谢https://github.com/wangwei1237/CameraHook 提供的HOOK思路!! -## 求有无极的大佬,希望帮忙测试一下此模块虚拟框架下是否可用,测试后希望在issue中反馈一下,谢谢!!! - -## 具体的使用方法(English version is below): -1、安装xposed框架(传统xposed,edxp,lsposed等均可,不确定虚拟框架能否使用,已经确定VMOS可用,应用转生不可用) -2、安装模块,启用模块,lsposed等包含定义域的框架需要选勾目标app,但无需选勾系统框架。 -3、将需要替换的视频命名为`virtual.mp4`,放在`/sdcard/DCIM/Camera1/`目录下。(前置摄像头需要水平翻转后右旋90°保存,如果打开相机时看见“宽:...高:...”,则需要匹配分辨率) -4、若需要拦截拍照事件,请在`/sdcard/DCIM/Camera1/`目录下放置 `1000.bmp` 用于替换,(前置摄像头需要水平翻转后右旋90°保存,需要匹配分辨率) -> 注意:不是所有应用的拍照都调用`1000.bmp`,有很多app只是从`virtual.mp4`里截屏,只有在拍照时弹出“发现拍照”才是真正的调用`1000.bmp` - -5、**授予目标应用读取本地文件的权限,至少是允许读取媒体文件。** -6、强制结束目标应用/重启手机。 - -## 如何获得分辨率??(仅看见“宽:...高:...”需要) -在目标应用中打开摄像头,可在弹出的toast消息里看见。 - -## Camera2接口有问题?? -是的,目前Camera2接口的HOOK不是所有应用程序都能生效,部分app报错打开相机失败,如果想停用Camera2接口的HOOK,可在`/sdcard/DCIM/Camera1/`下创建`disable.jpg`,以停用此项HOOK - -## 我不需要静音?? -在`/sdcard/DCIM/Camera1/`下创建`no-silent.jpg`,就不会静音了。 - -## Detailed usage : -1. Install this moudle. enable it in Xposed. Framework which has a scope list need to choose target app, but needn't to choose system framework. -2. Create `virtual.mp4` and put it under `/sdcard/DCIM/Camera1/` ,(if use front camera ,image should be Flip horizontal and right rotation 90 degrees, if you see "宽:...高:..." when opan camera, the resolution should be matched) -3. If you wants to hook image capture event, you should create `1000.bmp` under `/sdcard/DCIM/Camera1/` for replace. (if use front camera ,image should be Flip horizontal and right rotation 90 degrees, the resolution should be matched) -> ATTENTION: NOT all apps invoke `1000.bmp` when capture image, some apps directly capture a photo from `virtual.mp4`, when you see “发现拍照” when take photos, the app invoke `1000.bmp` . -4. authorize the target app to access local storage in system. -5. Reboot your phone or shutdown target app. - -## bugs with camera2 api, need to disable it? -create `disable.jpg` under `/sdcard/DCIM/Camera1/` to disable this method hook. -## how to get resolution ??(only needed if you see "宽:...高:...")? -open camera in target app, and you can find resolution in toast message. -## Needn't mute? -Create `no-silent.jpg` under `/sdcard/DCIM/Camera1/`, and it will play sounds. - -## release无法下载/gitee下载(gitee与github作者同id,同仓库名)?? -在/app/release/app-release.apk,下载前请注意分支。 -GitHub: https://github.com/w2016561536/android_virtual_cam/blob/master/app/release/app-release.apk -gitee(中国大陆建议此点): https://gitee.com/w2016561536/android_virtual_cam/blob/master/app/release/app-release.apk - -# 请勿用于非法用途,所有后果自负。 -# DO NOT USE FOR ANY ILLEAGLE INTENTION!!YOU NEED TO TAKE ALL RESPONSIBILITY AND CONSEQUENCE!!" + +[简体中文](./README.md) | [繁體中文](./README_tc.md) | [English](./README_en.md) + +基于Xposed的虚拟摄像头 + +# 请勿用于任何非法用途,所有后果自负。 + +## 支持平台: + +- 安卓5.0+ + +## 使用方法 + +1. 安装此模块,并在Xposed中启用此模块,Lsposed等包含作用域的框架需要选择目标app,无需选择系统框架。 + +2. 在系统设置中,授予目标应用读取本地存储的权限,并强制结束目标应用程序。若应用程序未申请此权限,请见步骤3。 + +3. 打开目标应用,若应用未能获得读取存储的权限,则会以气泡消息提示,`Camera1`目录被重定向至应用程序私有目录`/[内部存储]/Android/data/[应用包名]/files/Camera1/`。若未提示,则默认`Camera1`目录为`/[内部存储]/DCIM/Camera1/`。若目录不存在,请手动创建。 + +> 注意:私有目录下的`Camera1`仅对该应用单独生效。 + +4. 在目标应用中打开相机预览,会以气泡消息提示“宽:……高:……”,需要根据此分辨率数据制作替换视频,放置于`Camera1`目录下,并命名为`virtual.mp4`,若打开相机并无提示消息,则无需调整视频分辨率。 + +5. 若在目标应用中拍照却显示真实图片,且出现气泡消息`发现拍照`和分辨率,则需根据此分辨率数据准备一张照片,命名为`1000.bmp`,放入`Camera1`目录下(支持其它格式改后缀为bmp)。如果拍照时无气泡消息提示,则`1000.bmp`无效。 + +6. 如果需要播放视频的声音,需在`Camera1`目录下创建`no-silent.jpg`文件。 + +7. 如果需要临时停用视频替换,需在`Camera1`目录下创建`disable.jpg`。 + + +## 常见问题 + +A1. 前置摄像头方向问题? +Q1. 大多数情况下,替换前置摄像头的视频需要水平翻转并右旋90度,并且视频***处理后***的分辨率应与气泡消息内分辨率相同。但有时这并不需要,具体请根据实际情况判断。 + + +Q2. 画面黑屏,相机启动失败? +A2. 目前有些应用并不能成功替换(特别是系统相机),但剩下的大多是由于视频**编码格式**不正确(并非封装格式),目前仅支持`H.264`编码格式。或者时因为视频路径不对。 + + +Q3. 画面花屏? +A3. 视频分辨率不对。 + +## 反馈问题 + +请直接在issues中反馈,如果为BUG反馈,请附带日志信息。 + + +## 致谢: + +提供HOOK思路: https://github.com/wangwei1237/CameraHook + +H264硬解码: https://github.com/zhantong/Android-VideoToImages + +JPEG转YUV: https://blog.csdn.net/jacke121/article/details/73888732 diff --git a/README_en.md b/README_en.md new file mode 100644 index 0000000..4a85cf2 --- /dev/null +++ b/README_en.md @@ -0,0 +1,52 @@ +# android_virtual_cam + +[简体中文](./README.md) | [繁體中文](./README_tc.md) | [English](./README_en.md) + +A virtual camera based on Xposed + +## DO NOT USE FOR ANY ILLEGAL PURPOSE, YOU NEED TO TAKE ALL RESPONSIBILITY AND CONSEQUENCE! + +## Supported platform + +- Android 5.0+ + +## Usage + +1. Install this module , enable it in Xposed . Lsposed and other framework which have a scope list, you need to choose target app instead of System Framework. + +2. In system Setting, authorize target to access local storage, and force stop the app. If the app does not request this permission, see step3. + +3. open target app, if the app does not have the permission to access local storage. There will be a toast message showing that `Camera1` directory has been redirect to app's private directory `/[INTERNEL_STORAGE]/Android/data/[package_name]/files/Camera1/`. If there isn't the message, the default `Camera1` directory is `/[INTERNEL_STORAGE]/DCIM/Camera1/`. If the directory doesn't exist. Please create it by yourself. + +> Attention: `Camera1` in the private directory only works for single app. + +4. Open the camera in target app. There will be a toast message showing the resolution (宽width: , 高height:) . And you need to adjust the replacing video's resolution to make them same. Name it as `virtual.mp4`, put it under `Camera1` directory. + +5. If there is a toast message when you take photoes in app ("发现拍照"),it shows the photo's resolution. You need to prepare a photo which has the same resolution. Name it as `1000.bmp` . Put it under `Camera1` directory. (it support other image format renamed to bmp ). If there isn't a toast message , `1000.bmp` will have nothing to do with replacing capture. + +6. If you need to play video's sound, create `no-silent.jpg` under `Camera1` directory. + +7. If you nedd to turn off the module temporarily, create `disable.jpg` under `Camera1` directory. + +## FAQ + +Q1. The problems of front camera? +A1. In most cases , the video for replacing front camera need to be flipped horizontally and rotated right 90 degrees. The video's resolution **after being processed** need to same with that in toast message. But in some came, it dosen't need to make adjustment, so you need to judge it according to situation. + +Q2. Black screen ? Open camera fail ? +A2. Till now ,there are a few apps that can't be hooked, especially the system camera. But most of it caused by wrong encoding format (only support `H.264`). Or it caused by wrong `Camera1` directory. + +Q3. Blurred screen? +A3. The resolution of video is wrong. + +## Question report: + +raise it in issues directly. If it is a bug, please attach with Xposed log. + +## Credit + +Provide hook method: https://github.com/wangwei1237/CameraHook + +H.264 hardware decode: https://github.com/zhantong/Android-VideoToImages + +JPEG-YUV convert: https://blog.csdn.net/jacke121/article/details/73888732 diff --git a/README_tc.md b/README_tc.md new file mode 100644 index 0000000..5eabecf --- /dev/null +++ b/README_tc.md @@ -0,0 +1,53 @@ +# android_virtual_cam + +[简体中文](./README.md) | [繁體中文](./README_tc.md) | [English](./README_en.md) + +基於Xposed的虛擬攝影機 + +# 請勿用於任何非法用途,所有後果自負。 + +## 支持平臺: + +- 安卓5.0+ + +## 使用方法 + +1. 安裝此模組,並在Xposed中啟用此模組,Lsposed等包含作用域的框架需要選擇目標app,無需選擇系統框架。 + +2. 在系統設定中,授予目標應用讀取本地存儲的許可權,並強制結束目標應用程序。 若應用程序未申請此許可權,請見步驟3。 + +3. 打開目標應用,若應用未能獲得讀取存儲的許可權,則會以氣泡消息提示,`Camera1`目錄被重定向至應用程序私有目錄`/[內部存儲]/Android/data/[應用包名]/files/Camera1/`。 若未提示,則默認`Camera1`目錄為`/[內部存儲]/DCIM/Camera1/`。 若目錄不存在,請手動創建。 + +> 注意:私有目錄下的`Camera1`僅對該應用單獨生效。 + +4. 在目標應用中打開相機預覽,會以氣泡消息提示“寬:……高:……”,需要根據此解析度數據製作替換影片,放置於`Camera1`目錄下,並命名為`virtual.mp4`,若打開相機並無提示消息,則無需調整影片解析度。 + +5. 若在目標應用中拍照卻顯示真實圖片,且出現氣泡消息`發現拍照`和解析度,則需根據此解析度數據準備一張照片,命名為`1000.bmp`,放入`Camera1`目錄下(支持其它格式改尾碼為bmp)。 如果拍照時無氣泡消息提示,則`1000.bmp`無效。 + +6. 如果需要播放影片的聲音,需在`Camera1`目錄下創建`no-silent.jpg`檔案。 + +7. 如果需要臨時停用影片替換,需在`Camera1`目錄下創建`disable.jpg`。 + +## 常見問題 + +A1. 前置攝影機方向問題? +Q1.大多數情况下,替換前置攝影機的影片需要水准翻轉並右旋90度,並且影片***處理後***的解析度應與氣泡消息內解析度相同。 但有時這並不需要,具體請根據實際情況判斷。 + +Q2. 畫面黑屏,相機啟動失敗? +A2. 現時有些應用並不能成功替換(特別是系統相機),但剩下的大多是由於影片**編碼格式**不正確(並非封裝格式),現時僅支持`H.264`編碼格式。 或者時因為影片路徑不對。 + +Q3. 畫面花屏? +A3. 影片解析度不對。 + +## 迴響問題 + +請直接在issues中迴響,如果為BUG迴響,請附帶日誌資訊。 + + +##致謝: + +提供HOOK思路: https://github.com/wangwei1237/CameraHook + +H264硬解碼: https://github.com/zhantong/Android-VideoToImages + +JPEG轉YUV: https://blog.csdn.net/jacke121/article/details/73888732 \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 22a0f0b..b0dce34 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,10 +9,10 @@ android { applicationId "com.example.vcam" minSdk 21 targetSdk 28 - versionCode 17 - versionName "3.4" + versionCode 18 + versionName "3.5" - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { @@ -28,13 +28,7 @@ android { } dependencies { - - implementation 'androidx.appcompat:appcompat:1.2.0' - implementation 'com.google.android.material:material:1.3.0' - implementation 'androidx.constraintlayout:constraintlayout:2.0.4' compileOnly 'de.robv.android.xposed:api:82' compileOnly 'de.robv.android.xposed:api:82:sources' - testImplementation 'junit:junit:4.+' - androidTestImplementation 'androidx.test.ext:junit:1.1.2' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + } \ No newline at end of file diff --git a/app/release/app-release.apk b/app/release/app-release.apk index d58736d..e9da87d 100644 Binary files a/app/release/app-release.apk and b/app/release/app-release.apk differ diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json index 9177647..1a47272 100644 --- a/app/release/output-metadata.json +++ b/app/release/output-metadata.json @@ -11,8 +11,8 @@ "type": "SINGLE", "filters": [], "attributes": [], - "versionCode": 17, - "versionName": "3.4", + "versionCode": 18, + "versionName": "3.5", "outputFile": "app-release.apk" } ], diff --git a/app/src/main/java/com/example/vcam/HookMain.java b/app/src/main/java/com/example/vcam/HookMain.java index d60a6c2..78c8641 100644 --- a/app/src/main/java/com/example/vcam/HookMain.java +++ b/app/src/main/java/com/example/vcam/HookMain.java @@ -1,9 +1,11 @@ package com.example.vcam; +import android.Manifest; import android.annotation.SuppressLint; import android.app.Application; import android.content.Context; +import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.ImageFormat; @@ -30,8 +32,10 @@ import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; +import java.nio.file.Path; import java.util.Arrays; import java.util.List; import java.util.concurrent.Executor; @@ -73,6 +77,8 @@ public class HookMain implements IXposedHookLoadPackage { public static int onemwidth; public static Class camera_callback_calss; + public static String video_path = Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera1/"; + public static Surface c2_preview_Surfcae; public static Surface c2_preview_Surfcae_1; public static Surface c2_reader_Surfcae; @@ -84,6 +90,8 @@ public class HookMain implements IXposedHookLoadPackage { public static SurfaceTexture c2_virtual_surfaceTexture; public boolean need_recreate; + public static String last_package_name; + public int c2_ori_width = 1280; public int c2_ori_height = 720; @@ -91,12 +99,16 @@ public class HookMain implements IXposedHookLoadPackage { public Context toast_content; public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam lpparam) throws Exception { - File file = new File(Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera1/virtual.mp4"); + File file = new File(video_path + "virtual.mp4"); if (file.exists()) { Class cameraclass = XposedHelpers.findClass("android.hardware.Camera", lpparam.classLoader); XposedHelpers.findAndHookMethod(cameraclass, "setPreviewTexture", SurfaceTexture.class, new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) { + File control_file = new File(video_path + "disable.jpg"); + if (control_file.exists()) { + return; + } if (is_hooked) { is_hooked = false; return; @@ -108,22 +120,22 @@ protected void beforeHookedMethod(MethodHookParam param) { return; } if (reallycamera != null && reallycamera.equals(param.thisObject)) { - param.args[0] = HookMain.virtual_st; - XposedBridge.log("发现重复" + reallycamera.toString()); + param.args[0] = virtual_st; + XposedBridge.log("【VCAM】发现重复" + reallycamera.toString()); return; } else { - XposedBridge.log("创建预览"); + XposedBridge.log("【VCAM】创建预览"); } reallycamera = (Camera) param.thisObject; - HookMain.msurftext = (SurfaceTexture) param.args[0]; - if (HookMain.virtual_st == null) { - HookMain.virtual_st = new SurfaceTexture(10); + msurftext = (SurfaceTexture) param.args[0]; + if (virtual_st == null) { + virtual_st = new SurfaceTexture(10); } else { - HookMain.virtual_st.release(); - HookMain.virtual_st = new SurfaceTexture(10); + virtual_st.release(); + virtual_st = new SurfaceTexture(10); } - param.args[0] = HookMain.virtual_st; + param.args[0] = virtual_st; } }); } else { @@ -136,18 +148,18 @@ protected void beforeHookedMethod(MethodHookParam param) { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { c2_state_callback = param.args[1].getClass(); - File control_file = new File(Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera1/disable.jpg"); + File control_file = new File(video_path + "disable.jpg"); if (control_file.exists()) { return; } - File file = new File(Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera1/virtual.mp4"); + File file = new File(video_path + "virtual.mp4"); if (!file.exists()) { if (toast_content != null) { Toast.makeText(toast_content, "不存在替换视频", Toast.LENGTH_SHORT).show(); return; } } - XposedBridge.log("1位参数初始化相机,类:" + c2_state_callback.toString()); + XposedBridge.log("【VCAM】1位参数初始化相机,类:" + c2_state_callback.toString()); is_first_hook_build = true; process_camera2_init(c2_state_callback); } @@ -158,11 +170,11 @@ protected void beforeHookedMethod(MethodHookParam param) throws Throwable { XposedHelpers.findAndHookMethod("android.hardware.camera2.CameraManager", lpparam.classLoader, "openCamera", String.class, Executor.class, CameraDevice.StateCallback.class, new XC_MethodHook() { @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { - File control_file = new File(Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera1/disable.jpg"); + File control_file = new File(video_path + "disable.jpg"); if (control_file.exists()) { return; } - File file = new File(Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera1/virtual.mp4"); + File file = new File(video_path + "virtual.mp4"); if (!file.exists()) { if (toast_content != null) { Toast.makeText(toast_content, "不存在替换视频", Toast.LENGTH_SHORT).show(); @@ -170,7 +182,7 @@ protected void afterHookedMethod(MethodHookParam param) throws Throwable { } } c2_state_callback = param.args[2].getClass(); - XposedBridge.log("2位参数初始化相机,类:" + c2_state_callback.toString()); + XposedBridge.log("【VCAM】2位参数初始化相机,类:" + c2_state_callback.toString()); is_first_hook_build = true; process_camera2_init(c2_state_callback); } @@ -217,7 +229,7 @@ protected void beforeHookedMethod(MethodHookParam param) { XposedHelpers.findAndHookMethod("android.hardware.Camera", lpparam.classLoader, "takePicture", Camera.ShutterCallback.class, Camera.PictureCallback.class, Camera.PictureCallback.class, Camera.PictureCallback.class, new XC_MethodHook() { @Override protected void afterHookedMethod(MethodHookParam param) { - XposedBridge.log("4参数拍照"); + XposedBridge.log("【VCAM】4参数拍照"); if (param.args[1] != null) { process_a_shot_YUV(param); } @@ -257,7 +269,49 @@ protected void beforeHookedMethod(MethodHookParam param) throws Throwable { protected void afterHookedMethod(MethodHookParam param) throws Throwable { super.afterHookedMethod(param); if (param.args[0] instanceof Application) { - toast_content = ((Application) param.args[0]).getApplicationContext(); + try { + toast_content = ((Application) param.args[0]).getApplicationContext(); + }catch (Exception ee){ + XposedBridge.log("【VCAM】"+ee.toString()); + } + if (toast_content != null){ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + int auth_statue = PackageManager.PERMISSION_DENIED; + try { + auth_statue = toast_content.checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE); + }catch (Exception eee){ + + } + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + auth_statue = toast_content.checkSelfPermission(Manifest.permission.MANAGE_EXTERNAL_STORAGE); + } + }catch (Exception eee){ + + } + if ( auth_statue != PackageManager.PERMISSION_GRANTED){ + File shown_file = new File(Environment.getExternalStorageDirectory()+"/Android/data/"+lpparam.packageName+"/files/Camera1/" + "has_shown.jpg"); + if (!(lpparam.packageName.equals(BuildConfig.APPLICATION_ID) || shown_file.exists())) { + Toast.makeText(toast_content, "未授予读取本地目录权限,请检查权限\nCamera1目前重定向为 " + Environment.getExternalStorageDirectory() + "/Android/data/" + lpparam.packageName + "/files/Camera1/", Toast.LENGTH_LONG).show(); + String path = Environment.getExternalStorageDirectory()+"/Android/data/"+lpparam.packageName+"/files/Camera1/" ; + try { + FileOutputStream fos = new FileOutputStream(path+ "has_shown.jpg"); + String info = "shown"; + fos.write(info.getBytes()); + fos.flush(); + fos.close(); + } catch (Exception e) { + XposedBridge.log("【VCAM】"+ e.toString()); + } + } + video_path = Environment.getExternalStorageDirectory()+"/Android/data/"+lpparam.packageName+"/files/Camera1/"; + }else { + video_path = Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera1/"; + } + }else { + video_path = Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera1/"; + } + } } } }); @@ -265,15 +319,19 @@ protected void afterHookedMethod(MethodHookParam param) throws Throwable { XposedHelpers.findAndHookMethod("android.hardware.Camera", lpparam.classLoader, "startPreview", new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { - File file = new File(Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera1/virtual.mp4"); + File file = new File(video_path + "virtual.mp4"); if (!file.exists()) { if (toast_content != null) { Toast.makeText(toast_content, "不存在替换视频", Toast.LENGTH_SHORT).show(); return; } } + File control_file = new File(video_path + "disable.jpg"); + if (control_file.exists()) { + return; + } is_someone_playing = false; - XposedBridge.log("开始预览"); + XposedBridge.log("【VCAM】开始预览"); start_preview_camera = (Camera) param.thisObject; if (ori_holder != null) { @@ -284,73 +342,73 @@ protected void beforeHookedMethod(MethodHookParam param) throws Throwable { mplayer1 = null; mplayer1 = new MediaPlayer(); } - if (!HookMain.ori_holder.getSurface().isValid() || HookMain.ori_holder == null) { + if (!ori_holder.getSurface().isValid() || ori_holder == null) { return; } - HookMain.mplayer1.setSurface(HookMain.ori_holder.getSurface()); - File sfile = new File(Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera1/no-silent.jpg"); + mplayer1.setSurface(ori_holder.getSurface()); + File sfile = new File(video_path + "no-silent.jpg"); if (!(sfile.exists() && (!is_someone_playing))) { - HookMain.mplayer1.setVolume(0, 0); + mplayer1.setVolume(0, 0); is_someone_playing = false; } else { is_someone_playing = true; } - HookMain.mplayer1.setLooping(true); + mplayer1.setLooping(true); - HookMain.mplayer1.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { + mplayer1.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mp) { - HookMain.mplayer1.start(); + mplayer1.start(); } }); try { - HookMain.mplayer1.setDataSource(Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera1/virtual.mp4"); - HookMain.mplayer1.prepare(); + mplayer1.setDataSource(video_path + "virtual.mp4"); + mplayer1.prepare(); } catch (IOException e) { - XposedBridge.log(e.toString()); + XposedBridge.log("【VCAM】"+e.toString()); } } if (msurftext != null) { - if (HookMain.msurf == null) { - HookMain.msurf = new Surface(HookMain.msurftext); + if (msurf == null) { + msurf = new Surface(msurftext); } else { - HookMain.msurf.release(); - HookMain.msurf = new Surface(HookMain.msurftext); + msurf.release(); + msurf = new Surface(msurftext); } - if (HookMain.mMedia == null) { - HookMain.mMedia = new MediaPlayer(); + if (mMedia == null) { + mMedia = new MediaPlayer(); } else { - HookMain.mMedia.release(); - HookMain.mMedia = new MediaPlayer(); + mMedia.release(); + mMedia = new MediaPlayer(); } - HookMain.mMedia.setSurface(HookMain.msurf); + mMedia.setSurface(msurf); - File sfile = new File(Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera1/no-silent.jpg"); + File sfile = new File(video_path + "no-silent.jpg"); if (!(sfile.exists() && (!is_someone_playing))) { - HookMain.mMedia.setVolume(0, 0); + mMedia.setVolume(0, 0); is_someone_playing = false; } else { is_someone_playing = true; } - HookMain.mMedia.setLooping(true); + mMedia.setLooping(true); - HookMain.mMedia.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { + mMedia.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mp) { - HookMain.mMedia.start(); + mMedia.start(); } }); try { - HookMain.mMedia.setDataSource(Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera1/virtual.mp4"); - HookMain.mMedia.prepare(); + mMedia.setDataSource(video_path + "virtual.mp4"); + mMedia.prepare(); } catch (IOException e) { - XposedBridge.log(e.toString()); + XposedBridge.log("【VCAM】" + e.toString()); } } } @@ -359,14 +417,18 @@ public void onPrepared(MediaPlayer mp) { XposedHelpers.findAndHookMethod("android.hardware.Camera", lpparam.classLoader, "setPreviewDisplay", SurfaceHolder.class, new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { - XposedBridge.log("添加Surfaceview预览"); - File file = new File(Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera1/virtual.mp4"); + XposedBridge.log("【VCAM】添加Surfaceview预览"); + File file = new File(video_path + "virtual.mp4"); if (!file.exists()) { if (toast_content != null) { Toast.makeText(toast_content, "不存在替换视频", Toast.LENGTH_SHORT).show(); return; } } + File control_file = new File(video_path + "disable.jpg"); + if (control_file.exists()) { + return; + } mcamera1 = (Camera) param.thisObject; ori_holder = (SurfaceHolder) param.args[0]; if (c1_fake_texture == null) { @@ -397,7 +459,7 @@ protected void beforeHookedMethod(MethodHookParam param) { if (param.args[0] == null) { return; } - File control_file = new File(Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera1/disable.jpg"); + File control_file = new File(video_path + "disable.jpg"); if (control_file.exists()) { return; } @@ -419,7 +481,7 @@ protected void beforeHookedMethod(MethodHookParam param) { } } } - XposedBridge.log("添加目标:" + param.args[0].toString()); + XposedBridge.log("【VCAM】添加目标:" + param.args[0].toString()); param.args[0] = c2_virtual_surface; } @@ -428,11 +490,11 @@ protected void beforeHookedMethod(MethodHookParam param) { XposedHelpers.findAndHookMethod("android.hardware.camera2.CaptureRequest.Builder", lpparam.classLoader, "build", new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { - File control_file = new File(Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera1/disable.jpg"); + File control_file = new File(video_path + "disable.jpg"); if (control_file.exists()) { return; } - XposedBridge.log("开始build请求"); + XposedBridge.log("【VCAM】开始build请求"); process_camera2_play(); } }); @@ -462,12 +524,12 @@ protected void beforeHookedMethod(MethodHookParam param) { XposedHelpers.findAndHookMethod("android.media.ImageReader", lpparam.classLoader, "newInstance", int.class, int.class, int.class, int.class, new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) { - XposedBridge.log("应用创建了渲染器:宽:" + param.args[0] + " 高:" + param.args[1] + "格式" + param.args[2]); + XposedBridge.log("【VCAM】应用创建了渲染器:宽:" + param.args[0] + " 高:" + param.args[1] + "格式" + param.args[2]); c2_ori_width = (int) param.args[0]; c2_ori_height = (int) param.args[1]; Imagereader_format = (int) param.args[2]; if (toast_content != null) { - Toast.makeText(toast_content, "应用创建了渲染器:\n宽:" + param.args[0] + "\n高:" + param.args[1] + "\n一般只需要匹配宽高比", Toast.LENGTH_LONG).show(); + Toast.makeText(toast_content, "应用创建了渲染器:\n宽:" + param.args[0] + "\n高:" + param.args[1] + "\n一般只需要宽高比与视频相同", Toast.LENGTH_LONG).show(); } } }); @@ -477,7 +539,7 @@ protected void beforeHookedMethod(MethodHookParam param) { new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) { - XposedBridge.log("onCaptureFailed" + "原因:" + ((CaptureFailure) param.args[2]).getReason()); + XposedBridge.log("【VCAM】onCaptureFailed" + "原因:" + ((CaptureFailure) param.args[2]).getReason()); } }); @@ -485,58 +547,58 @@ protected void beforeHookedMethod(MethodHookParam param) { public void process_camera2_play() { if (c2_preview_Surfcae != null) { - if (HookMain.c2_player == null) { - HookMain.c2_player = new MediaPlayer(); + if (c2_player == null) { + c2_player = new MediaPlayer(); } else { - HookMain.c2_player.release(); - HookMain.c2_player = new MediaPlayer(); + c2_player.release(); + c2_player = new MediaPlayer(); } - HookMain.c2_player.setSurface(HookMain.c2_preview_Surfcae); - File sfile = new File(Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera1/no-silent.jpg"); + c2_player.setSurface(c2_preview_Surfcae); + File sfile = new File(video_path + "no-silent.jpg"); if (!sfile.exists()) { - HookMain.c2_player.setVolume(0, 0); + c2_player.setVolume(0, 0); } - HookMain.c2_player.setLooping(true); + c2_player.setLooping(true); - HookMain.c2_player.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { + c2_player.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { public void onPrepared(MediaPlayer mp) { - HookMain.c2_player.start(); + c2_player.start(); } }); try { - HookMain.c2_player.setDataSource(Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera1/virtual.mp4"); - HookMain.c2_player.prepare(); + c2_player.setDataSource(video_path + "virtual.mp4"); + c2_player.prepare(); } catch (IOException e) { - XposedBridge.log(e.toString()); + XposedBridge.log("【VCAM】"+e.toString()); } } if (c2_preview_Surfcae_1 != null) { - if (HookMain.c2_player_1 == null) { - HookMain.c2_player_1 = new MediaPlayer(); + if (c2_player_1 == null) { + c2_player_1 = new MediaPlayer(); } else { - HookMain.c2_player_1.release(); - HookMain.c2_player_1 = new MediaPlayer(); + c2_player_1.release(); + c2_player_1 = new MediaPlayer(); } - HookMain.c2_player_1.setSurface(HookMain.c2_preview_Surfcae_1); - File sfile = new File(Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera1/no-silent.jpg"); + c2_player_1.setSurface(c2_preview_Surfcae_1); + File sfile = new File(video_path + "no-silent.jpg"); if (!sfile.exists()) { - HookMain.c2_player_1.setVolume(0, 0); + c2_player_1.setVolume(0, 0); } - HookMain.c2_player_1.setLooping(true); + c2_player_1.setLooping(true); - HookMain.c2_player_1.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { + c2_player_1.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { public void onPrepared(MediaPlayer mp) { - HookMain.c2_player_1.start(); + c2_player_1.start(); } }); try { - HookMain.c2_player_1.setDataSource(Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera1/virtual.mp4"); - HookMain.c2_player_1.prepare(); + c2_player_1.setDataSource(video_path + "virtual.mp4"); + c2_player_1.prepare(); } catch (IOException e) { - XposedBridge.log(e.toString()); + XposedBridge.log("【VCAM】"+e.toString()); } } @@ -554,10 +616,10 @@ public void onPrepared(MediaPlayer mp) { } else { c2_hw_decode_obj.setSaveFrames(Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera2/", OutputImageFormat.NV21); } - c2_hw_decode_obj.set_surfcae(HookMain.c2_reader_Surfcae); - c2_hw_decode_obj.decode(Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera1/virtual.mp4"); + c2_hw_decode_obj.set_surfcae(c2_reader_Surfcae); + c2_hw_decode_obj.decode(video_path + "virtual.mp4"); } catch (Throwable throwable) { - XposedBridge.log(throwable.toString()); + XposedBridge.log("【VCAM】"+throwable.toString()); } } @@ -574,17 +636,17 @@ public void onPrepared(MediaPlayer mp) { } else { c2_hw_decode_obj_1.setSaveFrames(Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera2/", OutputImageFormat.NV21); } - c2_hw_decode_obj_1.set_surfcae(HookMain.c2_reader_Surfcae_1); - c2_hw_decode_obj_1.decode(Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera1/virtual.mp4"); + c2_hw_decode_obj_1.set_surfcae(c2_reader_Surfcae_1); + c2_hw_decode_obj_1.decode(video_path + "virtual.mp4"); } catch (Throwable throwable) { - XposedBridge.log(throwable.toString()); + XposedBridge.log("【VCAM】"+throwable.toString()); } } } public Surface create_virtual_surface() { if (need_recreate) { - XposedBridge.log("重建垃圾场"); + XposedBridge.log("【VCAM】重建垃圾场"); if (c2_virtual_surfaceTexture != null) { c2_virtual_surfaceTexture.release(); c2_virtual_surfaceTexture = null; @@ -637,17 +699,17 @@ protected void beforeHookedMethod(MethodHookParam param) throws Throwable { c2_reader_Surfcae = null; c2_preview_Surfcae = null; is_first_hook_build = true; - XposedBridge.log("打开相机C2"); + XposedBridge.log("【VCAM】打开相机C2"); XposedHelpers.findAndHookMethod(param.args[0].getClass(), "createCaptureSession", List.class, CameraCaptureSession.StateCallback.class, Handler.class, new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam paramd) throws Throwable { create_virtual_surface(); - XposedBridge.log("创捷捕获,原始:" + paramd.args[0].toString() + "虚拟:" + HookMain.c2_virtual_surface.toString()); - paramd.args[0] = Arrays.asList(HookMain.c2_virtual_surface); + XposedBridge.log("【VCAM】创捷捕获,原始:" + paramd.args[0].toString() + "虚拟:" + c2_virtual_surface.toString()); + paramd.args[0] = Arrays.asList(c2_virtual_surface); XposedHelpers.findAndHookMethod(paramd.args[1].getClass(), "onConfigureFailed", CameraCaptureSession.class, new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { - XposedBridge.log("onConfigureFailed :" + param.args[0].toString()); + XposedBridge.log("【VCAM】onConfigureFailed :" + param.args[0].toString()); } }); @@ -655,7 +717,7 @@ protected void beforeHookedMethod(MethodHookParam param) throws Throwable { XposedHelpers.findAndHookMethod(paramd.args[1].getClass(), "onConfigured", CameraCaptureSession.class, new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { - XposedBridge.log("onConfigured :" + param.args[0].toString()); + XposedBridge.log("【VCAM】onConfigured :" + param.args[0].toString()); } }); @@ -716,7 +778,7 @@ protected void afterHookedMethod(MethodHookParam paramd) throws Throwable { XposedHelpers.findAndHookMethod(hooked_class, "onError", CameraDevice.class, int.class, new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { - XposedBridge.log("相机错误onerror:" + (int) param.args[1]); + XposedBridge.log("【VCAM】相机错误onerror:" + (int) param.args[1]); } }); @@ -725,7 +787,7 @@ protected void beforeHookedMethod(MethodHookParam param) throws Throwable { XposedHelpers.findAndHookMethod(hooked_class, "onDisconnected", CameraDevice.class, new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { - XposedBridge.log("相机断开onDisconnected :"); + XposedBridge.log("【VCAM】相机断开onDisconnected :"); } }); @@ -735,9 +797,9 @@ protected void beforeHookedMethod(MethodHookParam param) throws Throwable { public void process_a_shot_jpeg(XC_MethodHook.MethodHookParam param, int index) { try { - XposedBridge.log("第二个jpeg:" + param.args[index].toString()); + XposedBridge.log("【VCAM】第二个jpeg:" + param.args[index].toString()); } catch (Exception eee) { - XposedBridge.log(eee.toString()); + XposedBridge.log("【VCAM】"+eee.toString()); } Class callback = param.args[index].getClass(); @@ -749,17 +811,21 @@ protected void beforeHookedMethod(MethodHookParam paramd) throws Throwable { Camera loaclcam = (Camera) paramd.args[1]; onemwidth = loaclcam.getParameters().getPreviewSize().width; onemhight = loaclcam.getParameters().getPreviewSize().height; - XposedBridge.log("JPEG拍照回调初始化:宽:" + onemwidth + "高:" + onemhight + "对应的类:" + loaclcam.toString()); + XposedBridge.log("【VCAM】JPEG拍照回调初始化:宽:" + onemwidth + "高:" + onemhight + "对应的类:" + loaclcam.toString()); if (toast_content != null) { Toast.makeText(toast_content, "发现拍照\n宽:" + onemwidth + "\n高:" + onemhight + "\n格式:JPEG", Toast.LENGTH_LONG).show(); } - Bitmap pict = getBMP(Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera1/1000.bmp"); + File control_file = new File(video_path + "disable.jpg"); + if (control_file.exists()) { + return; + } + Bitmap pict = getBMP(video_path + "1000.bmp"); ByteArrayOutputStream temp_array = new ByteArrayOutputStream(); pict.compress(Bitmap.CompressFormat.JPEG, 100, temp_array); byte[] jpeg_data = temp_array.toByteArray(); paramd.args[0] = jpeg_data; } catch (Exception ee) { - XposedBridge.log(ee.toString()); + XposedBridge.log("【VCAM】"+ee.toString()); } } }); @@ -767,9 +833,9 @@ protected void beforeHookedMethod(MethodHookParam paramd) throws Throwable { public void process_a_shot_YUV(XC_MethodHook.MethodHookParam param) { try { - XposedBridge.log("发现拍照YUV:" + param.args[1].toString()); + XposedBridge.log("【VCAM】发现拍照YUV:" + param.args[1].toString()); } catch (Exception eee) { - XposedBridge.log(eee.toString()); + XposedBridge.log("【VCAM】"+eee.toString()); } Class callback = param.args[1].getClass(); XposedHelpers.findAndHookMethod(callback, "onPictureTaken", byte[].class, android.hardware.Camera.class, new XC_MethodHook() { @@ -779,59 +845,73 @@ protected void beforeHookedMethod(MethodHookParam paramd) throws Throwable { Camera loaclcam = (Camera) paramd.args[1]; onemwidth = loaclcam.getParameters().getPreviewSize().width; onemhight = loaclcam.getParameters().getPreviewSize().height; - XposedBridge.log("YUV拍照回调初始化:宽:" + onemwidth + "高:" + onemhight + "对应的类:" + loaclcam.toString()); + XposedBridge.log("【VCAM】YUV拍照回调初始化:宽:" + onemwidth + "高:" + onemhight + "对应的类:" + loaclcam.toString()); if (toast_content != null) { Toast.makeText(toast_content, "发现拍照\n宽:" + onemwidth + "\n高:" + onemhight + "\n格式:YUV_420_888" , Toast.LENGTH_LONG).show(); } - input = getYUVByBitmap(getBMP(Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera1/1000.bmp")); + File control_file = new File(video_path + "disable.jpg"); + if (control_file.exists()) { + return; + } + input = getYUVByBitmap(getBMP(video_path + "1000.bmp")); paramd.args[0] = input; } catch (Exception ee) { - XposedBridge.log(ee.toString()); + XposedBridge.log("【VCAM】"+ee.toString()); } } }); } public void process_callback(XC_MethodHook.MethodHookParam param) { - Class nmb = param.args[0].getClass(); - XposedHelpers.findAndHookMethod(nmb, "onPreviewFrame", byte[].class, android.hardware.Camera.class, new XC_MethodHook() { + Class preview_cb_class = param.args[0].getClass(); + int need_stop = 0; + File control_file = new File(video_path + "disable.jpg"); + if (control_file.exists()) { + need_stop = 1; + } + File file = new File(video_path + "virtual.mp4"); + if (!file.exists()) { + if (toast_content != null) { + Toast.makeText(toast_content, "不存在替换视频", Toast.LENGTH_SHORT).show(); + need_stop = 1; + } + } + int finalNeed_stop = need_stop; + XposedHelpers.findAndHookMethod(preview_cb_class, "onPreviewFrame", byte[].class, android.hardware.Camera.class, new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam paramd) throws Throwable { Camera localcam = (android.hardware.Camera) paramd.args[1]; if (localcam.equals(data_camera)) { while (data_buffer == null) { } - System.arraycopy(HookMain.data_buffer, 0, paramd.args[0], 0, Math.min(HookMain.data_buffer.length, ((byte[]) paramd.args[0]).length)); + System.arraycopy(data_buffer, 0, paramd.args[0], 0, Math.min(data_buffer.length, ((byte[]) paramd.args[0]).length)); } else { - camera_callback_calss = nmb; - HookMain.data_camera = (android.hardware.Camera) paramd.args[1]; + camera_callback_calss = preview_cb_class; + data_camera = (android.hardware.Camera) paramd.args[1]; mwidth = data_camera.getParameters().getPreviewSize().width; mhight = data_camera.getParameters().getPreviewSize().height; int frame_Rate = data_camera.getParameters().getPreviewFrameRate(); - XposedBridge.log("帧预览回调初始化:宽:" + mwidth + " 高:" + mhight + " 帧率:" + frame_Rate); + XposedBridge.log("【VCAM】帧预览回调初始化:宽:" + mwidth + " 高:" + mhight + " 帧率:" + frame_Rate); if (toast_content != null) { - Toast.makeText(toast_content, "发现预览\n宽:" + mwidth + "\n高:" + mhight + "\n" + "需要完全匹配分辨率", Toast.LENGTH_LONG).show(); + Toast.makeText(toast_content, "发现预览\n宽:" + mwidth + "\n高:" + mhight + "\n" + "需要视频分辨率与其完全相同", Toast.LENGTH_LONG).show(); } - File file = new File(Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera1/virtual.mp4"); - if (!file.exists()) { - if (toast_content != null) { - Toast.makeText(toast_content, "不存在替换视频", Toast.LENGTH_SHORT).show(); - return; - } + if (finalNeed_stop == 1){ + return; } if (hw_decode_obj != null) { hw_decode_obj.stopDecode(); } hw_decode_obj = new VideoToFrames(); hw_decode_obj.setSaveFrames("", OutputImageFormat.NV21); - hw_decode_obj.decode(Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera1/virtual.mp4"); + hw_decode_obj.decode(video_path + "virtual.mp4"); while (data_buffer == null) { } - System.arraycopy(HookMain.data_buffer, 0, paramd.args[0], 0, Math.min(HookMain.data_buffer.length, ((byte[]) paramd.args[0]).length)); + System.arraycopy(data_buffer, 0, paramd.args[0], 0, Math.min(data_buffer.length, ((byte[]) paramd.args[0]).length)); } } }); + } //以下代码来源:https://blog.csdn.net/jacke121/article/details/73888732 @@ -953,7 +1033,7 @@ public void run() { @SuppressLint("WrongConstant") public void videoDecode(String videoFilePath) throws IOException { - XposedBridge.log("开始解码"); + XposedBridge.log("【VCAM】开始解码"); MediaExtractor extractor = null; MediaCodec decoder = null; try { @@ -1063,7 +1143,7 @@ private void decodeFramesToImage(MediaCodec decoder, MediaExtractor extractor, M try { mQueue.put(arr); } catch (InterruptedException e) { - XposedBridge.log(e.toString()); + XposedBridge.log("【VCAM】"+e.toString()); } } if (outputImageFormat != null) { @@ -1076,8 +1156,8 @@ private void decodeFramesToImage(MediaCodec decoder, MediaExtractor extractor, M try { Thread.sleep(sleepTime); } catch (InterruptedException e) { - XposedBridge.log(e.toString()); - XposedBridge.log("线程延迟出错"); + XposedBridge.log("【VCAM】"+e.toString()); + XposedBridge.log("【VCAM】线程延迟出错"); } } decoder.releaseOutputBuffer(outputBufferId, true); diff --git a/app/src/main/java/com/example/vcam/MainActivity.java b/app/src/main/java/com/example/vcam/MainActivity.java index b97dbf9..7a3f088 100644 --- a/app/src/main/java/com/example/vcam/MainActivity.java +++ b/app/src/main/java/com/example/vcam/MainActivity.java @@ -1,24 +1,15 @@ package com.example.vcam; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; -import android.content.ContentProvider; -import android.content.ContentValues; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.UriMatcher; -import android.database.Cursor; +import android.app.Activity; +import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.view.View; import android.widget.Button; -import android.widget.EditText; -import android.widget.Toast; -public class MainActivity extends AppCompatActivity { +public class MainActivity extends Activity { @SuppressLint("WorldReadableFiles") public void onCreate(Bundle savedInstanceState) { @@ -26,5 +17,20 @@ public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); + Button repo_button = findViewById(R.id.button); + + repo_button.setOnClickListener(new View.OnClickListener() { + + @Override + + public void onClick(View v) { + + Uri uri = Uri.parse("https://github.com/w2016561536/android_virtual_cam"); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + startActivity(intent); + } + }); } } + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 9aa52c2..72e49a8 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,24 +1,32 @@ - + xmlns:android="http://schemas.android.com/apk/res/android"> - + + + +