diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 000000000..b73537336
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,35 @@
+---
+name: Bug report
+about: Create a report to help us improve
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Desktop (please complete the following information):**
+ - OS: [e.g. iOS]
+ - Browser [e.g. chrome, safari]
+ - Version [e.g. 22]
+
+**Smartphone (please complete the following information):**
+ - Device: [e.g. iPhone6]
+ - OS: [e.g. iOS8.1]
+ - Browser [e.g. stock browser, safari]
+ - Version [e.g. 22]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/bug_report_cn.md b/.github/ISSUE_TEMPLATE/bug_report_cn.md
new file mode 100644
index 000000000..74bfdf050
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report_cn.md
@@ -0,0 +1,29 @@
+---
+name: BUG反馈
+about: 中文BUG反馈
+
+---
+
+**反馈BUG之前,先issue里面搜看看有没有别人已经反馈过,重复的不予处理!!**
+
+## 问题描述
+
+(请尽量详细地描述你遇到的问题)
+
+## 复现步骤
+
+(请分步骤描述如何复现这个BUG,非毕现BUG请给出如何能大概率复现的步骤)
+
+## 环境
+
+机型:
+系统版本:
+ROM版本:(请区分内测版和开发版稳定版,除稳定版本外不予修复)
+Xposed 插件以及插件版本:
+VirtualXposed版本:
+
+## 补充
+
+(别的需要描述的内容)
+
+**写完之后,请自己再读一遍自己写的,如果你自己都读不懂,就不用说修复了**
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 000000000..2fb7441da
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,21 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+
+Add any other context or screenshots about the feature request here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request_cn.md b/.github/ISSUE_TEMPLATE/feature_request_cn.md
new file mode 100644
index 000000000..27cd8d15a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request_cn.md
@@ -0,0 +1,19 @@
+---
+name: 意见和建议
+about: Feature中文版
+
+---
+
+**BUG反馈请不要用这个模版,否则直接关闭!!**
+
+## 场景描述
+
+(请详细和精确地表述你的使用场景)
+
+## 希望的解决方案
+
+(你希望如何解决这个问题?)
+
+## 其他信息
+
+(其他你认为有用的信息)
\ No newline at end of file
diff --git a/.github/issue-close-app.yml b/.github/issue-close-app.yml
new file mode 100644
index 000000000..a504b93b9
--- /dev/null
+++ b/.github/issue-close-app.yml
@@ -0,0 +1,19 @@
+# Comment that will be sent if an issue is judged to be closed
+comment: "This issue is closed because it does not meet our issue template/为方便解决问题,请使用 issue 模版提交问题。"
+issueConfigs:
+# There can be several configs for different kind of issues.
+- content:
+# Example 1: bug report
+ - "Expected behavior"
+ - "To Reproduce"
+ - "Describe the bug"
+- content:
+# Example 2: feature request
+ - "Describe the solution you'd like"
+- content:
+ - "问题描述"
+ - "复现步骤"
+ - "环境"
+- content:
+ - "场景描述"
+ - "希望的解决方案"
\ No newline at end of file
diff --git a/.github/stale.yml b/.github/stale.yml
new file mode 100644
index 000000000..1b547d34c
--- /dev/null
+++ b/.github/stale.yml
@@ -0,0 +1,17 @@
+# Number of days of inactivity before an issue becomes stale
+daysUntilStale: 20
+# Number of days of inactivity before a stale issue is closed
+daysUntilClose: 7
+# Issues with these labels will never be considered stale
+exemptLabels:
+ - bug
+ - security
+# Label to use when marking an issue as stale
+staleLabel: wontfix
+# Comment to post when marking an issue as stale. Set to `false` to disable
+markComment: >
+ This issue has been automatically marked as stale because it has not had
+ recent activity. It will be closed if no further activity occurs. Thank you
+ for your contributions.
+# Comment to post when closing a stale issue. Set to `false` to disable
+closeComment: false
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 000000000..d1c4983af
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "VirtualApp/launcher"]
+ path = VirtualApp/launcher
+ url = https://github.com/android-hacker/Launcher3.git
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 000000000..a546f4d35
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,21 @@
+language: android
+android:
+ components:
+ - tools
+ - build-tools-28.0.3
+ - android-27
+ - android-28
+ - extra-android-m2repository
+ - extra-android-support
+
+install:
+ - echo y | sdkmanager 'ndk-bundle'
+ - echo y | sdkmanager 'cmake;3.6.4111459'
+ - echo y | sdkmanager 'lldb;3.0'
+
+before_install:
+ - chmod +x ./VirtualApp/gradlew
+
+script:
+ - cd VirtualApp
+ - ./gradlew assembleRelease
diff --git a/CHINESE.md b/CHINESE.md
index f285cbcb5..8a214fe75 100644
--- a/CHINESE.md
+++ b/CHINESE.md
@@ -1,80 +1,120 @@
-[](https://github.com/asLody/VirtualApp)
+[](https://travis-ci.org/android-hacker/VirtualXposed)
简介
----
-**VirtualApp**是一个**App虚拟化引擎**(简称`VA`)。
+-----
+**VirtualXposed** 是基于[VirtualApp](https://github.com/asLody/VirtualApp) 和 [epic](https://github.com/tiann/epic) 在**非ROOT**环境下运行Xposed模块的实现(支持5.0~9.0)。
-**VirtualApp已兼容Android 0(8.0 Preview)。**
+与 Xposed 相比,目前 VirtualXposed 有两个限制:
-VirtualApp在你的App内创建一个`虚拟空间`,你可以在虚拟空间内任意的`安装`、`启动`和`卸载`APK,这一切都与外部隔离,如同一个`沙盒`。
+1. 不支持修改系统(可以修改普通APP中对系统API的调用),因此重力工具箱,应用控制器等无法使用。
+2. 暂不支持资源HOOK,因此资源钩子不会起任何作用;使用资源HOOK的模块,相应的功能不会生效。
-运行在`VA`中的APK无需在外部安装,即VA支持**免安装运行APK**。
-VA目前被广泛应用于双开/多开,但它决不仅限于此,Android本身就是一个极其开放的平台,免安装运行APK这一Feature打开了太多太多的可能--------这都取决于你的想象力。
+警告
+-------
+本项目使用的 VirtualApp 不允许用于商业用途,如果有这个需求,请联系 Lody (imlody@foxmail.com)。
-申明
----
-**您没有权利将VirtualApp的app模块作为您自己的app上架到软件市场,一经发现,后果你懂的。**
+使用
+----------
-**您需要授权才可以使用lib的代码,VirtualApp已申请国家专利, 并获得软件著作权保护, 当你的行为对项目或是项目作者构成利益冲突时,我们将追究法律责任。若需使用本项目,请与作者联系。**
+## 准备
-谁在使用本项目
--------------
-* 地铁跑酷
-* 骑士助手
-* X-Phone
-* Dual app
-* 机友精灵
-* 隐秘(PrivateMe)
+首先在 [发布页面](https://github.com/android-hacker/VirtualXposed/releases) 下载最新的VAExposed安装包安装到手机。
-已支持的加固
-----------
-* 360加固
-* 腾讯加固
-* 梆梆加固
-* 爱加密
-* 百度加固
-* 娜迦加固
-* (非VMP的加固都可以通过VA来脱壳,但目前本技术尚不公开)
+## 安装模块
+打开 VirtualXposed,在里面安装要使用的APP,以及相应的Xposed模块即可。
-使用说明
-----------
+注意:**所有的工作(安装Xposed模块,安装APP)必须在 VirtualXposed中**进行,否则Xposed模块不会有任何作用!比如,将微信直接安装在系统上(而非VirtualXposed中),防撤回安装在VirtualXposed中;或者把微信安装在VirtualXposed上,防撤回插件直接安装在系统上;或者两者都直接安装在系统上,**均不会起任何作用**。
+
+在VirtualXposed中安装App有两种方式:
+
+1. 直接复制已经在系统中安装好的APP,比如如果你系统中装了微信,那么可以直接复制一份。
+2. 通过外置存储直接安装APK文件;点主界面的底部按钮-添加应用,然后选择后面两个TAB即可。
+
+在VirtualXposed中安装Xposed模块,可以跟安装正常的APK一样,以上两种安装App的方式也适用于安装Xposed模块。不过,你也可以通过VirtualXposed中内置的XposedInstaller来安装和管理模块,跟通常的XposedInstaller使用方式一样;去下载页面,下载安装即可。
-**前往你的Application并添加如下代码:**
-```java
- @Override
- protected void attachBaseContext(Context base) {
- super.attachBaseContext(base);
- try {
- VirtualCore.getCore().startup(base);
- } catch (Throwable e) {
- e.printStackTrace();
- }
- }
-```
-**安装App:**
-```java
- VirtualCore.getCore().installApp({APK PATH}, flags);
-```
-**启动App:**
-```java
- VirtualCore.getCore().launchApp({PackageName});
-```
-**移除App:**
-```java
- VirtualCore.getCore().uninstallApp({PackageName});
-```
-**该App的基本信息:**
-```java
- VirtualCore.getCore().findApp({PackageName});
-```
-
-License
+## 亲测可用的模块
+
+- [XPrivacyLua][xpl]: Really simple to use privacy manager for Android 6.0 Marshmallow and later.
+- [XInsta][xinsta]: Instagram module(Feed downing, stories downloading, etc).
+- [Minminguard][minminguard]: Completely remove both the ads inside apps and the empty space caused by those ads.
+- [YouTube AdAway][yta]: Get rid of ads on the official YouTube App.
+- [微X模块][wx]: 微信模块,功能强大。
+- [畅玩微信][cwwx]: 微信模块新秀,功能丰富。
+- [微信巫师][wxws]: 微信模块,项目开源,代码优秀。
+- [MDWechat][mdwechat]: 微信美化模块,可以把微信整成MD风格。
+- [应用变量][yybl]: 可以用来进行机型修改,比如王者荣耀高帧率;QQ空间修改小尾巴等。
+- [音量增强器][ylzqq]: 网易云音乐模块,非常好用,低调。
+- [微信学英语][wxxyy]: 自动把微信消息翻译为英语,非常实用。
+- [情迁抢包][qqqb]: 微信QQ抢红包模块。
+- [微信跳一跳助手][ttzs]: 微信跳一跳游戏辅助模块。
+- [步数修改器][bsxg]: 运动步数修改模块。
+- [模拟位置][mnwz]: 虚拟定位模块,稳定好用。
+- [指纹支付][zwzf]: 对不支持指纹支付但系统本身有指纹的手机开启指纹支付的模块。
+- [QQ精简模块 2.0][qqjj]: QQ模块,不仅可以精简QQ,还能防撤回,防闪照。
+- [微信增强插件][wxzqcj]: 微信模块,VXP内最稳定的微信模块;如无特殊需求建议用这个。
+- [QX模块][qx]: QQ模块,防撤回抢红包斗图一应俱全。
+- [QQ斗图神器][qqdtsq]: 各种表情,斗图神器。
+- [微信斗图神器][wxdtsq]: 斗图神器,微信用的。
+- [大圣净化][dsjh]: 去广告神器,推荐使用。
+
+真正能用的模块远不止这么多,要用的话可以自己测试;如果你发现某些模块可以用但不在上面的列表中,欢迎给我发个PR。
+
+其他
-------
-GPL 3.0
-技术支持
+### GameGuardian
+
+VirtualXposed也支持GG修改器,如果你需要用GG,那么请使用GG专版(可以在发布页面下载,带 For_GameGuardian后缀)。
+
+[GG修改器使用视频教程](https://gameguardian.net/forum/gallery/image/437-no-root-via-virtualxposed-without-error-105-gameguardian/)
+
+### VirusTotal
+
+VirusTotal 还有一些其他的杀毒引擎检测到VirtualXposed有病毒,这一点我该不承认,而且我觉得这些愚蠢的杀毒引擎是在胡扯。请看[我的说明](https://github.com/android-hacker/VirtualXposed/issues/10).
+
+而且,VirtualXposed是开源的,你可以直接查看代码;我可以打包票,VirtualXposed本身没有做任何有害的事情(但是它确实有这个能力,所以请不要下载不明来源的Xposed插件)。
+
+如果你还是不放心,那么你可以使用 [0.8.7版本](https://github.com/android-hacker/VirtualXposed/releases/tag/0.8.7), 这个版本杀毒引擎的检测结果是安全的(简直就是扯淡)。
+
+
+支持和加入
------------
-Lody (imlody@foxmail.com)
-QQ/WeChat (382816028)
+
+目前VirtualXposed 还不完善,如果你对非ROOT下实现Xposed感兴趣;欢迎加入!你可以通过如下方式来支持:
+
+1. 直接贡献代码,提供Feature,修复BUG!
+2. 使用你拥有的手机,安装你常用的Xposed模块,反馈不可用情况;协助帮忙解决兼容性问题!
+3. 提出体验上,功能上的建议,帮助完善VirtualXposed!
+
+致谢
+------
+
+1. [VirtualApp](https://github.com/asLody/VirtualApp)
+2. [Xposed](https://github.com/rovo89/Xposed)
+
+[wx]: http://repo.xposed.info/module/com.fkzhang.wechatxposed
+[qx]: http://repo.xposed.info/module/com.fkzhang.qqxposed
+[wxws]: https://github.com/Gh0u1L5/WechatMagician/releases
+[yybl]: https://www.coolapk.com/apk/com.sollyu.xposed.hook.model
+[ylzqq]: https://github.com/bin456789/Unblock163MusicClient-Xposed/releases
+[wxxyy]: https://www.coolapk.com/apk/com.hiwechart.translate
+[qqqb]: http://repo.xposed.info/module/cn.qssq666.redpacket
+[ttzs]: http://repo.xposed.info/module/com.emily.mmjumphelper
+[mnwz]: https://www.coolapk.com/apk/com.rong.xposed.fakelocation
+[zwzf]: https://github.com/android-hacker/Xposed-Fingerprint-pay/releases
+[bsxg]: https://www.coolapk.com/apk/com.specher.sm
+[mdwechat]: https://github.com/Blankeer/MDWechat
+[wxzqcj]:https://github.com/firesunCN/WechatEnhancement
+[qqjj]: https://www.coolapk.com/apk/me.zpp0196.qqsimple
+[qqdtsq]: https://www.coolapk.com/apk/x.hook.qqemoji
+[wxdtsq]: https://www.coolapk.com/apk/x.hook.emojihook
+[dsjh]: https://wiki.ad-gone.com/archives/32
+[xpl]: https://github.com/android-hacker/VirtualXposed/wiki/Privacy-control(XPrivacyLua)
+[minminguard]: http://repo.xposed.info/module/tw.fatminmin.xposed.minminguard
+[yta]: http://repo.xposed.info/module/ma.wanam.youtubeadaway
+[xinsta]: http://repo.xposed.info/module/com.ihelp101.instagram
+[cwwx]: http://repo.xposed.info/module/com.example.wx_plug_in3
+
+
diff --git a/LICENSE.txt b/LICENSE.txt
deleted file mode 100644
index 94a9ed024..000000000
--- a/LICENSE.txt
+++ /dev/null
@@ -1,674 +0,0 @@
- GNU GENERAL PUBLIC LICENSE
- Version 3, 29 June 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc.
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
- Preamble
-
- The GNU General Public License is a free, copyleft license for
-software and other kinds of works.
-
- The licenses for most software and other practical works are designed
-to take away your freedom to share and change the works. By contrast,
-the GNU General Public License is intended to guarantee your freedom to
-share and change all versions of a program--to make sure it remains free
-software for all its users. We, the Free Software Foundation, use the
-GNU General Public License for most of our software; it applies also to
-any other work released this way by its authors. You can apply it to
-your programs, too.
-
- When we speak of free software, we are referring to freedom, not
-price. Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-them if you wish), that you receive source code or can get it if you
-want it, that you can change the software or use pieces of it in new
-free programs, and that you know you can do these things.
-
- To protect your rights, we need to prevent others from denying you
-these rights or asking you to surrender the rights. Therefore, you have
-certain responsibilities if you distribute copies of the software, or if
-you modify it: responsibilities to respect the freedom of others.
-
- For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must pass on to the recipients the same
-freedoms that you received. You must make sure that they, too, receive
-or can get the source code. And you must show them these terms so they
-know their rights.
-
- Developers that use the GNU GPL protect your rights with two steps:
-(1) assert copyright on the software, and (2) offer you this License
-giving you legal permission to copy, distribute and/or modify it.
-
- For the developers' and authors' protection, the GPL clearly explains
-that there is no warranty for this free software. For both users' and
-authors' sake, the GPL requires that modified versions be marked as
-changed, so that their problems will not be attributed erroneously to
-authors of previous versions.
-
- Some devices are designed to deny users access to install or run
-modified versions of the software inside them, although the manufacturer
-can do so. This is fundamentally incompatible with the aim of
-protecting users' freedom to change the software. The systematic
-pattern of such abuse occurs in the area of products for individuals to
-use, which is precisely where it is most unacceptable. Therefore, we
-have designed this version of the GPL to prohibit the practice for those
-products. If such problems arise substantially in other domains, we
-stand ready to extend this provision to those domains in future versions
-of the GPL, as needed to protect the freedom of users.
-
- Finally, every program is threatened constantly by software patents.
-States should not allow patents to restrict development and use of
-software on general-purpose computers, but in those that do, we wish to
-avoid the special danger that patents applied to a free program could
-make it effectively proprietary. To prevent this, the GPL assures that
-patents cannot be used to render the program non-free.
-
- The precise terms and conditions for copying, distribution and
-modification follow.
-
- TERMS AND CONDITIONS
-
- 0. Definitions.
-
- "This License" refers to version 3 of the GNU General Public License.
-
- "Copyright" also means copyright-like laws that apply to other kinds of
-works, such as semiconductor masks.
-
- "The Program" refers to any copyrightable work licensed under this
-License. Each licensee is addressed as "you". "Licensees" and
-"recipients" may be individuals or organizations.
-
- To "modify" a work means to copy from or adapt all or part of the work
-in a fashion requiring copyright permission, other than the making of an
-exact copy. The resulting work is called a "modified version" of the
-earlier work or a work "based on" the earlier work.
-
- A "covered work" means either the unmodified Program or a work based
-on the Program.
-
- To "propagate" a work means to do anything with it that, without
-permission, would make you directly or secondarily liable for
-infringement under applicable copyright law, except executing it on a
-computer or modifying a private copy. Propagation includes copying,
-distribution (with or without modification), making available to the
-public, and in some countries other activities as well.
-
- To "convey" a work means any kind of propagation that enables other
-parties to make or receive copies. Mere interaction with a user through
-a computer network, with no transfer of a copy, is not conveying.
-
- An interactive user interface displays "Appropriate Legal Notices"
-to the extent that it includes a convenient and prominently visible
-feature that (1) displays an appropriate copyright notice, and (2)
-tells the user that there is no warranty for the work (except to the
-extent that warranties are provided), that licensees may convey the
-work under this License, and how to view a copy of this License. If
-the interface presents a list of user commands or options, such as a
-menu, a prominent item in the list meets this criterion.
-
- 1. Source Code.
-
- The "source code" for a work means the preferred form of the work
-for making modifications to it. "Object code" means any non-source
-form of a work.
-
- A "Standard Interface" means an interface that either is an official
-standard defined by a recognized standards body, or, in the case of
-interfaces specified for a particular programming language, one that
-is widely used among developers working in that language.
-
- The "System Libraries" of an executable work include anything, other
-than the work as a whole, that (a) is included in the normal form of
-packaging a Major Component, but which is not part of that Major
-Component, and (b) serves only to enable use of the work with that
-Major Component, or to implement a Standard Interface for which an
-implementation is available to the public in source code form. A
-"Major Component", in this context, means a major essential component
-(kernel, window system, and so on) of the specific operating system
-(if any) on which the executable work runs, or a compiler used to
-produce the work, or an object code interpreter used to run it.
-
- The "Corresponding Source" for a work in object code form means all
-the source code needed to generate, install, and (for an executable
-work) run the object code and to modify the work, including scripts to
-control those activities. However, it does not include the work's
-System Libraries, or general-purpose tools or generally available free
-programs which are used unmodified in performing those activities but
-which are not part of the work. For example, Corresponding Source
-includes interface definition files associated with source files for
-the work, and the source code for shared libraries and dynamically
-linked subprograms that the work is specifically designed to require,
-such as by intimate data communication or control flow between those
-subprograms and other parts of the work.
-
- The Corresponding Source need not include anything that users
-can regenerate automatically from other parts of the Corresponding
-Source.
-
- The Corresponding Source for a work in source code form is that
-same work.
-
- 2. Basic Permissions.
-
- All rights granted under this License are granted for the term of
-copyright on the Program, and are irrevocable provided the stated
-conditions are met. This License explicitly affirms your unlimited
-permission to run the unmodified Program. The output from running a
-covered work is covered by this License only if the output, given its
-content, constitutes a covered work. This License acknowledges your
-rights of fair use or other equivalent, as provided by copyright law.
-
- You may make, run and propagate covered works that you do not
-convey, without conditions so long as your license otherwise remains
-in force. You may convey covered works to others for the sole purpose
-of having them make modifications exclusively for you, or provide you
-with facilities for running those works, provided that you comply with
-the terms of this License in conveying all material for which you do
-not control copyright. Those thus making or running the covered works
-for you must do so exclusively on your behalf, under your direction
-and control, on terms that prohibit them from making any copies of
-your copyrighted material outside their relationship with you.
-
- Conveying under any other circumstances is permitted solely under
-the conditions stated below. Sublicensing is not allowed; section 10
-makes it unnecessary.
-
- 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-
- No covered work shall be deemed part of an effective technological
-measure under any applicable law fulfilling obligations under article
-11 of the WIPO copyright treaty adopted on 20 December 1996, or
-similar laws prohibiting or restricting circumvention of such
-measures.
-
- When you convey a covered work, you waive any legal power to forbid
-circumvention of technological measures to the extent such circumvention
-is effected by exercising rights under this License with respect to
-the covered work, and you disclaim any intention to limit operation or
-modification of the work as a means of enforcing, against the work's
-users, your or third parties' legal rights to forbid circumvention of
-technological measures.
-
- 4. Conveying Verbatim Copies.
-
- You may convey verbatim copies of the Program's source code as you
-receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice;
-keep intact all notices stating that this License and any
-non-permissive terms added in accord with section 7 apply to the code;
-keep intact all notices of the absence of any warranty; and give all
-recipients a copy of this License along with the Program.
-
- You may charge any price or no price for each copy that you convey,
-and you may offer support or warranty protection for a fee.
-
- 5. Conveying Modified Source Versions.
-
- You may convey a work based on the Program, or the modifications to
-produce it from the Program, in the form of source code under the
-terms of section 4, provided that you also meet all of these conditions:
-
- a) The work must carry prominent notices stating that you modified
- it, and giving a relevant date.
-
- b) The work must carry prominent notices stating that it is
- released under this License and any conditions added under section
- 7. This requirement modifies the requirement in section 4 to
- "keep intact all notices".
-
- c) You must license the entire work, as a whole, under this
- License to anyone who comes into possession of a copy. This
- License will therefore apply, along with any applicable section 7
- additional terms, to the whole of the work, and all its parts,
- regardless of how they are packaged. This License gives no
- permission to license the work in any other way, but it does not
- invalidate such permission if you have separately received it.
-
- d) If the work has interactive user interfaces, each must display
- Appropriate Legal Notices; however, if the Program has interactive
- interfaces that do not display Appropriate Legal Notices, your
- work need not make them do so.
-
- A compilation of a covered work with other separate and independent
-works, which are not by their nature extensions of the covered work,
-and which are not combined with it such as to form a larger program,
-in or on a volume of a storage or distribution medium, is called an
-"aggregate" if the compilation and its resulting copyright are not
-used to limit the access or legal rights of the compilation's users
-beyond what the individual works permit. Inclusion of a covered work
-in an aggregate does not cause this License to apply to the other
-parts of the aggregate.
-
- 6. Conveying Non-Source Forms.
-
- You may convey a covered work in object code form under the terms
-of sections 4 and 5, provided that you also convey the
-machine-readable Corresponding Source under the terms of this License,
-in one of these ways:
-
- a) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by the
- Corresponding Source fixed on a durable physical medium
- customarily used for software interchange.
-
- b) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by a
- written offer, valid for at least three years and valid for as
- long as you offer spare parts or customer support for that product
- model, to give anyone who possesses the object code either (1) a
- copy of the Corresponding Source for all the software in the
- product that is covered by this License, on a durable physical
- medium customarily used for software interchange, for a price no
- more than your reasonable cost of physically performing this
- conveying of source, or (2) access to copy the
- Corresponding Source from a network server at no charge.
-
- c) Convey individual copies of the object code with a copy of the
- written offer to provide the Corresponding Source. This
- alternative is allowed only occasionally and noncommercially, and
- only if you received the object code with such an offer, in accord
- with subsection 6b.
-
- d) Convey the object code by offering access from a designated
- place (gratis or for a charge), and offer equivalent access to the
- Corresponding Source in the same way through the same place at no
- further charge. You need not require recipients to copy the
- Corresponding Source along with the object code. If the place to
- copy the object code is a network server, the Corresponding Source
- may be on a different server (operated by you or a third party)
- that supports equivalent copying facilities, provided you maintain
- clear directions next to the object code saying where to find the
- Corresponding Source. Regardless of what server hosts the
- Corresponding Source, you remain obligated to ensure that it is
- available for as long as needed to satisfy these requirements.
-
- e) Convey the object code using peer-to-peer transmission, provided
- you inform other peers where the object code and Corresponding
- Source of the work are being offered to the general public at no
- charge under subsection 6d.
-
- A separable portion of the object code, whose source code is excluded
-from the Corresponding Source as a System Library, need not be
-included in conveying the object code work.
-
- A "User Product" is either (1) a "consumer product", which means any
-tangible personal property which is normally used for personal, family,
-or household purposes, or (2) anything designed or sold for incorporation
-into a dwelling. In determining whether a product is a consumer product,
-doubtful cases shall be resolved in favor of coverage. For a particular
-product received by a particular user, "normally used" refers to a
-typical or common use of that class of product, regardless of the status
-of the particular user or of the way in which the particular user
-actually uses, or expects or is expected to use, the product. A product
-is a consumer product regardless of whether the product has substantial
-commercial, industrial or non-consumer uses, unless such uses represent
-the only significant mode of use of the product.
-
- "Installation Information" for a User Product means any methods,
-procedures, authorization keys, or other information required to install
-and execute modified versions of a covered work in that User Product from
-a modified version of its Corresponding Source. The information must
-suffice to ensure that the continued functioning of the modified object
-code is in no case prevented or interfered with solely because
-modification has been made.
-
- If you convey an object code work under this section in, or with, or
-specifically for use in, a User Product, and the conveying occurs as
-part of a transaction in which the right of possession and use of the
-User Product is transferred to the recipient in perpetuity or for a
-fixed term (regardless of how the transaction is characterized), the
-Corresponding Source conveyed under this section must be accompanied
-by the Installation Information. But this requirement does not apply
-if neither you nor any third party retains the ability to install
-modified object code on the User Product (for example, the work has
-been installed in ROM).
-
- The requirement to provide Installation Information does not include a
-requirement to continue to provide support service, warranty, or updates
-for a work that has been modified or installed by the recipient, or for
-the User Product in which it has been modified or installed. Access to a
-network may be denied when the modification itself materially and
-adversely affects the operation of the network or violates the rules and
-protocols for communication across the network.
-
- Corresponding Source conveyed, and Installation Information provided,
-in accord with this section must be in a format that is publicly
-documented (and with an implementation available to the public in
-source code form), and must require no special password or key for
-unpacking, reading or copying.
-
- 7. Additional Terms.
-
- "Additional permissions" are terms that supplement the terms of this
-License by making exceptions from one or more of its conditions.
-Additional permissions that are applicable to the entire Program shall
-be treated as though they were included in this License, to the extent
-that they are valid under applicable law. If additional permissions
-apply only to part of the Program, that part may be used separately
-under those permissions, but the entire Program remains governed by
-this License without regard to the additional permissions.
-
- When you convey a copy of a covered work, you may at your option
-remove any additional permissions from that copy, or from any part of
-it. (Additional permissions may be written to require their own
-removal in certain cases when you modify the work.) You may place
-additional permissions on material, added by you to a covered work,
-for which you have or can give appropriate copyright permission.
-
- Notwithstanding any other provision of this License, for material you
-add to a covered work, you may (if authorized by the copyright holders of
-that material) supplement the terms of this License with terms:
-
- a) Disclaiming warranty or limiting liability differently from the
- terms of sections 15 and 16 of this License; or
-
- b) Requiring preservation of specified reasonable legal notices or
- author attributions in that material or in the Appropriate Legal
- Notices displayed by works containing it; or
-
- c) Prohibiting misrepresentation of the origin of that material, or
- requiring that modified versions of such material be marked in
- reasonable ways as different from the original version; or
-
- d) Limiting the use for publicity purposes of names of licensors or
- authors of the material; or
-
- e) Declining to grant rights under trademark law for use of some
- trade names, trademarks, or service marks; or
-
- f) Requiring indemnification of licensors and authors of that
- material by anyone who conveys the material (or modified versions of
- it) with contractual assumptions of liability to the recipient, for
- any liability that these contractual assumptions directly impose on
- those licensors and authors.
-
- All other non-permissive additional terms are considered "further
-restrictions" within the meaning of section 10. If the Program as you
-received it, or any part of it, contains a notice stating that it is
-governed by this License along with a term that is a further
-restriction, you may remove that term. If a license document contains
-a further restriction but permits relicensing or conveying under this
-License, you may add to a covered work material governed by the terms
-of that license document, provided that the further restriction does
-not survive such relicensing or conveying.
-
- If you add terms to a covered work in accord with this section, you
-must place, in the relevant source files, a statement of the
-additional terms that apply to those files, or a notice indicating
-where to find the applicable terms.
-
- Additional terms, permissive or non-permissive, may be stated in the
-form of a separately written license, or stated as exceptions;
-the above requirements apply either way.
-
- 8. Termination.
-
- You may not propagate or modify a covered work except as expressly
-provided under this License. Any attempt otherwise to propagate or
-modify it is void, and will automatically terminate your rights under
-this License (including any patent licenses granted under the third
-paragraph of section 11).
-
- However, if you cease all violation of this License, then your
-license from a particular copyright holder is reinstated (a)
-provisionally, unless and until the copyright holder explicitly and
-finally terminates your license, and (b) permanently, if the copyright
-holder fails to notify you of the violation by some reasonable means
-prior to 60 days after the cessation.
-
- Moreover, your license from a particular copyright holder is
-reinstated permanently if the copyright holder notifies you of the
-violation by some reasonable means, this is the first time you have
-received notice of violation of this License (for any work) from that
-copyright holder, and you cure the violation prior to 30 days after
-your receipt of the notice.
-
- Termination of your rights under this section does not terminate the
-licenses of parties who have received copies or rights from you under
-this License. If your rights have been terminated and not permanently
-reinstated, you do not qualify to receive new licenses for the same
-material under section 10.
-
- 9. Acceptance Not Required for Having Copies.
-
- You are not required to accept this License in order to receive or
-run a copy of the Program. Ancillary propagation of a covered work
-occurring solely as a consequence of using peer-to-peer transmission
-to receive a copy likewise does not require acceptance. However,
-nothing other than this License grants you permission to propagate or
-modify any covered work. These actions infringe copyright if you do
-not accept this License. Therefore, by modifying or propagating a
-covered work, you indicate your acceptance of this License to do so.
-
- 10. Automatic Licensing of Downstream Recipients.
-
- Each time you convey a covered work, the recipient automatically
-receives a license from the original licensors, to run, modify and
-propagate that work, subject to this License. You are not responsible
-for enforcing compliance by third parties with this License.
-
- An "entity transaction" is a transaction transferring control of an
-organization, or substantially all assets of one, or subdividing an
-organization, or merging organizations. If propagation of a covered
-work results from an entity transaction, each party to that
-transaction who receives a copy of the work also receives whatever
-licenses to the work the party's predecessor in interest had or could
-give under the previous paragraph, plus a right to possession of the
-Corresponding Source of the work from the predecessor in interest, if
-the predecessor has it or can get it with reasonable efforts.
-
- You may not impose any further restrictions on the exercise of the
-rights granted or affirmed under this License. For example, you may
-not impose a license fee, royalty, or other charge for exercise of
-rights granted under this License, and you may not initiate litigation
-(including a cross-claim or counterclaim in a lawsuit) alleging that
-any patent claim is infringed by making, using, selling, offering for
-sale, or importing the Program or any portion of it.
-
- 11. Patents.
-
- A "contributor" is a copyright holder who authorizes use under this
-License of the Program or a work on which the Program is based. The
-work thus licensed is called the contributor's "contributor version".
-
- A contributor's "essential patent claims" are all patent claims
-owned or controlled by the contributor, whether already acquired or
-hereafter acquired, that would be infringed by some manner, permitted
-by this License, of making, using, or selling its contributor version,
-but do not include claims that would be infringed only as a
-consequence of further modification of the contributor version. For
-purposes of this definition, "control" includes the right to grant
-patent sublicenses in a manner consistent with the requirements of
-this License.
-
- Each contributor grants you a non-exclusive, worldwide, royalty-free
-patent license under the contributor's essential patent claims, to
-make, use, sell, offer for sale, import and otherwise run, modify and
-propagate the contents of its contributor version.
-
- In the following three paragraphs, a "patent license" is any express
-agreement or commitment, however denominated, not to enforce a patent
-(such as an express permission to practice a patent or covenant not to
-sue for patent infringement). To "grant" such a patent license to a
-party means to make such an agreement or commitment not to enforce a
-patent against the party.
-
- If you convey a covered work, knowingly relying on a patent license,
-and the Corresponding Source of the work is not available for anyone
-to copy, free of charge and under the terms of this License, through a
-publicly available network server or other readily accessible means,
-then you must either (1) cause the Corresponding Source to be so
-available, or (2) arrange to deprive yourself of the benefit of the
-patent license for this particular work, or (3) arrange, in a manner
-consistent with the requirements of this License, to extend the patent
-license to downstream recipients. "Knowingly relying" means you have
-actual knowledge that, but for the patent license, your conveying the
-covered work in a country, or your recipient's use of the covered work
-in a country, would infringe one or more identifiable patents in that
-country that you have reason to believe are valid.
-
- If, pursuant to or in connection with a single transaction or
-arrangement, you convey, or propagate by procuring conveyance of, a
-covered work, and grant a patent license to some of the parties
-receiving the covered work authorizing them to use, propagate, modify
-or convey a specific copy of the covered work, then the patent license
-you grant is automatically extended to all recipients of the covered
-work and works based on it.
-
- A patent license is "discriminatory" if it does not include within
-the scope of its coverage, prohibits the exercise of, or is
-conditioned on the non-exercise of one or more of the rights that are
-specifically granted under this License. You may not convey a covered
-work if you are a party to an arrangement with a third party that is
-in the business of distributing software, under which you make payment
-to the third party based on the extent of your activity of conveying
-the work, and under which the third party grants, to any of the
-parties who would receive the covered work from you, a discriminatory
-patent license (a) in connection with copies of the covered work
-conveyed by you (or copies made from those copies), or (b) primarily
-for and in connection with specific products or compilations that
-contain the covered work, unless you entered into that arrangement,
-or that patent license was granted, prior to 28 March 2007.
-
- Nothing in this License shall be construed as excluding or limiting
-any implied license or other defenses to infringement that may
-otherwise be available to you under applicable patent law.
-
- 12. No Surrender of Others' Freedom.
-
- If conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License. If you cannot convey a
-covered work so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you may
-not convey it at all. For example, if you agree to terms that obligate you
-to collect a royalty for further conveying from those to whom you convey
-the Program, the only way you could satisfy both those terms and this
-License would be to refrain entirely from conveying the Program.
-
- 13. Use with the GNU Affero General Public License.
-
- Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
-under version 3 of the GNU Affero General Public License into a single
-combined work, and to convey the resulting work. The terms of this
-License will continue to apply to the part which is the covered work,
-but the special requirements of the GNU Affero General Public License,
-section 13, concerning interaction through a network will apply to the
-combination as such.
-
- 14. Revised Versions of this License.
-
- The Free Software Foundation may publish revised and/or new versions of
-the GNU General Public License from time to time. Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
- Each version is given a distinguishing version number. If the
-Program specifies that a certain numbered version of the GNU General
-Public License "or any later version" applies to it, you have the
-option of following the terms and conditions either of that numbered
-version or of any later version published by the Free Software
-Foundation. If the Program does not specify a version number of the
-GNU General Public License, you may choose any version ever published
-by the Free Software Foundation.
-
- If the Program specifies that a proxy can decide which future
-versions of the GNU General Public License can be used, that proxy's
-public statement of acceptance of a version permanently authorizes you
-to choose that version for the Program.
-
- Later license versions may give you additional or different
-permissions. However, no additional obligations are imposed on any
-author or copyright holder as a result of your choosing to follow a
-later version.
-
- 15. Disclaimer of Warranty.
-
- THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
-HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
-OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
-IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
-ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
- 16. Limitation of Liability.
-
- IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
-THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
-USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
-DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
-EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGES.
-
- 17. Interpretation of Sections 15 and 16.
-
- If the disclaimer of warranty and limitation of liability provided
-above cannot be given local legal effect according to their terms,
-reviewing courts shall apply local law that most closely approximates
-an absolute waiver of all civil liability in connection with the
-Program, unless a warranty or assumption of liability accompanies a
-copy of the Program in return for a fee.
-
- END OF TERMS AND CONDITIONS
-
- How to Apply These Terms to Your New Programs
-
- If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
- To do so, attach the following notices to the program. It is safest
-to attach them to the start of each source file to most effectively
-state the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
-
- Copyright (C)
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see .
-
-Also add information on how to contact you by electronic and paper mail.
-
- If the program does terminal interaction, make it output a short
-notice like this when it starts in an interactive mode:
-
- Copyright (C)
- This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
- This is free software, and you are welcome to redistribute it
- under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License. Of course, your program's commands
-might be different; for a GUI interface, you would use an "about box".
-
- You should also get your employer (if you work as a programmer) or school,
-if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU GPL, see
-.
-
- The GNU General Public License does not permit incorporating your program
-into proprietary programs. If your program is a subroutine library, you
-may consider it more useful to permit linking proprietary applications with
-the library. If this is what you want to do, use the GNU Lesser General
-Public License instead of this License. But first, please read
-.
diff --git a/README.md b/README.md
index 8ee3bf35d..4ec36cdcb 100644
--- a/README.md
+++ b/README.md
@@ -1,80 +1,137 @@
-[](https://github.com/asLody/VirtualApp)
+[](https://travis-ci.org/android-hacker/VirtualXposed)
-[中国人猛戳这里](CHINESE.md "中文")
+[中文文档](CHINESE.md "中文")
-About
------
-**VirtualApp** is an open platform for Android that allows you to create a `Virtual Space`,
-you can install and run apk inside. Beyond that, VirtualApp is also a `Plugin Framework`,
-the plugins running on VirtualApp does not require any constraints.
-VirtualApp does **not** require root, it is running on the `local process`.
+Introduction
+------------
+**VirtualXposed** is a simple APP based on [VirtualApp](https://github.com/asLody/VirtualApp) and [epic](https://github.com/tiann/epic) that allows you to use an Xposed Module without needing to root, unlock the bootloader, or flash a custom system image. (Supports Android 5.0~9.0)
+
+The only two restriction of VirtualXposed are:
+
+1. Unable to modify system, so any Module which modifies system won't be able to work properly.
+2. Currently resource hooks are not supported. (Theming modules use Resource Hooks).
+
+Warning
+-----------
-NOTICE
+Usage for Commercial Purposes are not allowed!!! Please refer to VirtualApp's [declaration](https://github.com/asLody/VirtualApp).
+
+Usage
-------
-**This project has been authorized by the business.**
-**You are not allowed to modify the app module and put to the software market, if you do that, The consequences you know :)**
+### Preparation
-**VirtualApp is not free, If you need to use the lib code, please send email to me :)**
+Download the latest APK from the [release page](https://github.com/android-hacker/VirtualXposed/releases), and install it on your Android device.
-Background
-----------
+### Install APP and Xposed Module
-VirtualApp was born in early 2015, Originally, it is just a simple plugin framework,
-But as time goes on,
-the compatibility of it is getting better and better.
-in the end, it evolved into a `Virtual Container`.
+Open VirtualXposed, Click on the **Drawer Button** at the bottom of home page(Or long click the screen), add your desired APP and Xposed Module to VirtualXposed's virtual environment.
+Note: **All operations(installation of Xposed Module, APP)must be done in VirtualXposed**, otherwise the Xposed Module installed won't take effect. For example, if you install the YouTube app on your system (Your phone's original system, not in VirtualXposed), and then install YouTube AdAway (A YouTube Xposed Module) in VirtualXposed; or you install YouTube in VirtualXposed, and install YouTube AdAway on original system; or both of them are installed on original system, **neither of these three cases will work!**
-Get started
------------
-If you use latest android studio (version 2.0 or above), please disable `Instant Run`.
-Open `Setting | Build,Exception,Deployment`, and disable `Enable Instant Run to hot swap...`
-
-**Goto your Application and insert the following code:**
-```java
- @Override
- protected void attachBaseContext(Context base) {
- super.attachBaseContext(base);
- try {
- VirtualCore.get().startup(base);
- } catch (Throwable e) {
- e.printStackTrace();
- }
- }
-```
-
-**Install a virtual App:**
-```java
- VirtualCore.get().installPackage({APK PATH}, flags);
-
-```
-
-**Launch a virtual App:**
-```java
- //VirtualApp support multi-user-mode which can run multiple instances of a same app.
- //if you don't need this feature, just set `{userId}` to 0.
- Intent intent = VirtualCore.get().getLaunchIntent({PackageName}, {userId});
- VActivityManager.get().startActivity(intent, {userId});
-```
-
-**Uninstall a virtual App:**
-```java
- VirtualCore.get().uninstallPackage({PackageName});
-```
-
-More details, please read the source code of demo app, :-)
-
-Documentation
--------------
-
-VirtualApp currently has **no documentation**, If you are interested in VirtualApp, please send email to me.
-
-License
+
+
+There are three ways to install an APP or Xposed Module to VirtualXposed:
+
+1. **Clone an installed app from your original system.** (Click Button at bottom of home page, then click Add App, the first page shows a list of installed apps.)
+2. **Install via an APK file.** (Click Button at bottom of home page, then click Add App, the second page shows APKs found in your sdcard)
+3. **Install via an external file chooser.** (Click Button at bottom of home page, then click Add App, use the floating action button to choose an APK file to install)
+
+For Xposed Module, You can install it from Xposed Installer, too.
+
+### Activate the Xposed Module
+
+Open Xposed Installer in VirtualXposed, go to the module fragment, check the module you want to use:
+
+
+
+### Reboot
+
+You only need to reboot VirtualXposed, **There's no need to reboot your phone**; Just click Settings in home page of VirtualXposed, click `Reboot` button, and VirtualXposed will reboot in a blink.
+
+
+
+Modules tested by myself
+-------------------------
+
+- [XPrivacyLua][xpl]: Really simple to use privacy manager for Android 6.0 Marshmallow and later.
+- [XInsta][xinsta]: Instagram module(Feed downing, stories downloading, etc).
+- [Minminguard][minminguard]: Completely remove both the ads inside apps and the empty space caused by those ads.
+- [YouTube AdAway][yta]: Get rid of ads on the official YouTube App.
+- [微X模块][wx]: 微信模块,功能强大。
+- [畅玩微信][cwwx]: 微信模块新秀,功能丰富。
+- [微信巫师][wxws]: 微信模块,项目开源,代码优秀。
+- [MDWechat][mdwechat]: 微信美化模块,可以把微信整成MD风格。
+- [应用变量][yybl]: 可以用来进行机型修改,比如王者荣耀高帧率;QQ空间修改小尾巴等。
+- [音量增强器][ylzqq]: 网易云音乐模块,非常好用,低调。
+- [微信学英语][wxxyy]: 自动把微信消息翻译为英语,非常实用。
+- [情迁抢包][qqqb]: 微信QQ抢红包模块。
+- [微信跳一跳助手][ttzs]: 微信跳一跳游戏辅助模块。
+- [步数修改器][bsxg]: 运动步数修改模块。
+- [模拟位置][mnwz]: 虚拟定位模块,稳定好用。
+- [指纹支付][zwzf]: 对不支持指纹支付但系统本身有指纹的手机开启指纹支付的模块。
+- [QQ精简模块 2.0][qqjj]: QQ模块,不仅可以精简QQ,还能防撤回,防闪照。
+- [微信增强插件][wxzqcj]: 微信模块,VXP内最稳定的微信模块;如无特殊需求建议用这个。
+- [QX模块][qx]: QQ模块,防撤回抢红包斗图一应俱全。
+- [QQ斗图神器][qqdtsq]: 各种表情,斗图神器。
+- [微信斗图神器][wxdtsq]: 斗图神器,微信用的。
+- [大圣净化][dsjh]: 去广告神器,推荐使用。
+
+Supported modules are far more than above, you should test it by yourself, and welcome to send me a PR for the list above.
+
+Others
-------
-GPL 3.0
-About Author
-------------
+### GameGuardian
+
+VirtualXposed also supports GameGuardian, **you should use the separate version for GameGuardian**.(Download it in release page).
+
+[Video Tutorial](https://gameguardian.net/forum/gallery/image/437-no-root-via-virtualxposed-without-error-105-gameguardian/)
+
+### VirusTotal
+
+VirusTotal might report VirtualXposed as a malware, it is stupid, you can refer to my [explanation](https://github.com/android-hacker/VirtualXposed/issues/10).
+
+And obviously, VirtualXposed is open source, so you can refer to the source code. I am sure that it is safe to use.
+
+If you still couldn't believe in me, you can install version [0.8.7](https://github.com/android-hacker/VirtualXposed/releases/tag/0.8.7); VirusTotal reports this version as safe.
+
+Support
+-----------
+
+Contributions to VirtualXposed are always welcomed!!
+
+For Developers
+--------------
+
+- [File a bug](https://github.com/android-hacker/exposed/issues)
+- [Wiki](https://github.com/android-hacker/VirtualXposed/wiki)
+
+Credits
+-------
- Lody (imlody@foxmail.com)
+1. [VirtualApp](https://github.com/asLody/VirtualApp)
+2. [Xposed](https://github.com/rovo89/Xposed)
+
+[wx]: http://repo.xposed.info/module/com.fkzhang.wechatxposed
+[qx]: http://repo.xposed.info/module/com.fkzhang.qqxposed
+[wxws]: https://github.com/Gh0u1L5/WechatMagician/releases
+[yybl]: https://www.coolapk.com/apk/com.sollyu.xposed.hook.model
+[ylzqq]: https://github.com/bin456789/Unblock163MusicClient-Xposed/releases
+[wxxyy]: https://www.coolapk.com/apk/com.hiwechart.translate
+[qqqb]: http://repo.xposed.info/module/cn.qssq666.redpacket
+[ttzs]: http://repo.xposed.info/module/com.emily.mmjumphelper
+[mnwz]: https://www.coolapk.com/apk/com.rong.xposed.fakelocation
+[zwzf]: https://github.com/android-hacker/Xposed-Fingerprint-pay/releases
+[bsxg]: https://www.coolapk.com/apk/com.specher.sm
+[mdwechat]: https://github.com/Blankeer/MDWechat
+[wxzqcj]:https://github.com/firesunCN/WechatEnhancement
+[qqjj]: https://www.coolapk.com/apk/me.zpp0196.qqsimple
+[qqdtsq]: https://www.coolapk.com/apk/x.hook.qqemoji
+[wxdtsq]: https://www.coolapk.com/apk/x.hook.emojihook
+[dsjh]: https://wiki.ad-gone.com/archives/32
+[xpl]: https://github.com/android-hacker/VirtualXposed/wiki/Privacy-control(XPrivacyLua)
+[minminguard]: http://repo.xposed.info/module/tw.fatminmin.xposed.minminguard
+[yta]: http://repo.xposed.info/module/ma.wanam.youtubeadaway
+[xinsta]: http://repo.xposed.info/module/com.ihelp101.instagram
+[cwwx]: http://repo.xposed.info/module/com.example.wx_plug_in3
diff --git a/VirtualApp/.gitignore b/VirtualApp/.gitignore
index c07e41de4..6f0efc106 100644
--- a/VirtualApp/.gitignore
+++ b/VirtualApp/.gitignore
@@ -7,3 +7,4 @@
.DS_Store
/build
/captures
+!*.apk
diff --git a/VirtualApp/app/build.gradle b/VirtualApp/app/build.gradle
index d7bc65045..6c4ae80c4 100644
--- a/VirtualApp/app/build.gradle
+++ b/VirtualApp/app/build.gradle
@@ -1,51 +1,103 @@
apply plugin: 'com.android.application'
-apply plugin: 'me.tatarka.retrolambda'
+apply plugin: 'io.fabric'
+repositories {
+ maven { url 'https://maven.fabric.io/public' }
+}
+
+Properties properties = new Properties()
+def localProp = file(project.rootProject.file('local.properties'))
+if (localProp.exists()) {
+ properties.load(localProp.newDataInputStream())
+}
+def keyFile = file(properties.getProperty("keystore.path") ?: "/tmp/does_not_exist")
android {
- compileSdkVersion 25
- buildToolsVersion "25.0.2"
+ signingConfigs {
+ config {
+ keyAlias properties.getProperty("keystore.alias")
+ keyPassword properties.getProperty("keystore.pwd")
+ storeFile keyFile
+ storePassword properties.getProperty("keystore.alias_pwd")
+ }
+ }
+
+ compileSdkVersion 28
+ buildToolsVersion '28.0.3'
defaultConfig {
- applicationId "io.virtualapp"
- minSdkVersion 15
- targetSdkVersion 21
- versionCode 23
- versionName "1.2.4"
+ applicationId "io.va.exposed"
+ minSdkVersion 21
+ targetSdkVersion 23
+ versionCode 170
+ versionName "0.17.0"
+ multiDexEnabled false
android {
defaultConfig {
ndk {
- abiFilters "armeabi", "armeabi-v7a", "x86"
+ abiFilters "armeabi-v7a", "x86"
}
}
}
}
+ // https://developer.android.com/studio/build/gradle-plugin-3-0-0-migration.html?utm_source=android-studio
+ flavorDimensions 'main'
+ productFlavors {
+ aosp {
+ dimension 'main'
+ //matchingFallbacks ['aosp']
+ if (keyFile.exists()) {
+ signingConfig signingConfigs.config
+ }
+ }
+ }
+ sourceSets {
+ main {
+ jniLibs.srcDirs = ['libs']
+ }
+ }
buildTypes {
release {
minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ debuggable
+ debuggable false
}
}
-
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
+ lintOptions {
+ checkReleaseBuilds false
+ // Or, if you prefer, you can continue to check for errors in release builds,
+ // but continue the build even when errors are found:
+ abortOnError false
+ }
}
dependencies {
- compile fileTree(include: ['*.jar'], dir: 'libs')
- compile project(':lib')
+ implementation fileTree(include: ['*.jar'], dir: 'libs')
+ implementation project(':lib')
+ implementation project(':launcher')
//Android Lib
- compile 'com.android.support:appcompat-v7:25.0.1'
- compile 'com.melnykov:floatingactionbutton:1.3.0'
- compile 'com.android.support:recyclerview-v7:25.0.1'
- compile 'com.android.support:percent:25.0.1'
- compile 'com.android.support:design:25.0.1'
- compile 'com.android.support:cardview-v7:25.0.1'
+ implementation 'com.android.support:multidex:1.0.3'
+ implementation 'com.android.support:appcompat-v7:27.1.1'
+ implementation 'com.android.support:recyclerview-v7:27.1.1'
+ implementation 'com.android.support:design:27.1.1'
//Promise Support
- compile 'org.jdeferred:jdeferred-android-aar:1.2.4'
+ implementation 'org.jdeferred:jdeferred-android-aar:1.2.4'
// ThirdParty
- compile 'com.jonathanfinerty.once:once:1.0.3'
+ implementation 'com.jonathanfinerty.once:once:1.0.3'
+ implementation('com.crashlytics.sdk.android:crashlytics:2.9.0@aar') {
+ transitive = true
+ }
+ implementation 'com.kyleduo.switchbutton:library:1.4.6'
+ implementation 'com.allenliu.versionchecklib:library:1.8.3'
+ implementation 'com.github.medyo:android-about-page:1.2.2'
+ implementation 'moe.feng:AlipayZeroSdk:1.1'
- compile 'com.flurry.android:analytics:6.9.2'
+ //Glide
+ implementation ('com.github.bumptech.glide:glide:4.8.0') {
+ exclude(group: "com.android.support")
+ }
+ annotationProcessor 'com.github.bumptech.glide:compiler:4.8.0'
}
diff --git a/VirtualApp/app/proguard-rules.pro b/VirtualApp/app/proguard-rules.pro
index 93c6c7ca0..b592419bc 100644
--- a/VirtualApp/app/proguard-rules.pro
+++ b/VirtualApp/app/proguard-rules.pro
@@ -15,3 +15,31 @@
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
+-keep class com.amap.api.maps.**{*;}
+-keep class com.autonavi.**{*;}
+-keep class com.amap.api.trace.**{*;}
+
+#定位
+-keep class com.amap.api.location.**{*;}
+-keep class com.amap.api.fence.**{*;}
+-keep class com.autonavi.aps.amapapi.model.**{*;}
+
+#搜索
+-keep class com.amap.api.services.**{*;}
+
+#2D地图
+-keep class com.amap.api.maps2d.**{*;}
+-keep class com.amap.api.mapcore2d.**{*;}
+
+#导航
+-keep class com.amap.api.navi.**{*;}
+-keep class com.autonavi.**{*;}
+
+##--Glide--
+-keep class com.bumptech.glide.**{*;}
+-keep public class * implements com.bumptech.glide.module.GlideModule
+-keep public class * extends com.bumptech.glide.module.AppGlideModule
+-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
+ **[] $VALUES;
+ public *;
+}
\ No newline at end of file
diff --git a/VirtualApp/app/src/main/AndroidManifest.xml b/VirtualApp/app/src/main/AndroidManifest.xml
index 0f906711e..774e06b3c 100644
--- a/VirtualApp/app/src/main/AndroidManifest.xml
+++ b/VirtualApp/app/src/main/AndroidManifest.xml
@@ -1,16 +1,37 @@
+
+
+
+
+
+ android:label="@string/vxp"
+ android:theme="@style/LauncherTheme"
+ tools:replace="android:icon,android:label">
+
+
+
+
+
+
@@ -18,21 +39,130 @@
-
-
+ android:screenOrientation="portrait"
+ android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/VirtualApp/app/src/main/assets/OaceOaT8w5Xda6wa b/VirtualApp/app/src/main/assets/OaceOaT8w5Xda6wa
deleted file mode 100644
index 0f6cebfa8..000000000
Binary files a/VirtualApp/app/src/main/assets/OaceOaT8w5Xda6wa and /dev/null differ
diff --git a/VirtualApp/app/src/main/assets/XposedInstaller_3.1.5.apk_ b/VirtualApp/app/src/main/assets/XposedInstaller_3.1.5.apk_
new file mode 100644
index 000000000..26f4a17bb
Binary files /dev/null and b/VirtualApp/app/src/main/assets/XposedInstaller_3.1.5.apk_ differ
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/VCommends.java b/VirtualApp/app/src/main/java/io/virtualapp/VCommends.java
index a095e92d3..f9c4e5fe3 100644
--- a/VirtualApp/app/src/main/java/io/virtualapp/VCommends.java
+++ b/VirtualApp/app/src/main/java/io/virtualapp/VCommends.java
@@ -1,5 +1,13 @@
package io.virtualapp;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.Signature;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
/**
* @author Lody
*/
@@ -12,4 +20,51 @@ public class VCommends {
public static final String EXTRA_APP_INFO_LIST = "va.extra.APP_INFO_LIST";
+ public static final String TAG_ASK_INSTALL_GMS = "va.extra.ASK_INSTALL_GMS";
+
+ static String getSig(Context context) {
+ try {
+ PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
+ Signature[] signs = packageInfo.signatures;
+ Signature sign = signs[0];
+ String signStr = md(sign.toByteArray());
+ return signStr;
+ } catch (PackageManager.NameNotFoundException e) {
+ e.printStackTrace();
+ }
+ return "";
+ }
+
+ static String md(byte[] byteStr) {
+ MessageDigest messageDigest = null;
+ StringBuffer md5StrBuff = new StringBuffer();
+ try {
+ messageDigest = MessageDigest.getInstance("MD5");
+ messageDigest.reset();
+ messageDigest.update(byteStr);
+ byte[] byteArray = messageDigest.digest();
+ for (int i = 0; i < byteArray.length; i++) {
+ if (Integer.toHexString(0xFF & byteArray[i]).length() == 1) {
+ md5StrBuff.append("0").append(Integer.toHexString(0xFF & byteArray[i]));
+ } else {
+ md5StrBuff.append(Integer.toHexString(0xFF & byteArray[i]));
+ }
+ }
+ } catch (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ }
+ return md5StrBuff.toString();
+ }
+
+ /**
+ * 君子坦荡荡,小人常戚戚
+ * @param context the context
+ */
+ public static void c(Context context) {
+ String sig = getSig(context);
+ if (!"99A244F52F40581B48E4BA61E3435B6C".equalsIgnoreCase(sig)) {
+ System.exit(10);
+ android.os.Process.killProcess(android.os.Process.myPid());
+ }
+ }
}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/VApp.java b/VirtualApp/app/src/main/java/io/virtualapp/XApp.java
similarity index 57%
rename from VirtualApp/app/src/main/java/io/virtualapp/VApp.java
rename to VirtualApp/app/src/main/java/io/virtualapp/XApp.java
index c7756175c..d06d96489 100644
--- a/VirtualApp/app/src/main/java/io/virtualapp/VApp.java
+++ b/VirtualApp/app/src/main/java/io/virtualapp/XApp.java
@@ -2,34 +2,50 @@
import android.app.Application;
import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Build;
-import com.flurry.android.FlurryAgent;
+import com.crashlytics.android.Crashlytics;
+import com.lody.virtual.client.NativeEngine;
import com.lody.virtual.client.core.VirtualCore;
-import com.lody.virtual.client.stub.StubManifest;
+import com.lody.virtual.client.stub.VASettings;
+import com.lody.virtual.os.VEnvironment;
+import io.fabric.sdk.android.Fabric;
import io.virtualapp.delegate.MyAppRequestListener;
import io.virtualapp.delegate.MyComponentDelegate;
+import io.virtualapp.delegate.MyCrashHandler;
import io.virtualapp.delegate.MyPhoneInfoDelegate;
-import io.virtualapp.delegate.MyTaskDescriptionDelegate;
+import io.virtualapp.delegate.MyTaskDescDelegate;
import jonathanfinerty.once.Once;
+import me.weishu.exposed.LogcatService;
/**
* @author Lody
*/
-public class VApp extends Application {
+public class XApp extends Application {
+ private static final String TAG = "XApp";
- private static VApp gApp;
+ public static final String XPOSED_INSTALLER_PACKAGE = "de.robv.android.xposed.installer";
- public static VApp getApp() {
+ private static XApp gApp;
+ private SharedPreferences mPreferences;
+
+ public static XApp getApp() {
return gApp;
}
@Override
protected void attachBaseContext(Context base) {
+ gApp = this;
super.attachBaseContext(base);
- StubManifest.ENABLE_IO_REDIRECT = true;
- StubManifest.ENABLE_INNER_SHORTCUT = false;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ NativeEngine.disableJit(Build.VERSION.SDK_INT);
+ }
+ mPreferences = base.getSharedPreferences("va", Context.MODE_MULTI_PROCESS);
+ VASettings.ENABLE_IO_REDIRECT = true;
+ VASettings.ENABLE_INNER_SHORTCUT = false;
try {
VirtualCore.get().startup(base);
} catch (Throwable e) {
@@ -39,35 +55,35 @@ protected void attachBaseContext(Context base) {
@Override
public void onCreate() {
- gApp = this;
super.onCreate();
VirtualCore virtualCore = VirtualCore.get();
virtualCore.initialize(new VirtualCore.VirtualInitializer() {
@Override
public void onMainProcess() {
- Once.initialise(VApp.this);
- new FlurryAgent.Builder()
- .withLogEnabled(true)
- .withListener(() -> {
- // nothing
- })
- .build(VApp.this, "48RJJP7ZCZZBB6KMMWW5");
+ Once.initialise(XApp.this);
+ Fabric.with(XApp.this, new Crashlytics());
}
@Override
public void onVirtualProcess() {
+ Fabric.with(XApp.this, new Crashlytics());
+
//listener components
virtualCore.setComponentDelegate(new MyComponentDelegate());
//fake phone imei,macAddress,BluetoothAddress
virtualCore.setPhoneInfoDelegate(new MyPhoneInfoDelegate());
//fake task description's icon and title
- virtualCore.setTaskDescriptionDelegate(new MyTaskDescriptionDelegate());
+ virtualCore.setTaskDescriptionDelegate(new MyTaskDescDelegate());
+ virtualCore.setCrashHandler(new MyCrashHandler());
+
+ // ensure the logcat service alive when every virtual process start.
+ LogcatService.start(XApp.this, VEnvironment.getDataUserPackageDirectory(0, XPOSED_INSTALLER_PACKAGE));
}
@Override
public void onServerProcess() {
- virtualCore.setAppRequestListener(new MyAppRequestListener(VApp.this));
+ virtualCore.setAppRequestListener(new MyAppRequestListener(XApp.this));
virtualCore.addVisibleOutsidePackage("com.tencent.mobileqq");
virtualCore.addVisibleOutsidePackage("com.tencent.mobileqqi");
virtualCore.addVisibleOutsidePackage("com.tencent.minihd.qq");
@@ -80,4 +96,8 @@ public void onServerProcess() {
});
}
+ public static SharedPreferences getPreferences() {
+ return getApp().mPreferences;
+ }
+
}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/abs/Value.java b/VirtualApp/app/src/main/java/io/virtualapp/abs/Value.java
deleted file mode 100644
index 193e58ae8..000000000
--- a/VirtualApp/app/src/main/java/io/virtualapp/abs/Value.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package io.virtualapp.abs;
-
-/**
- * @author Lody
- */
-
-public class Value {
- public T val;
-}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/abs/nestedadapter/RecyclerViewAdapterWrapper.java b/VirtualApp/app/src/main/java/io/virtualapp/abs/nestedadapter/RecyclerViewAdapterWrapper.java
deleted file mode 100644
index 540395474..000000000
--- a/VirtualApp/app/src/main/java/io/virtualapp/abs/nestedadapter/RecyclerViewAdapterWrapper.java
+++ /dev/null
@@ -1,110 +0,0 @@
-package io.virtualapp.abs.nestedadapter;
-
-import android.support.v7.widget.RecyclerView;
-import android.view.ViewGroup;
-
-public class RecyclerViewAdapterWrapper extends RecyclerView.Adapter {
-
- protected final RecyclerView.Adapter wrapped;
-
- public RecyclerViewAdapterWrapper(RecyclerView.Adapter wrapped) {
- super();
- this.wrapped = wrapped;
- this.wrapped.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
- public void onChanged() {
- notifyDataSetChanged();
- }
-
- public void onItemRangeChanged(int positionStart, int itemCount) {
- notifyItemRangeChanged(positionStart, itemCount);
- }
-
- public void onItemRangeInserted(int positionStart, int itemCount) {
- notifyItemRangeInserted(positionStart, itemCount);
- }
-
- public void onItemRangeRemoved(int positionStart, int itemCount) {
- notifyItemRangeRemoved(positionStart, itemCount);
- }
-
- public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
- notifyItemMoved(fromPosition, toPosition);
- }
- });
- }
-
- @Override
- public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- return wrapped.onCreateViewHolder(parent, viewType);
- }
-
-
- @Override
- public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
- wrapped.onBindViewHolder(holder, position);
- }
-
- @Override
- public int getItemCount() {
- return wrapped.getItemCount();
- }
-
- @Override
- public int getItemViewType(int position) {
- return wrapped.getItemViewType(position);
- }
-
- @Override
- public void setHasStableIds(boolean hasStableIds) {
- wrapped.setHasStableIds(hasStableIds);
- }
-
- @Override
- public long getItemId(int position) {
- return wrapped.getItemId(position);
- }
-
- @Override
- public void onViewRecycled(RecyclerView.ViewHolder holder) {
- wrapped.onViewRecycled(holder);
- }
-
- @Override
- public boolean onFailedToRecycleView(RecyclerView.ViewHolder holder) {
- return wrapped.onFailedToRecycleView(holder);
- }
-
- @Override
- public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
- wrapped.onViewAttachedToWindow(holder);
- }
-
- @Override
- public void onViewDetachedFromWindow(RecyclerView.ViewHolder holder) {
- wrapped.onViewDetachedFromWindow(holder);
- }
-
- @Override
- public void registerAdapterDataObserver(RecyclerView.AdapterDataObserver observer) {
- wrapped.registerAdapterDataObserver(observer);
- }
-
- @Override
- public void unregisterAdapterDataObserver(RecyclerView.AdapterDataObserver observer) {
- wrapped.unregisterAdapterDataObserver(observer);
- }
-
- @Override
- public void onAttachedToRecyclerView(RecyclerView recyclerView) {
- wrapped.onAttachedToRecyclerView(recyclerView);
- }
-
- @Override
- public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
- wrapped.onDetachedFromRecyclerView(recyclerView);
- }
-
- public RecyclerView.Adapter getWrappedAdapter() {
- return wrapped;
- }
-}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/abs/nestedadapter/SmartRecyclerAdapter.java b/VirtualApp/app/src/main/java/io/virtualapp/abs/nestedadapter/SmartRecyclerAdapter.java
deleted file mode 100644
index ef8ee75ca..000000000
--- a/VirtualApp/app/src/main/java/io/virtualapp/abs/nestedadapter/SmartRecyclerAdapter.java
+++ /dev/null
@@ -1,126 +0,0 @@
-package io.virtualapp.abs.nestedadapter;
-
-import android.support.annotation.NonNull;
-import android.support.v7.widget.GridLayoutManager;
-import android.support.v7.widget.RecyclerView;
-import android.support.v7.widget.StaggeredGridLayoutManager;
-import android.view.View;
-import android.view.ViewGroup;
-
-public class SmartRecyclerAdapter extends RecyclerViewAdapterWrapper {
- public static final int TYPE_HEADER = -1;
- public static final int TYPE_FOOTER = -2;
-
- private RecyclerView.LayoutManager layoutManager;
-
- private View headerView, footerView;
-
- public SmartRecyclerAdapter(@NonNull RecyclerView.Adapter targetAdapter) {
- super(targetAdapter);
- }
-
- public void setHeaderView(View view) {
- headerView = view;
- getWrappedAdapter().notifyDataSetChanged();
- }
-
- public void removeHeaderView() {
- headerView = null;
- getWrappedAdapter().notifyDataSetChanged();
- }
-
- public void setFooterView(View view) {
- footerView = view;
- getWrappedAdapter().notifyDataSetChanged();
- }
-
- public void removeFooterView() {
- footerView = null;
- getWrappedAdapter().notifyDataSetChanged();
- }
-
- private void setGridHeaderFooter(RecyclerView.LayoutManager layoutManager) {
- if (layoutManager instanceof GridLayoutManager) {
- final GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
- gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
- @Override
- public int getSpanSize(int position) {
- boolean isShowHeader = (position == 0 && hasHeader());
- boolean isShowFooter = (position == getItemCount() - 1 && hasFooter());
- if (isShowFooter || isShowHeader) {
- return gridLayoutManager.getSpanCount();
- }
- return 1;
- }
- });
- }
- }
-
- private boolean hasHeader() {
- return headerView != null;
- }
-
- private boolean hasFooter() {
- return footerView != null;
- }
-
- @Override
- public void onAttachedToRecyclerView(RecyclerView recyclerView) {
- super.onAttachedToRecyclerView(recyclerView);
- layoutManager = recyclerView.getLayoutManager();
- setGridHeaderFooter(layoutManager);
- }
-
- @Override
- public int getItemCount() {
- return super.getItemCount() + (hasHeader() ? 1 : 0) + (hasFooter() ? 1 : 0);
- }
-
- @Override
- public int getItemViewType(int position) {
- if (hasHeader() && position == 0) {
- return TYPE_HEADER;
- }
-
- if (hasFooter() && position == getItemCount() - 1) {
- return TYPE_FOOTER;
- }
- return super.getItemViewType(hasHeader() ? position - 1 : position);
- }
-
- @Override
- public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- View itemView = null;
- if (viewType == TYPE_HEADER) {
- itemView = headerView;
- } else if (viewType == TYPE_FOOTER) {
- itemView = footerView;
- }
- if (itemView != null) {
- //set StaggeredGridLayoutManager header & footer view
- if (layoutManager instanceof StaggeredGridLayoutManager) {
- ViewGroup.LayoutParams targetParams = itemView.getLayoutParams();
- StaggeredGridLayoutManager.LayoutParams StaggerLayoutParams;
- if (targetParams != null) {
- StaggerLayoutParams = new StaggeredGridLayoutManager.LayoutParams(targetParams.width, targetParams.height);
- } else {
- StaggerLayoutParams = new StaggeredGridLayoutManager.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
- }
- StaggerLayoutParams.setFullSpan(true);
- itemView.setLayoutParams(StaggerLayoutParams);
- }
- return new RecyclerView.ViewHolder(itemView) {
- };
- }
- return super.onCreateViewHolder(parent, viewType);
- }
-
- @Override
- public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
- if (getItemViewType(position) == TYPE_HEADER || getItemViewType(position) == TYPE_FOOTER) {
- //if you need get header & footer state , do here
- return;
- }
- super.onBindViewHolder(holder, hasHeader() ? position - 1 : position);
- }
-}
\ No newline at end of file
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/abs/percent/PercentLinearLayout.java b/VirtualApp/app/src/main/java/io/virtualapp/abs/percent/PercentLinearLayout.java
deleted file mode 100644
index 1e0f0e851..000000000
--- a/VirtualApp/app/src/main/java/io/virtualapp/abs/percent/PercentLinearLayout.java
+++ /dev/null
@@ -1,77 +0,0 @@
-package io.virtualapp.abs.percent;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.support.percent.PercentLayoutHelper;
-import android.util.AttributeSet;
-import android.view.ViewGroup;
-import android.widget.LinearLayout;
-
-/**
- * @author Lody
- */
-public class PercentLinearLayout extends LinearLayout {
-
- private PercentLayoutHelper mPercentLayoutHelper;
-
- public PercentLinearLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- mPercentLayoutHelper = new PercentLayoutHelper(this);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- mPercentLayoutHelper.adjustChildren(widthMeasureSpec, heightMeasureSpec);
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- if (mPercentLayoutHelper.handleMeasuredStateTooSmall()) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
- }
-
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- super.onLayout(changed, l, t, r, b);
- mPercentLayoutHelper.restoreOriginalParams();
- }
-
- @Override
- public LayoutParams generateLayoutParams(AttributeSet attrs) {
- return new LayoutParams(getContext(), attrs);
- }
-
- public static class LayoutParams extends LinearLayout.LayoutParams
- implements
- PercentLayoutHelper.PercentLayoutParams {
- private PercentLayoutHelper.PercentLayoutInfo mPercentLayoutInfo;
-
- public LayoutParams(Context c, AttributeSet attrs) {
- super(c, attrs);
- mPercentLayoutInfo = PercentLayoutHelper.getPercentLayoutInfo(c, attrs);
- }
-
- public LayoutParams(int width, int height) {
- super(width, height);
- }
-
- public LayoutParams(ViewGroup.LayoutParams source) {
- super(source);
- }
-
- public LayoutParams(MarginLayoutParams source) {
- super(source);
- }
-
- @Override
- public PercentLayoutHelper.PercentLayoutInfo getPercentLayoutInfo() {
- return mPercentLayoutInfo;
- }
-
- @Override
- protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
- PercentLayoutHelper.fetchWidthAndHeight(this, a, widthAttr, heightAttr);
- }
-
- }
-
-}
\ No newline at end of file
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/abs/reflect/ReflectException.java b/VirtualApp/app/src/main/java/io/virtualapp/abs/reflect/ReflectException.java
deleted file mode 100644
index 3b6e57352..000000000
--- a/VirtualApp/app/src/main/java/io/virtualapp/abs/reflect/ReflectException.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package io.virtualapp.abs.reflect;
-
-/**
- * @author Lody
- */
-public class ReflectException extends RuntimeException {
-
- private static final long serialVersionUID = 663038727503637969L;
-
- public ReflectException(Throwable cause) {
- super(cause);
- }
-}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/abs/ui/VActivity.java b/VirtualApp/app/src/main/java/io/virtualapp/abs/ui/VActivity.java
index 7f091e00f..b968cbead 100644
--- a/VirtualApp/app/src/main/java/io/virtualapp/abs/ui/VActivity.java
+++ b/VirtualApp/app/src/main/java/io/virtualapp/abs/ui/VActivity.java
@@ -6,8 +6,6 @@
import android.support.v4.app.Fragment;
import android.support.v7.app.AppCompatActivity;
-import com.flurry.android.FlurryAgent;
-
import org.jdeferred.android.AndroidDeferredManager;
import io.virtualapp.abs.BaseView;
@@ -46,12 +44,10 @@ public void replaceFragment(@IdRes int id, Fragment fragment) {
@Override
protected void onStart() {
super.onStart();
- FlurryAgent.onStartSession(this);
}
@Override
protected void onStop() {
super.onStop();
- FlurryAgent.onEndSession(this);
}
}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/delegate/MyAppRequestListener.java b/VirtualApp/app/src/main/java/io/virtualapp/delegate/MyAppRequestListener.java
index 707d3fa42..358d61f1f 100644
--- a/VirtualApp/app/src/main/java/io/virtualapp/delegate/MyAppRequestListener.java
+++ b/VirtualApp/app/src/main/java/io/virtualapp/delegate/MyAppRequestListener.java
@@ -1,13 +1,15 @@
package io.virtualapp.delegate;
import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
import android.widget.Toast;
-import com.lody.virtual.client.core.InstallStrategy;
import com.lody.virtual.client.core.VirtualCore;
-import com.lody.virtual.remote.InstallResult;
-import java.io.IOException;
+import java.io.File;
+
+import io.virtualapp.sys.InstallerActivity;
/**
* @author Lody
@@ -23,27 +25,18 @@ public MyAppRequestListener(Context context) {
@Override
public void onRequestInstall(String path) {
- Toast.makeText(context, "Installing: " + path, Toast.LENGTH_SHORT).show();
- InstallResult res = VirtualCore.get().installPackage(path, InstallStrategy.UPDATE_IF_EXIST);
- if (res.isSuccess) {
- try {
- VirtualCore.get().preOpt(res.packageName);
- } catch (IOException e) {
- e.printStackTrace();
- }
- if (res.isUpdate) {
- Toast.makeText(context, "Update: " + res.packageName + " success!", Toast.LENGTH_SHORT).show();
- } else {
- Toast.makeText(context, "Install: " + res.packageName + " success!", Toast.LENGTH_SHORT).show();
- }
- } else {
- Toast.makeText(context, "Install failed: " + res.error, Toast.LENGTH_SHORT).show();
+ try {
+ Intent t = new Intent(context, InstallerActivity.class);
+ t.setDataAndType(Uri.fromFile(new File(path)), "application/vnd.android.package-archive");
+ t.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(t);
+ } catch (Throwable e) {
+ e.printStackTrace();
}
}
@Override
public void onRequestUninstall(String pkg) {
Toast.makeText(context, "Uninstall: " + pkg, Toast.LENGTH_SHORT).show();
-
}
}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/delegate/MyComponentDelegate.java b/VirtualApp/app/src/main/java/io/virtualapp/delegate/MyComponentDelegate.java
index 5a4a3af61..06a861292 100644
--- a/VirtualApp/app/src/main/java/io/virtualapp/delegate/MyComponentDelegate.java
+++ b/VirtualApp/app/src/main/java/io/virtualapp/delegate/MyComponentDelegate.java
@@ -1,6 +1,7 @@
package io.virtualapp.delegate;
import android.app.Activity;
+import android.app.Application;
import android.content.Intent;
import com.lody.virtual.client.hook.delegate.ComponentDelegate;
@@ -10,6 +11,17 @@
public class MyComponentDelegate implements ComponentDelegate {
+
+ @Override
+ public void beforeApplicationCreate(Application application) {
+
+ }
+
+ @Override
+ public void afterApplicationCreate(Application application) {
+
+ }
+
@Override
public void beforeActivityCreate(Activity activity) {
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/delegate/MyCrashHandler.java b/VirtualApp/app/src/main/java/io/virtualapp/delegate/MyCrashHandler.java
new file mode 100644
index 000000000..4eca7e8c8
--- /dev/null
+++ b/VirtualApp/app/src/main/java/io/virtualapp/delegate/MyCrashHandler.java
@@ -0,0 +1,86 @@
+package io.virtualapp.delegate;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.os.Looper;
+import android.util.Log;
+
+import com.crashlytics.android.Crashlytics;
+import com.lody.virtual.client.VClientImpl;
+import com.lody.virtual.client.core.CrashHandler;
+import com.lody.virtual.client.core.VirtualCore;
+import com.lody.virtual.os.VUserHandle;
+import com.lody.virtual.remote.InstalledAppInfo;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * author: weishu on 18/3/10.
+ */
+public class MyCrashHandler implements CrashHandler {
+
+ private static final String TAG = "XApp";
+ private static final String CRASH_SP = "vxp_crash";
+ private static final String KEY_LAST_CRASH_TIME = "last_crash_time";
+ private static final String KEY_LAST_CRASH_TYPE = "last_crash_type";
+
+ @SuppressLint("ApplySharedPref")
+ @Override
+ public void handleUncaughtException(Thread t, Throwable e) {
+ SharedPreferences sp = VirtualCore.get().getContext().getSharedPreferences(CRASH_SP, Context.MODE_MULTI_PROCESS);
+
+ try {
+ ApplicationInfo currentApplicationInfo = VClientImpl.get().getCurrentApplicationInfo();
+ if (currentApplicationInfo != null) {
+ String packageName = currentApplicationInfo.packageName;
+ String processName = currentApplicationInfo.processName;
+
+ Crashlytics.setString("process", processName);
+ Crashlytics.setString("package", packageName);
+
+ int userId = VUserHandle.myUserId();
+
+ Crashlytics.setInt("uid", userId);
+
+ InstalledAppInfo installedAppInfo = VirtualCore.get().getInstalledAppInfo(packageName, 0);
+ if (installedAppInfo != null) {
+ PackageInfo packageInfo = installedAppInfo.getPackageInfo(userId);
+ if (packageInfo != null) {
+ String versionName = packageInfo.versionName;
+ int versionCode = packageInfo.versionCode;
+
+ Crashlytics.setString("versionName", versionName);
+ Crashlytics.setInt("versionCode", versionCode);
+
+ }
+ }
+ }
+ } catch (Throwable ignored) {
+ }
+ final String exceptionType = e.getClass().getName();
+ final long now = System.currentTimeMillis();
+
+ final long lastCrash = sp.getLong(KEY_LAST_CRASH_TIME, 0);
+ final String lastCrashType = sp.getString(KEY_LAST_CRASH_TYPE, null);
+
+ if (exceptionType.equals(lastCrashType) && (now - lastCrash) < TimeUnit.MINUTES.toMillis(1)) {
+ // continues crash, do not upload
+ } else {
+ Crashlytics.logException(e);
+ }
+
+ Log.i(TAG, "uncaught :" + t, e);
+
+ // must commit.
+ sp.edit().putLong(KEY_LAST_CRASH_TIME, now).putString(KEY_LAST_CRASH_TYPE, exceptionType).commit();
+
+ if (t == Looper.getMainLooper().getThread()) {
+ System.exit(0);
+ } else {
+ Log.e(TAG, "ignore uncaught exception of sub thread: " + t);
+ }
+ }
+}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/delegate/MyTaskDescriptionDelegate.java b/VirtualApp/app/src/main/java/io/virtualapp/delegate/MyTaskDescDelegate.java
similarity index 73%
rename from VirtualApp/app/src/main/java/io/virtualapp/delegate/MyTaskDescriptionDelegate.java
rename to VirtualApp/app/src/main/java/io/virtualapp/delegate/MyTaskDescDelegate.java
index 6c8e733c4..49b1469f9 100644
--- a/VirtualApp/app/src/main/java/io/virtualapp/delegate/MyTaskDescriptionDelegate.java
+++ b/VirtualApp/app/src/main/java/io/virtualapp/delegate/MyTaskDescDelegate.java
@@ -12,12 +12,16 @@
* Patch the task description with the (Virtual) user name
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
-public class MyTaskDescriptionDelegate implements TaskDescriptionDelegate {
+public class MyTaskDescDelegate implements TaskDescriptionDelegate {
@Override
public ActivityManager.TaskDescription getTaskDescription(ActivityManager.TaskDescription oldTaskDescription) {
+ if (oldTaskDescription == null) {
+ return null;
+ }
String labelPrefix = "[" + VUserManager.get().getUserName() + "] ";
+ String oldLabel = oldTaskDescription.getLabel() != null ? oldTaskDescription.getLabel() : "";
- if (!oldTaskDescription.getLabel().startsWith(labelPrefix)) {
+ if (!oldLabel.startsWith(labelPrefix)) {
// Is it really necessary?
return new ActivityManager.TaskDescription(labelPrefix + oldTaskDescription.getLabel(), oldTaskDescription.getIcon(), oldTaskDescription.getPrimaryColor());
} else {
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/dev/CmdReceiver.java b/VirtualApp/app/src/main/java/io/virtualapp/dev/CmdReceiver.java
new file mode 100644
index 000000000..fc704a8ae
--- /dev/null
+++ b/VirtualApp/app/src/main/java/io/virtualapp/dev/CmdReceiver.java
@@ -0,0 +1,103 @@
+package io.virtualapp.dev;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.text.TextUtils;
+import android.widget.Toast;
+
+import com.lody.virtual.client.core.InstallStrategy;
+import com.lody.virtual.client.core.VirtualCore;
+import com.lody.virtual.remote.InstallResult;
+
+import io.virtualapp.home.LoadingActivity;
+
+/**
+ * author: weishu on 18/2/23.
+ */
+
+public class CmdReceiver extends BroadcastReceiver {
+
+ private static final String ACTION = "io.va.exposed.CMD";
+ private static final String KEY_CMD = "cmd";
+ private static final String KEY_PKG = "pkg";
+ private static final String KEY_UID = "uid";
+
+ private static final String CMD_UPDATE = "update";
+ private static final String CMD_REBOOT = "reboot";
+ private static final String CMD_LAUNCH = "launch";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (!ACTION.equalsIgnoreCase(action)) {
+ return;
+ }
+
+ String cmd = intent.getStringExtra(KEY_CMD);
+ if (TextUtils.isEmpty(cmd)) {
+ showTips(context, "No cmd found!");
+ return;
+ }
+
+ if (CMD_REBOOT.equalsIgnoreCase(cmd)) {
+ VirtualCore.get().killAllApps();
+ showTips(context, "Reboot Success!!");
+ return;
+ }
+
+ if (CMD_UPDATE.equalsIgnoreCase(cmd)) {
+ String pkg = intent.getStringExtra(KEY_PKG);
+ if (TextUtils.isEmpty(pkg)) {
+ showTips(context, "Please tell me the update package!!");
+ return;
+ }
+
+ PackageManager packageManager = context.getPackageManager();
+ if (packageManager == null) {
+ showTips(context, "system error, update failed!");
+ return;
+ }
+
+ try {
+ ApplicationInfo applicationInfo = packageManager.getApplicationInfo(pkg, 0);
+ String apkPath = applicationInfo.sourceDir;
+ InstallResult installResult = VirtualCore.get().installPackage(apkPath, InstallStrategy.UPDATE_IF_EXIST);
+ if (installResult.isSuccess) {
+ if (installResult.isUpdate) {
+ showTips(context, "Update " + pkg + " Success!!");
+ }
+ } else {
+ showTips(context, "Update " + pkg + " failed: " + installResult.error);
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ showTips(context, "Can not found " + pkg + " outside!");
+ }
+ } else if (CMD_LAUNCH.equalsIgnoreCase(cmd)) {
+ String pkg = intent.getStringExtra(KEY_PKG);
+ if (TextUtils.isEmpty(pkg)) {
+ showTips(context, "Please tell me the launch package!!");
+ return;
+ }
+ String uid = intent.getStringExtra(KEY_UID);
+ int userId = 0;
+ if (!TextUtils.isEmpty(uid)){
+ try {
+ userId = Integer.parseInt(uid);
+ }catch (NumberFormatException e){
+ e.printStackTrace();
+ }
+ }
+ LoadingActivity.launch(context, pkg, userId);
+
+
+ }
+ }
+
+ private void showTips(Context context, String tips) {
+ Toast.makeText(context, tips, Toast.LENGTH_SHORT).show();
+
+ }
+}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/effects/ExplosionAnimator.java b/VirtualApp/app/src/main/java/io/virtualapp/effects/ExplosionAnimator.java
deleted file mode 100644
index 90fc09b44..000000000
--- a/VirtualApp/app/src/main/java/io/virtualapp/effects/ExplosionAnimator.java
+++ /dev/null
@@ -1,139 +0,0 @@
-package io.virtualapp.effects;
-
-import java.util.Random;
-
-import android.animation.ValueAnimator;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.view.View;
-import android.view.animation.AccelerateInterpolator;
-import android.view.animation.Interpolator;
-
-import io.virtualapp.VApp;
-import io.virtualapp.abs.ui.VUiKit;
-
-public class ExplosionAnimator extends ValueAnimator {
-
- private static final Interpolator DEFAULT_INTERPOLATOR = new AccelerateInterpolator(0.6f);
- private static final float END_VALUE = 1.4f;
- private static final float X = VUiKit.dpToPx(VApp.getApp(), 5);
- private static final float Y = VUiKit.dpToPx(VApp.getApp(), 20);
- private static final float V = VUiKit.dpToPx(VApp.getApp(), 2);
- private static final float W = VUiKit.dpToPx(VApp.getApp(), 1);
- static long DEFAULT_DURATION = 0x450;
- private Paint mPaint;
- private Particle[] mParticles;
- private Rect mBound;
- private View mContainer;
-
- public ExplosionAnimator(View container, Bitmap bitmap, Rect bound) {
- mPaint = new Paint();
- mBound = new Rect(bound);
- int partLen = 15;
- mParticles = new Particle[partLen * partLen];
- Random random = new Random(System.currentTimeMillis());
- int w = bitmap.getWidth() / (partLen + 2);
- int h = bitmap.getHeight() / (partLen + 2);
- for (int i = 0; i < partLen; i++) {
- for (int j = 0; j < partLen; j++) {
- mParticles[(i * partLen) + j] = generateParticle(bitmap.getPixel((j + 1) * w, (i + 1) * h), random);
- }
- }
- mContainer = container;
- setFloatValues(0f, END_VALUE);
- setInterpolator(DEFAULT_INTERPOLATOR);
- setDuration(DEFAULT_DURATION);
- }
-
- private Particle generateParticle(int color, Random random) {
- Particle particle = new Particle();
- particle.color = color;
- particle.radius = V;
- if (random.nextFloat() < 0.2f) {
- particle.baseRadius = V + ((X - V) * random.nextFloat());
- } else {
- particle.baseRadius = W + ((V - W) * random.nextFloat());
- }
- float nextFloat = random.nextFloat();
- particle.top = mBound.height() * ((0.18f * random.nextFloat()) + 0.2f);
- particle.top = nextFloat < 0.2f ? particle.top : particle.top + ((particle.top * 0.2f) * random.nextFloat());
- particle.bottom = (mBound.height() * (random.nextFloat() - 0.5f)) * 1.8f;
- float f = nextFloat < 0.2f
- ? particle.bottom
- : nextFloat < 0.8f ? particle.bottom * 0.6f : particle.bottom * 0.3f;
- particle.bottom = f;
- particle.mag = 4.0f * particle.top / particle.bottom;
- particle.neg = (-particle.mag) / particle.bottom;
- f = mBound.centerX() + (Y * (random.nextFloat() - 0.5f));
- particle.baseCx = f;
- particle.cx = f;
- f = mBound.centerY() + (Y * (random.nextFloat() - 0.5f));
- particle.baseCy = f;
- particle.cy = f;
- particle.life = END_VALUE / 10 * random.nextFloat();
- particle.overflow = 0.4f * random.nextFloat();
- particle.alpha = 1f;
- return particle;
- }
-
- public boolean draw(Canvas canvas) {
- if (!isStarted()) {
- return false;
- }
- for (Particle particle : mParticles) {
- particle.advance((float) getAnimatedValue());
- if (particle.alpha > 0f) {
- mPaint.setColor(particle.color);
- mPaint.setAlpha((int) (Color.alpha(particle.color) * particle.alpha));
- canvas.drawCircle(particle.cx, particle.cy, particle.radius, mPaint);
- }
- }
- mContainer.invalidate();
- return true;
- }
-
- @Override
- public void start() {
- super.start();
- mContainer.invalidate(mBound);
- }
-
- private class Particle {
- float alpha;
- int color;
- float cx;
- float cy;
- float radius;
- float baseCx;
- float baseCy;
- float baseRadius;
- float top;
- float bottom;
- float mag;
- float neg;
- float life;
- float overflow;
-
- public void advance(float factor) {
- float f = 0f;
- float normalization = factor / END_VALUE;
- if (normalization < life || normalization > 1f - overflow) {
- alpha = 0f;
- return;
- }
- normalization = (normalization - life) / (1f - life - overflow);
- float f2 = normalization * END_VALUE;
- if (normalization >= 0.7f) {
- f = (normalization - 0.7f) / 0.3f;
- }
- alpha = 1f - f;
- f = bottom * f2;
- cx = baseCx + f;
- cy = (float) (baseCy - this.neg * Math.pow(f, 2.0)) - f * mag;
- radius = V + (baseRadius - V) * f2;
- }
- }
-}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/effects/ExplosionField.java b/VirtualApp/app/src/main/java/io/virtualapp/effects/ExplosionField.java
deleted file mode 100644
index 3171091f0..000000000
--- a/VirtualApp/app/src/main/java/io/virtualapp/effects/ExplosionField.java
+++ /dev/null
@@ -1,171 +0,0 @@
-package io.virtualapp.effects;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Random;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.app.Activity;
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.Window;
-import android.widget.ImageView;
-
-import io.virtualapp.VApp;
-import io.virtualapp.abs.ui.VUiKit;
-
-public class ExplosionField extends View {
-
- private static final Canvas sCanvas = new Canvas();
- private List mExplosions = new ArrayList<>();
- private int[] mExpandInset = new int[2];
-
- public ExplosionField(Context context) {
- super(context);
- init();
- }
-
- public ExplosionField(Context context, AttributeSet attrs) {
- super(context, attrs);
- init();
- }
-
- public ExplosionField(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- init();
- }
-
- public static Bitmap createBitmapFromView(View view) {
- if (view instanceof ImageView) {
- Drawable drawable = ((ImageView) view).getDrawable();
- if (drawable != null && drawable instanceof BitmapDrawable) {
- return ((BitmapDrawable) drawable).getBitmap();
- }
- }
- view.clearFocus();
- Bitmap bitmap = createBitmapSafely(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888, 1);
- if (bitmap != null) {
- synchronized (sCanvas) {
- Canvas canvas = sCanvas;
- canvas.setBitmap(bitmap);
- view.draw(canvas);
- canvas.setBitmap(null);
- }
- }
- return bitmap;
- }
-
- public static Bitmap createBitmapSafely(int width, int height, Bitmap.Config config, int retryCount) {
- try {
- return Bitmap.createBitmap(width, height, config);
- } catch (OutOfMemoryError e) {
- e.printStackTrace();
- if (retryCount > 0) {
- System.gc();
- return createBitmapSafely(width, height, config, retryCount - 1);
- }
- return null;
- }
- }
-
- public static ExplosionField attachToWindow(Activity activity) {
- ViewGroup rootView = (ViewGroup) activity.findViewById(Window.ID_ANDROID_CONTENT);
- ExplosionField explosionField = new ExplosionField(activity);
- rootView.addView(explosionField,
- new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
- return explosionField;
- }
-
- public static ExplosionField attachToWindow(ViewGroup rootView, Activity activity) {
- ExplosionField explosionField = new ExplosionField(activity);
- rootView.addView(explosionField,
- new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
- return explosionField;
- }
-
- private void init() {
- Arrays.fill(mExpandInset, VUiKit.dpToPx(VApp.getApp(), 32));
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- for (ExplosionAnimator explosion : mExplosions) {
- explosion.draw(canvas);
- }
- }
-
- public void expandExplosionBound(int dx, int dy) {
- mExpandInset[0] = dx;
- mExpandInset[1] = dy;
- }
-
- public void explode(Bitmap bitmap, Rect bound, long startDelay, long duration) {
- final ExplosionAnimator explosion = new ExplosionAnimator(this, bitmap, bound);
- explosion.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- mExplosions.remove(animation);
- }
- });
- explosion.setStartDelay(startDelay);
- explosion.setDuration(duration);
- mExplosions.add(explosion);
- explosion.start();
- }
-
- public void explode(final View view) {
- explode(view, null);
- }
-
- public void explode(final View view, OnExplodeFinishListener listener) {
- Rect r = new Rect();
- view.getGlobalVisibleRect(r);
- int[] location = new int[2];
- getLocationOnScreen(location);
- r.offset(-location[0], -location[1]);
- r.inset(-mExpandInset[0], -mExpandInset[1]);
- int startDelay = 100;
- ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f).setDuration(150);
- animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
- Random random = new Random();
-
- @Override
- public void onAnimationUpdate(ValueAnimator animation) {
- view.setTranslationX((random.nextFloat() - 0.5f) * view.getWidth() * 0.05f);
- view.setTranslationY((random.nextFloat() - 0.5f) * view.getHeight() * 0.05f);
- }
-
- });
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- if (listener != null) {
- listener.onExplodeFinish(view);
- }
- }
- });
- animator.start();
- view.animate().setDuration(150).setStartDelay(startDelay).scaleX(0f).scaleY(0f).alpha(0f).start();
- explode(createBitmapFromView(view), r, startDelay, ExplosionAnimator.DEFAULT_DURATION);
- }
-
- public void clear() {
- mExplosions.clear();
- invalidate();
- }
-
- public interface OnExplodeFinishListener {
- void onExplodeFinish(View v);
- }
-}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/glide/GlideUtils.java b/VirtualApp/app/src/main/java/io/virtualapp/glide/GlideUtils.java
new file mode 100644
index 000000000..ad7b85ab3
--- /dev/null
+++ b/VirtualApp/app/src/main/java/io/virtualapp/glide/GlideUtils.java
@@ -0,0 +1,33 @@
+package io.virtualapp.glide;
+
+import android.content.Context;
+import android.support.annotation.DrawableRes;
+import android.widget.ImageView;
+
+import com.bumptech.glide.load.engine.DiskCacheStrategy;
+
+import static io.virtualapp.glide.PackageIconResourceLoader.DATA_PACKAGE_FILE_PATH_PREFIX;
+import static io.virtualapp.glide.PackageIconResourceLoader.DATA_PACKAGE_PREFIX;
+
+/**
+ * Created by Windy on 2018/10/25
+ */
+public class GlideUtils {
+
+ public static void loadInstalledPackageIcon(Context context, String packageName, ImageView target, @DrawableRes int placeHolder) {
+ GlideApp.with(context)
+ .load(DATA_PACKAGE_PREFIX + packageName)
+ .placeholder(placeHolder)
+ .diskCacheStrategy(DiskCacheStrategy.NONE)
+ .into(target);
+ }
+
+ public static void loadPackageIconFromApkFile(Context context, String apkFilePath, ImageView target, @DrawableRes int placeHolder) {
+ GlideApp.with(context)
+ .load(DATA_PACKAGE_FILE_PATH_PREFIX + apkFilePath)
+ .placeholder(placeHolder)
+ .diskCacheStrategy(DiskCacheStrategy.NONE)
+ .into(target);
+ }
+
+}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/glide/MyGlideModule.java b/VirtualApp/app/src/main/java/io/virtualapp/glide/MyGlideModule.java
new file mode 100644
index 000000000..2b6ad9f81
--- /dev/null
+++ b/VirtualApp/app/src/main/java/io/virtualapp/glide/MyGlideModule.java
@@ -0,0 +1,40 @@
+package io.virtualapp.glide;
+
+import android.content.Context;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.GlideBuilder;
+import com.bumptech.glide.Registry;
+import com.bumptech.glide.annotation.GlideModule;
+import com.bumptech.glide.load.engine.cache.LruResourceCache;
+import com.bumptech.glide.load.engine.cache.MemorySizeCalculator;
+import com.bumptech.glide.module.AppGlideModule;
+import com.lody.virtual.helper.utils.VLog;
+
+import java.io.InputStream;
+
+/**
+ * Created by Windy on 2018/10/25
+ */
+@GlideModule
+public class MyGlideModule extends AppGlideModule {
+ @Override
+ public void applyOptions(Context context, GlideBuilder builder) {
+ MemorySizeCalculator calculator = new MemorySizeCalculator.Builder(context)
+ .build();
+ builder.setMemoryCache(new LruResourceCache(calculator.getMemoryCacheSize() / 2));
+
+ VLog.i("MyGlideModule", "applyOptions");
+ }
+
+ @Override
+ public boolean isManifestParsingEnabled() {
+ return false;
+ }
+
+ @Override
+ public void registerComponents(Context context, Glide glide, Registry registry) {
+ super.registerComponents(context, glide, registry);
+ registry.prepend(String.class, InputStream.class, new PackageIconResourceLoaderFactory(context));
+ }
+}
\ No newline at end of file
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/glide/PackageIconResourceDataFetcher.java b/VirtualApp/app/src/main/java/io/virtualapp/glide/PackageIconResourceDataFetcher.java
new file mode 100644
index 000000000..312f60b4d
--- /dev/null
+++ b/VirtualApp/app/src/main/java/io/virtualapp/glide/PackageIconResourceDataFetcher.java
@@ -0,0 +1,141 @@
+package io.virtualapp.glide;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.NonNull;
+
+import com.bumptech.glide.Priority;
+import com.bumptech.glide.load.DataSource;
+import com.bumptech.glide.load.data.DataFetcher;
+import com.lody.virtual.helper.utils.VLog;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import static io.virtualapp.glide.PackageIconResourceLoader.DATA_PACKAGE_FILE_PATH_PREFIX;
+import static io.virtualapp.glide.PackageIconResourceLoader.DATA_PACKAGE_PREFIX;
+
+/**
+ * Created by Windy on 2018/10/25
+ */
+public class PackageIconResourceDataFetcher implements DataFetcher {
+
+ private static final String TAG = PackageIconResourceDataFetcher.class.getSimpleName();
+
+ private Context context;
+ private String packageModel;
+
+ private InputStream data;
+
+ public PackageIconResourceDataFetcher(Context context, String packageName) {
+ this.context = context.getApplicationContext();
+ this.packageModel = packageName;
+ }
+
+ @Override
+ public void loadData(@NonNull Priority priority, @NonNull DataCallback super InputStream> callback) {
+ try {
+ data = loadResource();
+ } catch (Exception e) {
+ VLog.e(TAG, "Failed to load data from asset manager", e);
+ callback.onLoadFailed(e);
+ return;
+ }
+ callback.onDataReady(data);
+ }
+
+ @Override
+ public void cleanup() {
+ if (data == null) {
+ return;
+ }
+ try {
+ data.close();
+ } catch (IOException e) {
+ // Ignored.
+ }
+ }
+
+ @Override
+ public void cancel() {
+
+ }
+
+ @NonNull
+ @Override
+ public Class getDataClass() {
+ return InputStream.class;
+ }
+
+ @NonNull
+ @Override
+ public DataSource getDataSource() {
+ return DataSource.LOCAL;
+ }
+
+ //load icon res accord to package name, or apk path
+ private InputStream loadResource() {
+ PackageInfo packageInfo = null;
+ Drawable drawable = null;
+ try {
+ packageInfo = getPackageInfo();
+ if (packageInfo == null) {
+ return null;
+ }
+
+ drawable = packageInfo.applicationInfo.loadIcon(context.getPackageManager());
+ } catch (PackageManager.NameNotFoundException e) {
+ e.printStackTrace();
+ }
+ if (drawable == null) {
+ return null;
+ }
+ return drawableToInputStream(drawable);
+ }
+
+ private PackageInfo getPackageInfo() throws PackageManager.NameNotFoundException {
+ if (packageModel.startsWith(DATA_PACKAGE_PREFIX)) {
+ return context.getPackageManager().getPackageInfo(getPackageTrueModel(DATA_PACKAGE_PREFIX), 0);
+ } else if (packageModel.startsWith(DATA_PACKAGE_FILE_PATH_PREFIX)) {
+ return context.getPackageManager().getPackageArchiveInfo(getPackageTrueModel(DATA_PACKAGE_FILE_PATH_PREFIX), 0);
+ }
+ return null;
+ }
+
+ private String getPackageTrueModel(String prefix) {
+ return packageModel.replaceAll(prefix, "");
+ }
+
+ private InputStream drawableToInputStream(Drawable drawable) {
+ Bitmap bitmap = drawableToBitmap(drawable);
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); //use the compression format of your need
+ return new ByteArrayInputStream(stream.toByteArray());
+ }
+
+ private static Bitmap drawableToBitmap(Drawable drawable) {
+ if (drawable instanceof BitmapDrawable) {
+ return ((BitmapDrawable) drawable).getBitmap();
+ }
+
+ int width = drawable.getIntrinsicWidth();
+ width = width > 0 ? width : 1;
+ int height = drawable.getIntrinsicHeight();
+ height = height > 0 ? height : 1;
+
+ Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ drawable.draw(canvas);
+
+ return bitmap;
+ }
+
+}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/glide/PackageIconResourceLoader.java b/VirtualApp/app/src/main/java/io/virtualapp/glide/PackageIconResourceLoader.java
new file mode 100644
index 000000000..23e6e2e71
--- /dev/null
+++ b/VirtualApp/app/src/main/java/io/virtualapp/glide/PackageIconResourceLoader.java
@@ -0,0 +1,38 @@
+package io.virtualapp.glide;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import com.bumptech.glide.load.Options;
+import com.bumptech.glide.load.model.ModelLoader;
+import com.bumptech.glide.signature.ObjectKey;
+
+import java.io.InputStream;
+
+/**
+ * Created by Windy on 2018/10/25
+ */
+public class PackageIconResourceLoader implements ModelLoader {
+
+ public static final String DATA_PACKAGE_PREFIX = "data:packageName/";
+ public static final String DATA_PACKAGE_FILE_PATH_PREFIX = "data:packageFilePath/";
+
+ private Context context;
+
+
+ public PackageIconResourceLoader(Context context) {
+ this.context = context;
+ }
+
+ @Nullable
+ @Override
+ public LoadData buildLoadData(@NonNull String model, int width, int height, @NonNull Options options) {
+ return new LoadData<>(new ObjectKey(model), new PackageIconResourceDataFetcher(context, model));
+ }
+
+ @Override
+ public boolean handles(@NonNull String model) {
+ return model.startsWith(DATA_PACKAGE_PREFIX) || model.startsWith(DATA_PACKAGE_FILE_PATH_PREFIX);
+ }
+}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/glide/PackageIconResourceLoaderFactory.java b/VirtualApp/app/src/main/java/io/virtualapp/glide/PackageIconResourceLoaderFactory.java
new file mode 100644
index 000000000..a96734d6d
--- /dev/null
+++ b/VirtualApp/app/src/main/java/io/virtualapp/glide/PackageIconResourceLoaderFactory.java
@@ -0,0 +1,33 @@
+package io.virtualapp.glide;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+
+import com.bumptech.glide.load.model.ModelLoader;
+import com.bumptech.glide.load.model.ModelLoaderFactory;
+import com.bumptech.glide.load.model.MultiModelLoaderFactory;
+
+import java.io.InputStream;
+
+/**
+ * Created by Windy on 2018/10/25
+ */
+public class PackageIconResourceLoaderFactory implements ModelLoaderFactory {
+
+ private Context context;
+
+ public PackageIconResourceLoaderFactory(Context context) {
+ this.context = context;
+ }
+
+ @NonNull
+ @Override
+ public ModelLoader build(@NonNull MultiModelLoaderFactory multiFactory) {
+ return new PackageIconResourceLoader(context);
+ }
+
+ @Override
+ public void teardown() {
+
+ }
+}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/gms/FakeGms.java b/VirtualApp/app/src/main/java/io/virtualapp/gms/FakeGms.java
new file mode 100644
index 000000000..f54e61d98
--- /dev/null
+++ b/VirtualApp/app/src/main/java/io/virtualapp/gms/FakeGms.java
@@ -0,0 +1,422 @@
+package io.virtualapp.gms;
+
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.support.v7.app.AlertDialog;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.lody.virtual.client.core.InstallStrategy;
+import com.lody.virtual.client.core.VirtualCore;
+import com.lody.virtual.os.VEnvironment;
+import com.lody.virtual.remote.InstallResult;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import io.virtualapp.R;
+import io.virtualapp.abs.ui.VUiKit;
+import io.virtualapp.utils.DialogUtil;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+
+/**
+ * @author weishu
+ * @date 2018/6/9.
+ */
+public class FakeGms {
+
+ private static final String TAG = "FakeGms";
+
+ private static final String GMS_CONFIG_URL = "http://vaexposed.weishu.me/gms.json";
+
+ private static final String GMS_PKG = "com.google.android.gms";
+ private static final String GSF_PKG = "com.google.android.gsf";
+ private static final String STORE_PKG = "com.android.vending";
+ private static final String FAKE_GAPPS_PKG = "com.thermatk.android.xf.fakegapps";
+
+ private static ExecutorService executorService = Executors.newSingleThreadExecutor();
+
+ public static void uninstallGms(Activity activity) {
+ if (activity == null) {
+ return;
+ }
+
+ AlertDialog failDialog = new AlertDialog.Builder(activity, R.style.Theme_AppCompat_DayNight_Dialog_Alert)
+ .setTitle(R.string.uninstall_gms_title)
+ .setMessage(R.string.uninstall_gms_content)
+ .setPositiveButton(R.string.uninstall_gms_ok, ((dialog1, which1) -> {
+ ProgressDialog dialog = new ProgressDialog(activity);
+ dialog.show();
+ VUiKit.defer().when(() -> {
+ VirtualCore.get().uninstallPackage(GMS_PKG);
+ VirtualCore.get().uninstallPackage(GSF_PKG);
+ VirtualCore.get().uninstallPackage(STORE_PKG);
+ VirtualCore.get().uninstallPackage(FAKE_GAPPS_PKG);
+ }).then((v) -> {
+ dialog.dismiss();
+ AlertDialog hits = new AlertDialog.Builder(activity, R.style.Theme_AppCompat_DayNight_Dialog_Alert)
+ .setTitle(R.string.uninstall_gms_title)
+ .setMessage(R.string.uninstall_gms_success)
+ .setPositiveButton(android.R.string.ok, null)
+ .create();
+ DialogUtil.showDialog(hits);
+
+ }).fail((v) -> {
+ dialog.dismiss();
+ });
+
+ }))
+ .setNegativeButton(android.R.string.cancel, null)
+ .create();
+ DialogUtil.showDialog(failDialog);
+ }
+
+ public static boolean isAlreadyInstalled(Context context) {
+ if (context == null) {
+ return false;
+ }
+
+ boolean alreadyInstalled = true;
+ if (!VirtualCore.get().isAppInstalled(GMS_PKG)) {
+ alreadyInstalled = false;
+ }
+ if (!VirtualCore.get().isAppInstalled(GSF_PKG)) {
+ alreadyInstalled = false;
+ }
+ if (!VirtualCore.get().isAppInstalled(STORE_PKG)) {
+ alreadyInstalled = false;
+ }
+ if (!VirtualCore.get().isAppInstalled(FAKE_GAPPS_PKG)) {
+ alreadyInstalled = false;
+ }
+ return alreadyInstalled;
+ }
+
+ public static void installGms(Activity activity) {
+
+ if (activity == null) {
+ return;
+ }
+
+ AlertDialog alertDialog = new AlertDialog.Builder(activity, R.style.Theme_AppCompat_DayNight_Dialog_Alert)
+ .setTitle(R.string.install_gms_title)
+ .setMessage(R.string.install_gms_content)
+ .setPositiveButton(android.R.string.ok, ((dialog, which) -> {
+ // show a loading dialog and start install gms.
+
+ ProgressDialog progressDialog = new ProgressDialog(activity);
+ progressDialog.setCancelable(false);
+ progressDialog.show();
+
+ executorService.submit(() -> {
+ String failMsg = installGmsInternal(activity, progressDialog);
+ Log.i(TAG, "install gms result: " + failMsg);
+ try {
+ progressDialog.dismiss();
+ } catch (Throwable e) {
+ e.printStackTrace();
+ }
+
+ if (failMsg == null) {
+ activity.runOnUiThread(() -> {
+ AlertDialog failDialog = new AlertDialog.Builder(activity, R.style.Theme_AppCompat_DayNight_Dialog_Alert)
+ .setTitle(R.string.install_gms_title)
+ .setMessage(R.string.install_gms_success)
+ .setPositiveButton(android.R.string.ok, null)
+ .create();
+ DialogUtil.showDialog(failDialog);
+ });
+ } else {
+ activity.runOnUiThread(() -> {
+ AlertDialog failDialog = new AlertDialog.Builder(activity, R.style.Theme_AppCompat_DayNight_Dialog_Alert)
+ .setTitle(R.string.install_gms_fail_title)
+ .setMessage(R.string.install_gms_fail_content)
+ .setPositiveButton(R.string.install_gms_fail_ok, ((dialog1, which1) -> {
+ try {
+ Intent t = new Intent(Intent.ACTION_VIEW);
+ t.setData(Uri.parse("https://github.com/android-hacker/VirtualXposed/wiki/Google-service-support"));
+ activity.startActivity(t);
+ } catch (Throwable ignored) {
+ ignored.printStackTrace();
+ }
+ }))
+ .setNegativeButton(android.R.string.cancel, null)
+ .create();
+ DialogUtil.showDialog(failDialog);
+ });
+
+ }
+ });
+ }))
+ .setNegativeButton(android.R.string.cancel, null)
+ .create();
+
+ DialogUtil.showDialog(alertDialog);
+ }
+
+
+ private static String installGmsInternal(Activity activity, ProgressDialog dialog) {
+ File cacheDir = activity.getCacheDir();
+
+ // 下载配置文件,得到各自的URL
+ OkHttpClient client = new OkHttpClient.Builder()
+ .connectTimeout(30, TimeUnit.SECONDS)
+ .readTimeout(30, TimeUnit.SECONDS)
+ .writeTimeout(30, TimeUnit.SECONDS)
+ .build();
+
+ Request request = new Request.Builder()
+ .url(GMS_CONFIG_URL)
+ .build();
+
+ updateMessage(activity, dialog, "Fetching gms config...");
+ Response response;
+ try {
+ response = client.newCall(request).execute();
+ } catch (IOException e) {
+ return "Download gms config failed, please check your network, error: 0";
+ }
+
+ if (!response.isSuccessful()) {
+ return "Download gms config failed, please check your network, error: 1";
+ }
+
+ Log.i(TAG, "response success: " + response.code());
+ if (200 != response.code()) {
+ return "Download gms config failed, please check your network, error: 2";
+ }
+
+ updateMessage(activity, dialog, "Parsing gms config...");
+ ResponseBody body = response.body();
+ if (body == null) {
+ return "Download gms config failed, please check your network, error: 3";
+ }
+
+ String string = null;
+ try {
+ string = body.string();
+ } catch (IOException e) {
+ return "Download gms config failed, please check your network, error: 4";
+ }
+
+ JSONObject jsonObject = null;
+ try {
+ jsonObject = new JSONObject(string);
+ } catch (JSONException e) {
+ return "Download gms config failed, please check your network, error: 5";
+ }
+ String gmsCoreUrl = null;
+ try {
+ gmsCoreUrl = jsonObject.getString("gms");
+ } catch (JSONException e) {
+ return "Download gms config failed, please check your network, error: 6";
+ }
+ String gmsServiceUrl = null;
+ try {
+ gmsServiceUrl = jsonObject.getString("gsf");
+ } catch (JSONException e) {
+ return "Download gms config failed, please check your network, error: 7";
+ }
+ String storeUrl = null;
+ try {
+ storeUrl = jsonObject.getString("store");
+ } catch (JSONException e) {
+ return "Download gms config failed, please check your network, error: 8";
+ }
+ String fakeGappsUrl = null;
+ try {
+ fakeGappsUrl = jsonObject.getString("fakegapps");
+ } catch (JSONException e) {
+ return "Download gms config failed, please check your network, error: 9";
+ }
+
+ String yalpStoreUrl = null;
+ try {
+ yalpStoreUrl = jsonObject.getString("yalp");
+ } catch (JSONException e) {
+ // ignore.
+ Log.i(TAG, "Download gms config failed, please check your network");
+ }
+
+ updateMessage(activity, dialog, "config parse success!");
+
+ File gmsCoreFile = new File(cacheDir, "gms.apk");
+ File gmsServiceFile = new File(cacheDir, "gsf.apk");
+ File storeFile = new File(cacheDir, "store.apk");
+ File fakeGappsFile = new File(cacheDir, "fakegapps.apk");
+ File yalpStoreFile = new File(cacheDir, "yalpStore.apk");
+
+ // clear old files.
+ if (gmsCoreFile.exists()) {
+ gmsCoreFile.delete();
+ }
+ if (gmsServiceFile.exists()) {
+ gmsServiceFile.delete();
+ }
+ if (storeFile.exists()) {
+ storeFile.delete();
+ }
+ if (fakeGappsFile.exists()) {
+ fakeGappsFile.delete();
+ }
+
+ boolean downloadResult = downloadFile(gmsCoreUrl, gmsCoreFile,
+ (progress) -> updateMessage(activity, dialog, "download gms core..." + progress + "%"));
+ if (!downloadResult) {
+ return "Download gms config failed, please check your network, error: 10";
+ }
+
+ downloadResult = downloadFile(gmsServiceUrl, gmsServiceFile,
+ (progress -> updateMessage(activity, dialog, "download gms service framework proxy.." + progress + "%")));
+
+ if (!downloadResult) {
+ return "Download gms config failed, please check your network, error: 11";
+ }
+
+ updateMessage(activity, dialog, "download gms store...");
+
+ downloadResult = downloadFile(storeUrl, storeFile,
+ (progress -> updateMessage(activity, dialog, "download gms store.." + progress + "%")));
+ if (!downloadResult) {
+ return "Download gms config failed, please check your network, error: 12";
+ }
+
+ downloadResult = downloadFile(fakeGappsUrl, fakeGappsFile,
+ (progress -> updateMessage(activity, dialog, "download gms Xposed module.." + progress + "%")));
+ if (!downloadResult) {
+ return "Download gms config failed, please check your network, error: 13";
+ }
+
+ if (yalpStoreUrl != null) {
+ downloadFile(yalpStoreUrl,yalpStoreFile,
+ (progress -> updateMessage(activity, dialog, "download yalp store.." + progress + "%")));
+ }
+
+ updateMessage(activity, dialog, "installing gms core");
+ InstallResult installResult = VirtualCore.get().installPackage(gmsCoreFile.getAbsolutePath(), InstallStrategy.UPDATE_IF_EXIST);
+
+ if (!installResult.isSuccess) {
+ return "install gms core failed: " + installResult.error;
+ }
+
+ updateMessage(activity, dialog, "installing gms service framework...");
+ installResult = VirtualCore.get().installPackage(gmsServiceFile.getAbsolutePath(), InstallStrategy.UPDATE_IF_EXIST);
+ if (!installResult.isSuccess) {
+ return "install gms service framework failed: " + installResult.error;
+ }
+
+ updateMessage(activity, dialog, "installing gms store...");
+ installResult = VirtualCore.get().installPackage(storeFile.getAbsolutePath(), InstallStrategy.UPDATE_IF_EXIST);
+ if (!installResult.isSuccess) {
+ return "install gms store failed: " + installResult.error;
+ }
+
+ updateMessage(activity, dialog, "installing gms Xposed module...");
+ installResult = VirtualCore.get().installPackage(fakeGappsFile.getAbsolutePath(), InstallStrategy.UPDATE_IF_EXIST);
+ if (!installResult.isSuccess) {
+ return "install gms xposed module failed: " + installResult.error;
+ }
+
+ if (yalpStoreFile.exists()) {
+ updateMessage(activity, dialog, "installing yalp store...");
+ VirtualCore.get().installPackage(yalpStoreFile.getAbsolutePath(), InstallStrategy.UPDATE_IF_EXIST);
+ }
+
+ // Enable the Xposed module.
+ File dataDir = VEnvironment.getDataUserPackageDirectory(0, "de.robv.android.xposed.installer");
+ File modulePath = VEnvironment.getPackageResourcePath(FAKE_GAPPS_PKG);
+ File configDir = new File(dataDir, "exposed_conf" + File.separator + "modules.list");
+ FileWriter writer = null;
+ try {
+ writer = new FileWriter(configDir, true);
+ writer.append(modulePath.getAbsolutePath());
+ writer.flush();
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ if (writer != null) {
+ try {
+ writer.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ // success!!!
+ return null;
+ }
+
+ private static void updateMessage(Activity activity, ProgressDialog dialog, String msg) {
+ if (activity == null || dialog == null || TextUtils.isEmpty(msg)) {
+ return;
+ }
+ Log.i(TAG, "update dialog message: " + msg);
+ activity.runOnUiThread(() -> {
+ dialog.setMessage(msg);
+ });
+ }
+
+ public interface DownloadListener {
+ void onProgress(int progress);
+ }
+
+ public static boolean downloadFile(String url, File outFile, DownloadListener listener) {
+ OkHttpClient client = new OkHttpClient();
+ Request request = new Request.Builder().url(url).build();
+ FileOutputStream fos = null;
+ try {
+ Response response = client.newCall(request).execute();
+ if (response.code() != 200) {
+ return false;
+ }
+ ResponseBody body = response.body();
+ if (body == null) {
+ return false;
+ }
+ long toal = body.contentLength();
+ long sum = 0;
+
+ InputStream inputStream = body.byteStream();
+ fos = new FileOutputStream(outFile);
+ byte[] buffer = new byte[1024];
+ int count = 0;
+ while ((count = inputStream.read(buffer)) >= 0) {
+ fos.write(buffer, 0, count);
+ sum += count;
+ int progress = (int) ((sum * 1.0) / toal * 100);
+ if (listener != null) {
+ listener.onProgress(progress);
+ }
+ }
+ fos.flush();
+ return true;
+ } catch (IOException e) {
+ return false;
+ } finally {
+ if (fos != null) {
+ try {
+ fos.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ }
+}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/home/FlurryROMCollector.java b/VirtualApp/app/src/main/java/io/virtualapp/home/FlurryROMCollector.java
index d184ffe12..70dfdf8bc 100644
--- a/VirtualApp/app/src/main/java/io/virtualapp/home/FlurryROMCollector.java
+++ b/VirtualApp/app/src/main/java/io/virtualapp/home/FlurryROMCollector.java
@@ -4,8 +4,8 @@
import android.os.Build;
import android.util.Log;
-import com.flurry.android.FlurryAgent;
-import com.flurry.android.FlurryEventRecordStatus;
+import com.crashlytics.android.answers.Answers;
+import com.crashlytics.android.answers.CustomEvent;
import com.lody.virtual.client.natives.NativeMethods;
import com.lody.virtual.helper.utils.Reflect;
@@ -21,21 +21,33 @@ public class FlurryROMCollector {
private static final String TAG = FlurryROMCollector.class.getSimpleName();
public static void startCollect() {
- Log.d(TAG, "start collect...");
+ Log.i(TAG, "start collect...");
NativeMethods.init();
if (NativeMethods.gCameraNativeSetup == null) {
reportCameraNativeSetup();
}
- Log.d(TAG, "end collect...");
+ Log.i(TAG, "end collect...");
}
+ private static void reportOffsetInfo(Map info) {
+ Map toReport = new HashMap<>();
+ CustomEvent methodOffset = new CustomEvent("methodOffset");
+ for (String key : info.keySet()) {
+ toReport.put(key, String.valueOf(info.get(key)));
+ }
+ addRomInfo(toReport);
+ Answers.getInstance().logCustom(methodOffset);
+ }
private static void reportCameraNativeSetup() {
for (Method method : Camera.class.getDeclaredMethods()) {
if ("native_setup".equals(method.getName())) {
- FlurryEventRecordStatus status =
- FlurryAgent.logEvent("camera::native_setup", createLogContent("method_details", Reflect.getMethodDetails(method)));
- Log.d(TAG, "report CNS: " + status);
+ CustomEvent cameraSetup = new CustomEvent("camera::native_setup");
+ Map methodDetails = createLogContent("method_details", Reflect.getMethodDetails(method));
+ for (String key : methodDetails.keySet()) {
+ cameraSetup.putCustomAttribute(key, methodDetails.get(key));
+ }
+ Answers.getInstance().logCustom(cameraSetup);
break;
}
}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/home/HomeActivity.java b/VirtualApp/app/src/main/java/io/virtualapp/home/HomeActivity.java
deleted file mode 100644
index 626cbb282..000000000
--- a/VirtualApp/app/src/main/java/io/virtualapp/home/HomeActivity.java
+++ /dev/null
@@ -1,417 +0,0 @@
-package io.virtualapp.home;
-
-import android.animation.Animator;
-import android.animation.ObjectAnimator;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
-import android.support.annotation.Nullable;
-import android.support.v7.app.AlertDialog;
-import android.support.v7.widget.OrientationHelper;
-import android.support.v7.widget.RecyclerView;
-import android.support.v7.widget.StaggeredGridLayoutManager;
-import android.support.v7.widget.helper.ItemTouchHelper;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.melnykov.fab.FloatingActionButton;
-
-import java.util.List;
-
-import io.virtualapp.R;
-import io.virtualapp.VCommends;
-import io.virtualapp.abs.nestedadapter.SmartRecyclerAdapter;
-import io.virtualapp.abs.ui.VActivity;
-import io.virtualapp.abs.ui.VUiKit;
-import io.virtualapp.home.adapters.LaunchpadAdapter;
-import io.virtualapp.home.adapters.decorations.ItemOffsetDecoration;
-import io.virtualapp.home.models.AppData;
-import io.virtualapp.home.models.AppInfoLite;
-import io.virtualapp.home.models.EmptyAppData;
-import io.virtualapp.home.models.PackageAppData;
-import io.virtualapp.widgets.CircularAnim;
-import io.virtualapp.widgets.TwoGearsView;
-
-import static android.support.v7.widget.helper.ItemTouchHelper.ACTION_STATE_DRAG;
-import static android.support.v7.widget.helper.ItemTouchHelper.DOWN;
-import static android.support.v7.widget.helper.ItemTouchHelper.END;
-import static android.support.v7.widget.helper.ItemTouchHelper.LEFT;
-import static android.support.v7.widget.helper.ItemTouchHelper.RIGHT;
-import static android.support.v7.widget.helper.ItemTouchHelper.START;
-import static android.support.v7.widget.helper.ItemTouchHelper.UP;
-
-/**
- * @author Lody
- */
-public class HomeActivity extends VActivity implements HomeContract.HomeView {
-
- private static final String TAG = HomeActivity.class.getSimpleName();
-
- private HomeContract.HomePresenter mPresenter;
- private TwoGearsView mLoadingView;
- private RecyclerView mLauncherView;
- private FloatingActionButton mFloatingButton;
- private View bottomArea;
- private View createShortcutArea;
- private View deleteAppArea;
- private LaunchpadAdapter mLaunchpadAdapter;
- private Handler mUiHandler;
-
-
- public static void goHome(Context context) {
- Intent intent = new Intent(context, HomeActivity.class);
- intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- context.startActivity(intent);
- }
-
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- overridePendingTransition(0, 0);
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_home);
- mUiHandler = new Handler(Looper.getMainLooper());
- bindViews();
- initLaunchpad();
- initFab();
- new HomePresenterImpl(this).start();
- }
-
- private void bindViews() {
- mLoadingView = (TwoGearsView) findViewById(R.id.pb_loading_app);
- mLauncherView = (RecyclerView) findViewById(R.id.home_launcher);
- mFloatingButton = (FloatingActionButton) findViewById(R.id.home_fab);
- bottomArea = findViewById(R.id.bottom_area);
- createShortcutArea = findViewById(R.id.create_shortcut_area);
- deleteAppArea = findViewById(R.id.delete_app_area);
- }
-
- private void initLaunchpad() {
- mLauncherView.setHasFixedSize(true);
- StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(3, OrientationHelper.VERTICAL);
- mLauncherView.setLayoutManager(layoutManager);
- mLaunchpadAdapter = new LaunchpadAdapter(this);
- SmartRecyclerAdapter wrap = new SmartRecyclerAdapter(mLaunchpadAdapter);
- View footer = new View(this);
- footer.setLayoutParams(new StaggeredGridLayoutManager.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, VUiKit.dpToPx(this, 60)));
- wrap.setFooterView(footer);
- mLauncherView.setAdapter(wrap);
- mLauncherView.addItemDecoration(new ItemOffsetDecoration(this, R.dimen.desktop_divider));
- ItemTouchHelper touchHelper = new ItemTouchHelper(new LauncherTouchCallback());
- touchHelper.attachToRecyclerView(mLauncherView);
- mLaunchpadAdapter.setAppClickListener((pos, data) -> {
- if (!data.isLoading()) {
- mLaunchpadAdapter.notifyItemChanged(pos);
- mPresenter.launchApp(data);
- }
- });
- }
-
- private void initFab() {
- mFloatingButton.setOnClickListener(v -> {
- CircularAnim.fullActivity(this, mFloatingButton)
- .colorOrImageRes(R.color.colorPrimaryRavel)
- .go(() -> ListAppActivity.gotoListApp(this));
- });
- }
-
- private void deleteApp(int position) {
- AppData data = mLaunchpadAdapter.getList().get(position);
- new AlertDialog.Builder(this)
- .setTitle("Delete app")
- .setMessage("Do you want to delete " + data.getName() + "?")
- .setPositiveButton(android.R.string.yes, (dialog, which) -> {
- mPresenter.deleteApp(data);
- })
- .setNegativeButton(android.R.string.no, null)
- .show();
- }
-
- private void createShortcut(int position) {
- AppData model = mLaunchpadAdapter.getList().get(position);
- if (model instanceof PackageAppData) {
- mPresenter.createShortcut(model);
- }
- }
-
- @Override
- public void setPresenter(HomeContract.HomePresenter presenter) {
- mPresenter = presenter;
- }
-
- @Override
- public void showBottomAction() {
- hideFab();
- bottomArea.setTranslationY(bottomArea.getHeight());
- bottomArea.setVisibility(View.VISIBLE);
- bottomArea.animate().translationY(0).setDuration(500L).start();
- }
-
- @Override
- public void hideBottomAction() {
- bottomArea.setTranslationY(0);
- ObjectAnimator transAnim = ObjectAnimator.ofFloat(bottomArea, "translationY", 0, bottomArea.getHeight());
- transAnim.addListener(new Animator.AnimatorListener() {
- @Override
- public void onAnimationStart(Animator animator) {
-
- }
-
- @Override
- public void onAnimationEnd(Animator animator) {
- bottomArea.setVisibility(View.GONE);
- showFab();
- }
-
- @Override
- public void onAnimationCancel(Animator animator) {
- bottomArea.setVisibility(View.GONE);
- showFab();
- }
-
- @Override
- public void onAnimationRepeat(Animator animator) {
-
- }
- });
- transAnim.setDuration(500L);
- transAnim.start();
- }
-
- @Override
- public void showLoading() {
- mFloatingButton.hide(false);
- mLoadingView.setVisibility(View.VISIBLE);
- mLoadingView.startAnim();
- }
-
- @Override
- public void hideLoading() {
- mFloatingButton.show();
- mLoadingView.setVisibility(View.GONE);
- mLoadingView.stopAnim();
- }
-
- @Override
- public void loadFinish(List list) {
- while (list.size() < 9) {
- list.add(new EmptyAppData());
- }
- mLaunchpadAdapter.setList(list);
- hideLoading();
- }
-
- @Override
- public void loadError(Throwable err) {
- err.printStackTrace();
- hideLoading();
- }
-
- @Override
- public void showGuide() {
-
- }
-
- @Override
- public void addAppToLauncher(AppData model) {
- List dataList = mLaunchpadAdapter.getList();
- boolean replaced = false;
- for (int i = 0; i < dataList.size(); i++) {
- AppData data = dataList.get(i);
- if (data instanceof EmptyAppData) {
- mLaunchpadAdapter.replace(i, model);
- replaced = true;
- break;
- }
- }
- if (!replaced) {
- mLaunchpadAdapter.add(model);
- mLauncherView.smoothScrollToPosition(mLaunchpadAdapter.getItemCount() - 1);
- }
- }
-
-
- @Override
- public void removeAppToLauncher(AppData model) {
- mLaunchpadAdapter.remove(model);
- }
-
- @Override
- public void refreshLauncherItem(AppData model) {
- mLaunchpadAdapter.refresh(model);
- }
-
- @Override
- public void showFab() {
- mFloatingButton.show();
- }
-
- @Override
- public void hideFab() {
- mFloatingButton.hide();
- }
-
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
- if (resultCode == RESULT_OK && data != null) {
- List appList = data.getParcelableArrayListExtra(VCommends.EXTRA_APP_INFO_LIST);
- if (appList != null) {
- for (AppInfoLite info : appList) {
- mPresenter.addApp(info);
- }
- }
- }
- }
-
- private class LauncherTouchCallback extends ItemTouchHelper.SimpleCallback {
-
- int[] location = new int[2];
- boolean upAtDeleteAppArea;
- boolean upAtCreateShortcutArea;
- RecyclerView.ViewHolder dragHolder;
-
- LauncherTouchCallback() {
- super(UP | DOWN | LEFT | RIGHT | START | END, 0);
- }
-
- @Override
- public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize, int viewSizeOutOfBounds, int totalSize, long msSinceStartScroll) {
- return 0;
- }
-
- @Override
- public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
- try {
- AppData data = mLaunchpadAdapter.getList().get(viewHolder.getAdapterPosition());
- if (!data.canReorder()) {
- return makeMovementFlags(0, 0);
- }
- } catch (IndexOutOfBoundsException e) {
- e.printStackTrace();
- }
- return super.getMovementFlags(recyclerView, viewHolder);
- }
-
- @Override
- public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
- int pos = viewHolder.getAdapterPosition();
- int targetPos = target.getAdapterPosition();
- mLaunchpadAdapter.moveItem(pos, targetPos);
- return true;
- }
-
- @Override
- public boolean isLongPressDragEnabled() {
- return true;
- }
-
- @Override
- public boolean isItemViewSwipeEnabled() {
- return false;
- }
-
- @Override
- public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
- if (viewHolder instanceof LaunchpadAdapter.ViewHolder) {
- if (actionState == ACTION_STATE_DRAG) {
- if (dragHolder != viewHolder) {
- dragHolder = viewHolder;
- viewHolder.itemView.setScaleX(1.2f);
- viewHolder.itemView.setScaleY(1.2f);
- if (bottomArea.getVisibility() == View.GONE) {
- showBottomAction();
- }
- }
- }
- }
- super.onSelectedChanged(viewHolder, actionState);
- }
-
- @Override
- public boolean canDropOver(RecyclerView recyclerView, RecyclerView.ViewHolder current, RecyclerView.ViewHolder target) {
- if (upAtCreateShortcutArea || upAtDeleteAppArea) {
- return false;
- }
- try {
- AppData data = mLaunchpadAdapter.getList().get(target.getAdapterPosition());
- return data.canReorder();
- } catch (IndexOutOfBoundsException e) {
- e.printStackTrace();
- }
- return false;
- }
-
- @Override
- public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
- if (viewHolder instanceof LaunchpadAdapter.ViewHolder) {
- LaunchpadAdapter.ViewHolder holder = (LaunchpadAdapter.ViewHolder) viewHolder;
- viewHolder.itemView.setScaleX(1f);
- viewHolder.itemView.setScaleY(1f);
- viewHolder.itemView.setBackgroundColor(holder.color);
- }
- super.clearView(recyclerView, viewHolder);
- if (dragHolder == viewHolder) {
- if (bottomArea.getVisibility() == View.VISIBLE) {
- mUiHandler.postDelayed(HomeActivity.this::hideBottomAction, 200L);
- if (upAtCreateShortcutArea) {
- createShortcut(viewHolder.getAdapterPosition());
- } else if (upAtDeleteAppArea) {
- deleteApp(viewHolder.getAdapterPosition());
- }
- }
- dragHolder = null;
- }
- }
-
-
- @Override
- public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
- }
-
- @Override
- public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
- super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
- if (actionState != ACTION_STATE_DRAG || !isCurrentlyActive) {
- return;
- }
- View itemView = viewHolder.itemView;
- itemView.getLocationInWindow(location);
- int x = (int) (location[0] + dX);
- int y = (int) (location[1] + dY);
-
- bottomArea.getLocationInWindow(location);
- int baseLine = location[1] - bottomArea.getHeight();
- if (y >= baseLine) {
- deleteAppArea.getLocationInWindow(location);
- int deleteAppAreaStartX = location[0];
- if (x < deleteAppAreaStartX) {
- upAtCreateShortcutArea = true;
- upAtDeleteAppArea = false;
- createShortcutArea.setBackgroundColor(Color.parseColor("#0099cc"));
- deleteAppArea.setBackgroundColor(Color.TRANSPARENT);
- createShortcutArea.setAlpha(0.7f);
- deleteAppArea.setAlpha(1f);
- } else {
- upAtDeleteAppArea = true;
- upAtCreateShortcutArea = false;
- deleteAppArea.setBackgroundColor(Color.RED);
- createShortcutArea.setBackgroundColor(Color.TRANSPARENT);
- deleteAppArea.setAlpha(0.7f);
- createShortcutArea.setAlpha(1f);
- }
- } else {
- upAtCreateShortcutArea = false;
- upAtDeleteAppArea = false;
- createShortcutArea.setBackgroundColor(Color.TRANSPARENT);
- deleteAppArea.setBackgroundColor(Color.TRANSPARENT);
- createShortcutArea.setAlpha(1f);
- deleteAppArea.setAlpha(1f);
- }
- }
- }
-}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/home/HomeContract.java b/VirtualApp/app/src/main/java/io/virtualapp/home/HomeContract.java
deleted file mode 100644
index 2665c73d7..000000000
--- a/VirtualApp/app/src/main/java/io/virtualapp/home/HomeContract.java
+++ /dev/null
@@ -1,56 +0,0 @@
-package io.virtualapp.home;
-
-
-import java.util.List;
-
-import io.virtualapp.abs.BasePresenter;
-import io.virtualapp.abs.BaseView;
-import io.virtualapp.home.models.AppData;
-import io.virtualapp.home.models.AppInfoLite;
-
-/**
- * @author Lody
- */
-/* package */ class HomeContract {
-
- /* package */ interface HomeView extends BaseView {
-
- void showBottomAction();
-
- void hideBottomAction();
-
- void showLoading();
-
- void hideLoading();
-
- void loadFinish(List appModels);
-
- void loadError(Throwable err);
-
- void showGuide();
-
- void addAppToLauncher(AppData model);
-
- void showFab();
-
- void hideFab();
-
- void removeAppToLauncher(AppData model);
-
- void refreshLauncherItem(AppData model);
- }
-
- /* package */ interface HomePresenter extends BasePresenter {
-
- void launchApp(AppData data);
-
- void dataChanged();
-
- void addApp(AppInfoLite info);
-
- void deleteApp(AppData data);
-
- void createShortcut(AppData data);
- }
-
-}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/home/HomePresenterImpl.java b/VirtualApp/app/src/main/java/io/virtualapp/home/HomePresenterImpl.java
deleted file mode 100644
index 899581f0e..000000000
--- a/VirtualApp/app/src/main/java/io/virtualapp/home/HomePresenterImpl.java
+++ /dev/null
@@ -1,202 +0,0 @@
-package io.virtualapp.home;
-
-import android.app.Activity;
-import android.graphics.Bitmap;
-
-import com.lody.virtual.client.core.VirtualCore;
-import com.lody.virtual.os.VUserInfo;
-import com.lody.virtual.os.VUserManager;
-import com.lody.virtual.remote.InstallResult;
-import com.lody.virtual.remote.InstalledAppInfo;
-
-import java.io.IOException;
-
-import io.virtualapp.VCommends;
-import io.virtualapp.abs.ui.VUiKit;
-import io.virtualapp.home.models.AppData;
-import io.virtualapp.home.models.AppInfoLite;
-import io.virtualapp.home.models.MultiplePackageAppData;
-import io.virtualapp.home.models.PackageAppData;
-import io.virtualapp.home.repo.AppRepository;
-import io.virtualapp.home.repo.PackageAppDataStorage;
-import jonathanfinerty.once.Once;
-
-/**
- * @author Lody
- */
-class HomePresenterImpl implements HomeContract.HomePresenter {
-
- private HomeContract.HomeView mView;
- private Activity mActivity;
- private AppRepository mRepo;
-
- HomePresenterImpl(HomeContract.HomeView view) {
- mView = view;
- mActivity = view.getActivity();
- mRepo = new AppRepository(mActivity);
- mView.setPresenter(this);
- }
-
- @Override
- public void start() {
- dataChanged();
- if (!Once.beenDone(VCommends.TAG_SHOW_ADD_APP_GUIDE)) {
- mView.showGuide();
- Once.markDone(VCommends.TAG_SHOW_ADD_APP_GUIDE);
- }
- }
-
- @Override
- public void launchApp(AppData data) {
- try {
- if (data instanceof PackageAppData) {
- PackageAppData appData = (PackageAppData) data;
- appData.isFirstOpen = false;
- LoadingActivity.launch(mActivity, appData.packageName, 0);
- } else if (data instanceof MultiplePackageAppData) {
- MultiplePackageAppData multipleData = (MultiplePackageAppData) data;
- multipleData.isFirstOpen = false;
- LoadingActivity.launch(mActivity, multipleData.appInfo.packageName, ((MultiplePackageAppData) data).userId);
- }
- } catch (Throwable e) {
- e.printStackTrace();
- }
- }
-
- @Override
- public void dataChanged() {
- mView.showLoading();
- mRepo.getVirtualApps().done(mView::loadFinish).fail(mView::loadError);
- }
-
-
- @Override
- public void addApp(AppInfoLite info) {
- class AddResult {
- private PackageAppData appData;
- private int userId;
- private boolean justEnableHidden;
- }
- AddResult addResult = new AddResult();
- VUiKit.defer().when(() -> {
- InstalledAppInfo installedAppInfo = VirtualCore.get().getInstalledAppInfo(info.packageName, 0);
- addResult.justEnableHidden = installedAppInfo != null;
- if (addResult.justEnableHidden) {
- int[] userIds = installedAppInfo.getInstalledUsers();
- int nextUserId = userIds.length;
- /*
- Input : userIds = {0, 1, 3}
- Output: nextUserId = 2
- */
- for (int i = 0; i < userIds.length; i++) {
- if (userIds[i] != i) {
- nextUserId = i;
- break;
- }
- }
- addResult.userId = nextUserId;
-
- if (VUserManager.get().getUserInfo(nextUserId) == null) {
- // user not exist, create it automatically.
- String nextUserName = "Space " + (nextUserId + 1);
- VUserInfo newUserInfo = VUserManager.get().createUser(nextUserName, VUserInfo.FLAG_ADMIN);
- if (newUserInfo == null) {
- throw new IllegalStateException();
- }
- }
- boolean success = VirtualCore.get().installPackageAsUser(nextUserId, info.packageName);
- if (!success) {
- throw new IllegalStateException();
- }
- } else {
- InstallResult res = mRepo.addVirtualApp(info);
- if (!res.isSuccess) {
- throw new IllegalStateException();
- }
- }
- }).then((res) -> {
- addResult.appData = PackageAppDataStorage.get().acquire(info.packageName);
- }).done(res -> {
- boolean multipleVersion = addResult.justEnableHidden && addResult.userId != 0;
- if (!multipleVersion) {
- PackageAppData data = addResult.appData;
- data.isLoading = true;
- mView.addAppToLauncher(data);
- handleOptApp(data, info.packageName, true);
- } else {
- MultiplePackageAppData data = new MultiplePackageAppData(addResult.appData, addResult.userId);
- data.isLoading = true;
- mView.addAppToLauncher(data);
- handleOptApp(data, info.packageName, false);
- }
- });
- }
-
-
- private void handleOptApp(AppData data, String packageName, boolean needOpt) {
- VUiKit.defer().when(() -> {
- long time = System.currentTimeMillis();
- if (needOpt) {
- try {
- VirtualCore.get().preOpt(packageName);
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- time = System.currentTimeMillis() - time;
- if (time < 1500L) {
- try {
- Thread.sleep(1500L - time);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }).done((res) -> {
- if (data instanceof PackageAppData) {
- ((PackageAppData) data).isLoading = false;
- ((PackageAppData) data).isFirstOpen = true;
- } else if (data instanceof MultiplePackageAppData) {
- ((MultiplePackageAppData) data).isLoading = false;
- ((MultiplePackageAppData) data).isFirstOpen = true;
- }
- mView.refreshLauncherItem(data);
- });
- }
-
- @Override
- public void deleteApp(AppData data) {
- try {
- mView.removeAppToLauncher(data);
- if (data instanceof PackageAppData) {
- mRepo.removeVirtualApp(((PackageAppData) data).packageName, 0);
- } else {
- MultiplePackageAppData appData = (MultiplePackageAppData) data;
- mRepo.removeVirtualApp(appData.appInfo.packageName, appData.userId);
- }
- } catch (Throwable e) {
- e.printStackTrace();
- }
- }
-
- @Override
- public void createShortcut(AppData data) {
- VirtualCore.OnEmitShortcutListener listener = new VirtualCore.OnEmitShortcutListener() {
- @Override
- public Bitmap getIcon(Bitmap originIcon) {
- return originIcon;
- }
-
- @Override
- public String getName(String originName) {
- return originName + "(VA)";
- }
- };
- if (data instanceof PackageAppData) {
- VirtualCore.get().createShortcut(0, ((PackageAppData) data).packageName, listener);
- } else if (data instanceof MultiplePackageAppData) {
- MultiplePackageAppData appData = (MultiplePackageAppData) data;
- VirtualCore.get().createShortcut(appData.userId, appData.appInfo.packageName, listener);
- }
- }
-
-}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/home/ListAppActivity.java b/VirtualApp/app/src/main/java/io/virtualapp/home/ListAppActivity.java
index 6d1acc6ef..34b2cf0c7 100644
--- a/VirtualApp/app/src/main/java/io/virtualapp/home/ListAppActivity.java
+++ b/VirtualApp/app/src/main/java/io/virtualapp/home/ListAppActivity.java
@@ -4,15 +4,13 @@
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
-import android.graphics.drawable.ColorDrawable;
-import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.TabLayout;
import android.support.v4.app.ActivityCompat;
import android.support.v4.view.ViewPager;
-import android.support.v7.app.ActionBar;
+import android.support.v7.app.AlertDialog;
import android.support.v7.widget.Toolbar;
import android.view.MenuItem;
@@ -38,31 +36,29 @@ public static void gotoListApp(Activity activity) {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- getWindow().setBackgroundDrawable(new ColorDrawable(getResources().getColor(R.color.colorPrimaryDark)));
setContentView(R.layout.activity_clone_app);
mToolBar = (Toolbar) findViewById(R.id.clone_app_tool_bar);
mTabLayout = (TabLayout) mToolBar.findViewById(R.id.clone_app_tab_layout);
mViewPager = (ViewPager) findViewById(R.id.clone_app_view_pager);
- setupToolBar();
mViewPager.setAdapter(new AppPagerAdapter(getSupportFragmentManager()));
mTabLayout.setupWithViewPager(mViewPager);
+
// Request permission to access external storage
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
- if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
- ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 0);
+ if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+ AlertDialog alertDialog = new AlertDialog.Builder(this, R.style.Theme_AppCompat_DayNight_Dialog_Alert)
+ .setMessage(R.string.list_app_access_external_storage)
+ .setPositiveButton(android.R.string.ok, (dialog, which) -> {
+ ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 0);
+ })
+ .create();
+ try {
+ alertDialog.show();
+ } catch (Throwable ignored) {
+ // BadTokenException.
}
}
}
- private void setupToolBar() {
- setSupportActionBar(mToolBar);
- ActionBar actionBar = getSupportActionBar();
- if (actionBar != null) {
- actionBar.setDisplayHomeAsUpEnabled(true);
- }
- }
-
-
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/home/ListAppFragment.java b/VirtualApp/app/src/main/java/io/virtualapp/home/ListAppFragment.java
index 1d953d932..c05499f7d 100644
--- a/VirtualApp/app/src/main/java/io/virtualapp/home/ListAppFragment.java
+++ b/VirtualApp/app/src/main/java/io/virtualapp/home/ListAppFragment.java
@@ -1,9 +1,17 @@
package io.virtualapp.home;
import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.ComponentName;
+import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.graphics.drawable.ColorDrawable;
+import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.Nullable;
+import android.support.v7.widget.DividerItemDecoration;
import android.support.v7.widget.OrientationHelper;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.view.LayoutInflater;
@@ -19,24 +27,27 @@
import java.util.Locale;
import io.virtualapp.R;
-import io.virtualapp.VCommends;
+import io.virtualapp.XApp;
import io.virtualapp.abs.ui.VFragment;
-import io.virtualapp.abs.ui.VUiKit;
import io.virtualapp.home.adapters.CloneAppListAdapter;
-import io.virtualapp.home.adapters.decorations.ItemOffsetDecoration;
import io.virtualapp.home.models.AppInfo;
import io.virtualapp.home.models.AppInfoLite;
+import io.virtualapp.sys.Installd;
import io.virtualapp.widgets.DragSelectRecyclerView;
+
/**
* @author Lody
*/
public class ListAppFragment extends VFragment implements ListAppContract.ListAppView {
private static final String KEY_SELECT_FROM = "key_select_from";
+ private static final int REQUEST_GET_FILE = 1;
+
private DragSelectRecyclerView mRecyclerView;
private ProgressBar mProgressBar;
private Button mInstallButton;
private CloneAppListAdapter mAdapter;
+ private View mSelectFromExternal;
public static ListAppFragment newInstance(File selectFrom) {
Bundle args = new Bundle();
@@ -70,14 +81,88 @@ public void onSaveInstanceState(Bundle outState) {
mAdapter.saveInstanceState(outState);
}
+ private void whatIsTaiChi() {
+ AlertDialog alertDialog = new AlertDialog.Builder(getContext())
+ .setTitle(R.string.what_is_exp)
+ .setMessage(R.string.exp_tips)
+ .setPositiveButton(R.string.exp_introduce_title, (dialog, which) -> {
+ Intent t = new Intent(Intent.ACTION_VIEW);
+ t.setData(Uri.parse("https://www.coolapk.com/apk/me.weishu.exp"));
+ startActivity(t);
+ }).setNegativeButton(R.string.about_donate_title, (dialog, which) -> {
+ Intent t = new Intent(Intent.ACTION_VIEW);
+ t.setData(Uri.parse("https://vxposed.com/donate.html"));
+ startActivity(t);
+ })
+ .create();
+ try {
+ alertDialog.show();
+ } catch (Throwable ignored) {
+ }
+ }
+
+ private void chooseInstallWay(Runnable runnable, String path) {
+ AlertDialog alertDialog = new AlertDialog.Builder(getContext())
+ .setTitle(R.string.install_choose_way)
+ .setMessage(R.string.install_choose_content)
+ .setPositiveButton(R.string.install_choose_taichi, (dialog, which) -> {
+ PackageManager packageManager = getActivity().getPackageManager();
+ try {
+ packageManager.getPackageInfo("me.weishu.exp", 0);
+ Intent intent = new Intent();
+ intent.setComponent(new ComponentName("me.weishu.exp", "me.weishu.exp.ui.MainActivity"));
+ intent.putExtra("path", path);
+ startActivity(intent);
+ } catch (PackageManager.NameNotFoundException e) {
+ AlertDialog showInstallDialog = new AlertDialog.Builder(getContext())
+ .setTitle(android.R.string.dialog_alert_title)
+ .setMessage(R.string.install_taichi_not_exist)
+ .setPositiveButton(R.string.install_go_to_install_exp, (dialog1, which1) -> {
+ Intent t = new Intent(Intent.ACTION_VIEW);
+ t.setData(Uri.parse("https://www.coolapk.com/apk/me.weishu.exp"));
+ startActivity(t);
+ })
+ .create();
+ showInstallDialog.show();
+ } catch (Throwable e) {
+ AlertDialog showInstallDialog = new AlertDialog.Builder(getContext())
+ .setTitle(android.R.string.dialog_alert_title)
+ .setMessage(R.string.install_taichi_while_old_version)
+ .setPositiveButton(R.string.install_go_latest_exp, (dialog1, which1) -> {
+ Intent t = new Intent(Intent.ACTION_VIEW);
+ t.setData(Uri.parse("https://www.coolapk.com/apk/me.weishu.exp"));
+ startActivity(t);
+ })
+ .create();
+ showInstallDialog.show();
+ }
+ finishActivity();
+ }).setNegativeButton("VirtualXposed", (dialog, which) -> {
+ if (runnable != null) {
+ runnable.run();
+ }
+ finishActivity();
+ }).setNeutralButton(R.string.what_is_exp, ((dialog, which) -> {
+ whatIsTaiChi();
+ }))
+ .create();
+ try {
+ alertDialog.show();
+ } catch (Throwable ignored) {
+ }
+ }
+
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
mRecyclerView = (DragSelectRecyclerView) view.findViewById(R.id.select_app_recycler_view);
mProgressBar = (ProgressBar) view.findViewById(R.id.select_app_progress_bar);
mInstallButton = (Button) view.findViewById(R.id.select_app_install_btn);
- mRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(3, OrientationHelper.VERTICAL));
- mRecyclerView.addItemDecoration(new ItemOffsetDecoration(VUiKit.dpToPx(getContext(), 2)));
- mAdapter = new CloneAppListAdapter(getActivity());
+ mSelectFromExternal = view.findViewById(R.id.select_app_from_external);
+ mRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(1, OrientationHelper.VERTICAL));
+ DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(getContext(), DividerItemDecoration.VERTICAL);
+ dividerItemDecoration.setDrawable(new ColorDrawable(0x1f000000));
+ mRecyclerView.addItemDecoration(dividerItemDecoration);
+ mAdapter = new CloneAppListAdapter(getActivity(), getSelectFrom());
mRecyclerView.setAdapter(mAdapter);
mAdapter.setOnItemClickListener(new CloneAppListAdapter.ItemEventListener() {
@Override
@@ -85,7 +170,7 @@ public void onItemClick(AppInfo info, int position) {
int count = mAdapter.getSelectedCount();
if (!mAdapter.isIndexSelected(position)) {
if (count >= 9) {
- Toast.makeText(getContext(), "No more then 9 apps can be chosen at a time!", Toast.LENGTH_SHORT).show();
+ Toast.makeText(getContext(), R.string.install_too_much_once_time, Toast.LENGTH_SHORT).show();
return;
}
}
@@ -99,19 +184,30 @@ public boolean isSelectable(int position) {
});
mAdapter.setSelectionListener(count -> {
mInstallButton.setEnabled(count > 0);
- mInstallButton.setText(String.format(Locale.ENGLISH, "Install to SandBox (%d)", count));
+ mInstallButton.setText(String.format(Locale.ENGLISH, XApp.getApp().getResources().getString(R.string.install_d), count));
});
mInstallButton.setOnClickListener(v -> {
Integer[] selectedIndices = mAdapter.getSelectedIndices();
ArrayList dataList = new ArrayList(selectedIndices.length);
for (int index : selectedIndices) {
AppInfo info = mAdapter.getItem(index);
- dataList.add(new AppInfoLite(info.packageName, info.path, info.fastOpen));
+ dataList.add(new AppInfoLite(info.packageName, info.path, info.fastOpen, info.disableMultiVersion));
+ }
+
+ if (dataList.size() > 0) {
+ String path = dataList.get(0).path;
+ chooseInstallWay(() -> Installd.startInstallerActivity(getActivity(), dataList), path);
+ }
+ });
+ mSelectFromExternal.setOnClickListener(v -> {
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.setType("application/vnd.android.package-archive"); // apk file
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ try {
+ startActivityForResult(intent, REQUEST_GET_FILE);
+ } catch (Throwable ignored) {
+ Toast.makeText(getActivity(), "Error", Toast.LENGTH_SHORT).show();
}
- Intent data = new Intent();
- data.putParcelableArrayListExtra(VCommends.EXTRA_APP_INFO_LIST, dataList);
- getActivity().setResult(Activity.RESULT_OK, data);
- getActivity().finish();
});
new ListAppPresenterImpl(getActivity(), this, getSelectFrom()).start();
}
@@ -125,7 +221,7 @@ public void startLoading() {
@Override
public void loadFinish(List infoList) {
mAdapter.setList(infoList);
- mRecyclerView.setDragSelectActive(true, 0);
+ mRecyclerView.setDragSelectActive(false, 0);
mAdapter.setSelected(0, false);
mProgressBar.setVisibility(View.GONE);
mRecyclerView.setVisibility(View.VISIBLE);
@@ -136,4 +232,49 @@ public void setPresenter(ListAppContract.ListAppPresenter presenter) {
this.mPresenter = presenter;
}
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (!(requestCode == REQUEST_GET_FILE && resultCode == Activity.RESULT_OK)) {
+ return;
+ }
+ Activity current = getActivity();
+ if (current == null) {
+ return;
+ }
+
+ Uri uri = data.getData();
+ String path = getPath(getActivity(), uri);
+ if (path == null) {
+ return;
+ }
+
+ chooseInstallWay(() -> Installd.handleRequestFromFile(getActivity(), path), path);
+ }
+
+ public static String getPath(Context context, Uri uri) {
+ if (uri == null) {
+ return null;
+ }
+ if ("content".equalsIgnoreCase(uri.getScheme())) {
+ String[] projection = {"_data"};
+ Cursor cursor = null;
+ try {
+ cursor = context.getContentResolver().query(uri, projection, null, null, null);
+ int column_index = cursor.getColumnIndexOrThrow("_data");
+ if (cursor.moveToFirst()) {
+ return cursor.getString(column_index);
+ }
+ } catch (Exception e) {
+ // Eat it Or Log it.
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ } else if ("file".equalsIgnoreCase(uri.getScheme())) {
+ return uri.getPath();
+ }
+ return null;
+ }
}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/home/LoadingActivity.java b/VirtualApp/app/src/main/java/io/virtualapp/home/LoadingActivity.java
index 9d7547ff8..3c68a5704 100644
--- a/VirtualApp/app/src/main/java/io/virtualapp/home/LoadingActivity.java
+++ b/VirtualApp/app/src/main/java/io/virtualapp/home/LoadingActivity.java
@@ -1,16 +1,35 @@
package io.virtualapp.home;
+import android.app.ActivityManager;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.RemoteException;
+import android.os.SystemClock;
+import android.support.annotation.NonNull;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.AlertDialog;
+import android.text.TextUtils;
+import android.util.Log;
import android.widget.ImageView;
import android.widget.TextView;
+import android.widget.Toast;
import com.lody.virtual.client.core.VirtualCore;
+import com.lody.virtual.client.env.Constants;
import com.lody.virtual.client.ipc.VActivityManager;
+import com.lody.virtual.client.ipc.VPackageManager;
+import com.lody.virtual.helper.utils.VLog;
+import com.lody.virtual.server.pm.parser.VPackage;
+import java.util.HashSet;
+import java.util.List;
import java.util.Locale;
+import java.util.Set;
import io.virtualapp.R;
import io.virtualapp.abs.ui.VActivity;
@@ -18,6 +37,7 @@
import io.virtualapp.home.models.PackageAppData;
import io.virtualapp.home.repo.PackageAppDataStorage;
import io.virtualapp.widgets.EatBeansView;
+import jonathanfinerty.once.Once;
/**
* @author Lody
@@ -25,61 +45,234 @@
public class LoadingActivity extends VActivity {
- private static final String PKG_NAME_ARGUMENT = "MODEL_ARGUMENT";
- private static final String KEY_INTENT = "KEY_INTENT";
- private static final String KEY_USER = "KEY_USER";
+ private static final String TAG = "LoadingActivity";
+
private PackageAppData appModel;
private EatBeansView loadingView;
- public static void launch(Context context, String packageName, int userId) {
+ private static final int REQUEST_PERMISSION_CODE = 100;
+
+ private Intent intentToLaunch;
+ private int userToLaunch;
+
+ private long start;
+
+ public static boolean launch(Context context, String packageName, int userId) {
Intent intent = VirtualCore.get().getLaunchIntent(packageName, userId);
if (intent != null) {
Intent loadingPageIntent = new Intent(context, LoadingActivity.class);
- loadingPageIntent.putExtra(PKG_NAME_ARGUMENT, packageName);
+ loadingPageIntent.putExtra(Constants.PASS_PKG_NAME_ARGUMENT, packageName);
loadingPageIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- loadingPageIntent.putExtra(KEY_INTENT, intent);
- loadingPageIntent.putExtra(KEY_USER, userId);
+ loadingPageIntent.putExtra(Constants.PASS_KEY_INTENT, intent);
+ loadingPageIntent.putExtra(Constants.PASS_KEY_USER, userId);
context.startActivity(loadingPageIntent);
+ return true;
+ } else {
+ return false;
}
}
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ start = SystemClock.elapsedRealtime();
+
setContentView(R.layout.activity_loading);
loadingView = (EatBeansView) findViewById(R.id.loading_anim);
- int userId = getIntent().getIntExtra(KEY_USER, -1);
- String pkg = getIntent().getStringExtra(PKG_NAME_ARGUMENT);
+ int userId = getIntent().getIntExtra(Constants.PASS_KEY_USER, -1);
+ String pkg = getIntent().getStringExtra(Constants.PASS_PKG_NAME_ARGUMENT);
appModel = PackageAppDataStorage.get().acquire(pkg);
+ if (appModel == null) {
+ Toast.makeText(getApplicationContext(), "Open App:" + pkg + " failed.", Toast.LENGTH_SHORT).show();
+ finish();
+ return;
+ }
+
ImageView iconView = (ImageView) findViewById(R.id.app_icon);
iconView.setImageDrawable(appModel.icon);
TextView nameView = (TextView) findViewById(R.id.app_name);
nameView.setText(String.format(Locale.ENGLISH, "Opening %s...", appModel.name));
- Intent intent = getIntent().getParcelableExtra(KEY_INTENT);
+ Intent intent = getIntent().getParcelableExtra(Constants.PASS_KEY_INTENT);
if (intent == null) {
+ finish();
return;
}
VirtualCore.get().setUiCallback(intent, mUiCallback);
- VUiKit.defer().when(() -> {
- long startTime = System.currentTimeMillis();
- if (!appModel.fastOpen) {
- try {
- VirtualCore.get().preOpt(appModel.packageName);
- } catch (Exception e) {
- e.printStackTrace();
+
+ try {
+ // 如果已经在运行了,那么直接拉起,不做任何检测。
+ boolean uiRunning = false;
+ ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
+ if (am != null) {
+ List runningAppProcesses = am.getRunningAppProcesses();
+ for (ActivityManager.RunningAppProcessInfo runningAppProcess : runningAppProcesses) {
+ String appProcessName = VActivityManager.get().getAppProcessName(runningAppProcess.pid);
+ if (TextUtils.equals(appProcessName, pkg)) {
+ uiRunning = true;
+ break;
+ }
}
}
- long spend = System.currentTimeMillis() - startTime;
- if (spend < 500) {
- try {
- Thread.sleep(500 - spend);
- } catch (InterruptedException e) {
- e.printStackTrace();
+
+ VLog.i(TAG, pkg + "is running: " + uiRunning);
+ if (uiRunning) {
+ launchActivity(intent, userId);
+ return;
+ }
+ } catch (Throwable ignored) {
+ ignored.printStackTrace();
+ }
+
+ checkAndLaunch(intent, userId);
+ }
+
+ private void checkAndLaunch(Intent intent, int userId) {
+ final int RUNTIME_PERMISSION_API_LEVEL = android.os.Build.VERSION_CODES.M;
+
+ if (android.os.Build.VERSION.SDK_INT < RUNTIME_PERMISSION_API_LEVEL) {
+ // the device is below Android M, the permissions are granted when install, start directly
+ Log.i(TAG, "device's api level below Android M, do not need runtime permission.");
+ launchActivityWithDelay(intent, userId);
+ return;
+ }
+
+ // The device is above android M, support runtime permission.
+ String packageName = appModel.packageName;
+ String name = appModel.name;
+
+ // analyze permission
+ try {
+ ApplicationInfo applicationInfo = VPackageManager.get().getApplicationInfo(packageName, 0, 0);
+ int targetSdkVersion = applicationInfo.targetSdkVersion;
+ Log.i(TAG, "target package: " + packageName + " targetSdkVersion: " + targetSdkVersion);
+
+ if (targetSdkVersion >= RUNTIME_PERMISSION_API_LEVEL) {
+ Log.i(TAG, "target package support runtime permission, launch directly.");
+ launchActivityWithDelay(intent, userId);
+ } else {
+
+ intentToLaunch = intent;
+ userToLaunch = userId;
+
+ PackageInfo packageInfo = VPackageManager.get().getPackageInfo(packageName, PackageManager.GET_PERMISSIONS, 0);
+ String[] requestedPermissions = packageInfo.requestedPermissions;
+
+ Set dangerousPermissions = new HashSet<>();
+ for (String requestedPermission : requestedPermissions) {
+ if (VPackage.PermissionComponent.DANGEROUS_PERMISSION.contains(requestedPermission)) {
+ // dangerous permission, check it
+ if (ContextCompat.checkSelfPermission(this, requestedPermission) != PackageManager.PERMISSION_GRANTED) {
+ dangerousPermissions.add(requestedPermission);
+ } else {
+ Log.i(TAG, "permission: " + requestedPermission + " is granted, ignore.");
+ }
+ }
+ }
+
+ if (dangerousPermissions.isEmpty()) {
+ Log.i(TAG, "all permission are granted, launch directly.");
+ // all permission are granted, launch directly.
+ launchActivityWithDelay(intent, userId);
+ } else {
+ // tell user that this app need that permission
+ Log.i(TAG, "request permission: " + dangerousPermissions);
+
+ AlertDialog alertDialog = new AlertDialog.Builder(this, R.style.Theme_AppCompat_DayNight_Dialog_Alert)
+ .setTitle(R.string.permission_tip_title)
+ .setMessage(getResources().getString(R.string.permission_tips_content, name))
+ .setPositiveButton(R.string.permission_tips_confirm, (dialog, which) -> {
+ String[] permissionToRequest = dangerousPermissions.toArray(new String[dangerousPermissions.size()]);
+ try {
+ ActivityCompat.requestPermissions(this, permissionToRequest, REQUEST_PERMISSION_CODE);
+ } catch (Throwable ignored) {
+ }
+ })
+ .create();
+ try {
+ alertDialog.show();
+ } catch (Throwable ignored) {
+ // BadTokenException.
+ finish();
+ Toast.makeText(this, getResources().getString(R.string.start_app_failed, appModel.name), Toast.LENGTH_SHORT).show();
+ }
}
}
- }).done((res) ->
- VActivityManager.get().startActivity(intent, userId));
+ } catch (Throwable e) {
+ Log.e(TAG, "check permission failed: ", e);
+ launchActivityWithDelay(intent, userId);
+ }
+ }
+
+ private void launchActivityWithDelay(Intent intent, int userId) {
+ final int MAX_WAIT = 1000;
+ long delta = SystemClock.elapsedRealtime() - start;
+ long waitTime = MAX_WAIT - delta;
+
+ if (waitTime <= 0) {
+ launchActivity(intent, userId);
+ } else {
+ loadingView.postDelayed(() -> launchActivity(intent, userId), waitTime);
+ }
+ }
+ private void launchActivity(Intent intent, int userId) {
+ try {
+ VActivityManager.get().startActivity(intent, userId);
+ } catch (Throwable e) {
+ VLog.e(TAG, "start activity failed:", e);
+ Toast.makeText(getApplicationContext(), getResources().getString(R.string.start_app_failed, appModel.name), Toast.LENGTH_SHORT).show();
+ finish();
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ if (requestCode == REQUEST_PERMISSION_CODE) {
+ boolean allGranted = true;
+ for (int grantResult : grantResults) {
+ if (grantResult == PackageManager.PERMISSION_DENIED) {
+ allGranted = false;
+ break;
+ }
+ }
+
+ if (allGranted) {
+ if (intentToLaunch == null) {
+ Toast.makeText(this, getResources().getString(R.string.start_app_failed, appModel.name), Toast.LENGTH_SHORT).show();
+ finish();
+ } else {
+ launchActivityWithDelay(intentToLaunch, userToLaunch);
+ }
+ } else {
+ // 提示用户,targetSdkVersion < 23 无法使用运行时权限
+ Log.i(TAG, "can not use runtime permission, you must grant all permission, otherwise the app may not work!");
+
+ final String tag = "permission_tips_" + appModel.packageName.replaceAll("\\.", "_");
+ // TODO find a device figuring out why some permissions are not detected.
+ if (!Once.beenDone(tag)) {
+ AlertDialog alertDialog = new AlertDialog.Builder(this, R.style.Theme_AppCompat_DayNight_Dialog_Alert)
+ .setTitle(android.R.string.dialog_alert_title)
+ .setMessage(getResources().getString(R.string.permission_denied_tips_content, appModel.name))
+ .setPositiveButton(R.string.permission_tips_confirm, (dialog, which) -> {
+ finish();
+ Once.markDone(tag);
+ launchActivityWithDelay(intentToLaunch, userToLaunch);
+ })
+ .create();
+ try {
+ alertDialog.show();
+ } catch (Throwable ignored) {
+ // BadTokenException.
+ Toast.makeText(this, getResources().getString(R.string.start_app_failed, appModel.name), Toast.LENGTH_SHORT).show();
+ }
+ } else {
+ launchActivityWithDelay(intentToLaunch, userToLaunch);
+ finish();
+ }
+ }
+ }
}
private final VirtualCore.UiCallback mUiCallback = new VirtualCore.UiCallback() {
@@ -88,17 +281,38 @@ protected void onCreate(Bundle savedInstanceState) {
public void onAppOpened(String packageName, int userId) throws RemoteException {
finish();
}
+
+ @Override
+ public void onOpenFailed(String packageName, int userId) throws RemoteException {
+ VUiKit.defer().when(() -> {
+ }).done((v) -> {
+ if (!isFinishing()) {
+ Toast.makeText(getApplicationContext(), getResources().getString(R.string.start_app_failed, packageName),
+ Toast.LENGTH_SHORT).show();
+ }
+ });
+
+ finish();
+ }
};
@Override
protected void onResume() {
super.onResume();
- loadingView.startAnim();
+ startAnim();
+ }
+
+ private void startAnim() {
+ if (loadingView != null) {
+ loadingView.startAnim();
+ }
}
@Override
- protected void onPause() {
- super.onPause();
- loadingView.stopAnim();
+ protected void onStop() {
+ super.onStop();
+ if (loadingView != null) {
+ loadingView.stopAnim();
+ }
}
}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/home/NewHomeActivity.java b/VirtualApp/app/src/main/java/io/virtualapp/home/NewHomeActivity.java
new file mode 100644
index 000000000..657fc58bc
--- /dev/null
+++ b/VirtualApp/app/src/main/java/io/virtualapp/home/NewHomeActivity.java
@@ -0,0 +1,341 @@
+package io.virtualapp.home;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.PowerManager;
+import android.os.SystemClock;
+import android.preference.PreferenceManager;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.Toast;
+
+import com.android.launcher3.LauncherFiles;
+import com.google.android.apps.nexuslauncher.NexusLauncherActivity;
+import com.lody.virtual.client.core.InstallStrategy;
+import com.lody.virtual.client.core.VirtualCore;
+import com.lody.virtual.helper.utils.DeviceUtil;
+import com.lody.virtual.helper.utils.FileUtils;
+import com.lody.virtual.helper.utils.MD5Utils;
+import com.lody.virtual.helper.utils.VLog;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.reflect.Method;
+
+import io.virtualapp.R;
+import io.virtualapp.abs.ui.VUiKit;
+import io.virtualapp.settings.SettingsActivity;
+import io.virtualapp.update.VAVersionService;
+
+import static io.virtualapp.XApp.XPOSED_INSTALLER_PACKAGE;
+
+/**
+ * @author weishu
+ * @date 18/2/9.
+ */
+
+public class NewHomeActivity extends NexusLauncherActivity {
+
+ private static final String SHOW_DOZE_ALERT_KEY = "SHOW_DOZE_ALERT_KEY";
+ private static final String WALLPAPER_FILE_NAME = "wallpaper.png";
+
+ private Handler mUiHandler;
+ private boolean mDirectlyBack = false;
+ private boolean checkXposedInstaller = true;
+
+ public static void goHome(Context context) {
+ Intent intent = new Intent(context, NewHomeActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(intent);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ SharedPreferences sharedPreferences = getSharedPreferences(LauncherFiles.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE);
+ super.onCreate(savedInstanceState);
+ showMenuKey();
+ mUiHandler = new Handler(getMainLooper());
+ alertForMeizu();
+ alertForDoze();
+ mDirectlyBack = sharedPreferences.getBoolean(SettingsActivity.DIRECTLY_BACK_KEY, false);
+ }
+
+ private void installXposed() {
+ boolean isXposedInstalled = false;
+ try {
+ isXposedInstalled = VirtualCore.get().isAppInstalled(XPOSED_INSTALLER_PACKAGE);
+ File oldXposedInstallerApk = getFileStreamPath("XposedInstaller_1_31.apk");
+ if (oldXposedInstallerApk.exists()) {
+ VirtualCore.get().uninstallPackage(XPOSED_INSTALLER_PACKAGE);
+ oldXposedInstallerApk.delete();
+ isXposedInstalled = false;
+ Log.d(TAG, "remove xposed installer success!");
+ }
+ } catch (Throwable e) {
+ VLog.d(TAG, "remove xposed install failed.", e);
+ }
+
+ if (!isXposedInstalled) {
+ ProgressDialog dialog = new ProgressDialog(this);
+ dialog.setCancelable(false);
+ dialog.setMessage(getResources().getString(R.string.prepare_xposed_installer));
+ dialog.show();
+
+ VUiKit.defer().when(() -> {
+ File xposedInstallerApk = getFileStreamPath("XposedInstaller_5_8.apk");
+ if (!xposedInstallerApk.exists()) {
+ InputStream input = null;
+ OutputStream output = null;
+ try {
+ input = getApplicationContext().getAssets().open("XposedInstaller_3.1.5.apk_");
+ output = new FileOutputStream(xposedInstallerApk);
+ byte[] buffer = new byte[1024];
+ int length;
+ while ((length = input.read(buffer)) > 0) {
+ output.write(buffer, 0, length);
+ }
+ } catch (Throwable e) {
+ VLog.e(TAG, "copy file error", e);
+ } finally {
+ FileUtils.closeQuietly(input);
+ FileUtils.closeQuietly(output);
+ }
+ }
+
+ if (xposedInstallerApk.isFile() && !DeviceUtil.isMeizuBelowN()) {
+ try {
+ if ("8537fb219128ead3436cc19ff35cfb2e".equals(MD5Utils.getFileMD5String(xposedInstallerApk))) {
+ VirtualCore.get().installPackage(xposedInstallerApk.getPath(), InstallStrategy.TERMINATE_IF_EXIST);
+ } else {
+ VLog.w(TAG, "unknown Xposed installer, ignore!");
+ }
+ } catch (Throwable ignored) {
+ }
+ }
+ }).then((v) -> {
+ dismissDialog(dialog);
+ }).fail((err) -> {
+ dismissDialog(dialog);
+ });
+ }
+ }
+
+ private static void dismissDialog(ProgressDialog dialog) {
+ if (dialog == null) {
+ return;
+ }
+ try {
+ dialog.dismiss();
+ } catch (Throwable e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (checkXposedInstaller) {
+ checkXposedInstaller = false;
+ installXposed();
+ }
+ // check for update
+ new Handler().postDelayed(() ->
+ VAVersionService.checkUpdate(getApplicationContext(), false), 1000);
+
+ // check for wallpaper
+ setWallpaper();
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_MENU) {
+ onSettingsClicked();
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ public Activity getActivity() {
+ return this;
+ }
+
+ public Context getContext() {
+ return this;
+ }
+
+ @Override
+ public void onClickAddWidgetButton(View view) {
+ onAddAppClicked();
+ }
+
+ private void onAddAppClicked() {
+ ListAppActivity.gotoListApp(this);
+ }
+
+ private void onSettingsClicked() {
+ startActivity(new Intent(NewHomeActivity.this, SettingsActivity.class));
+ }
+
+ @Override
+ public void onClickSettingsButton(View v) {
+ onSettingsClicked();
+ }
+
+ @Override
+ protected void onClickAllAppsButton(View v) {
+ onSettingsClicked();
+ }
+
+ @Override
+ public void startVirtualActivity(Intent intent, Bundle options, int usedId) {
+ String packageName = intent.getPackage();
+ if (TextUtils.isEmpty(packageName)) {
+ ComponentName component = intent.getComponent();
+ if (component != null) {
+ packageName = component.getPackageName();
+ }
+ }
+ if (packageName == null) {
+ try {
+ startActivity(intent);
+ return;
+ } catch (Throwable ignored) {
+ // ignore
+ }
+ }
+ boolean result = LoadingActivity.launch(this, packageName, usedId);
+ if (!result) {
+ throw new ActivityNotFoundException("can not launch activity for :" + intent);
+ }
+ if (mDirectlyBack) {
+ finish();
+ }
+ }
+
+ private void alertForMeizu() {
+ if (!DeviceUtil.isMeizuBelowN()) {
+ return;
+ }
+ boolean isXposedInstalled = VirtualCore.get().isAppInstalled(XPOSED_INSTALLER_PACKAGE);
+ if (isXposedInstalled) {
+ return;
+ }
+ mUiHandler.postDelayed(() -> {
+ AlertDialog alertDialog = new AlertDialog.Builder(getContext())
+ .setTitle(R.string.meizu_device_tips_title)
+ .setMessage(R.string.meizu_device_tips_content)
+ .setPositiveButton(android.R.string.yes, (dialog, which) -> {
+ })
+ .create();
+ try {
+ alertDialog.show();
+ } catch (Throwable ignored) {
+ }
+ }, 2000);
+ }
+
+ private void alertForDoze() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ return;
+ }
+ PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
+ if (powerManager == null) {
+ return;
+ }
+ boolean showAlert = PreferenceManager.getDefaultSharedPreferences(this).getBoolean(SHOW_DOZE_ALERT_KEY, true);
+ if (!showAlert) {
+ return;
+ }
+ String packageName = getPackageName();
+ boolean ignoringBatteryOptimizations = powerManager.isIgnoringBatteryOptimizations(packageName);
+ if (!ignoringBatteryOptimizations) {
+
+ mUiHandler.postDelayed(() -> {
+ AlertDialog alertDialog = new AlertDialog.Builder(getContext())
+ .setTitle(R.string.alert_for_doze_mode_title)
+ .setMessage(R.string.alert_for_doze_mode_content)
+ .setPositiveButton(R.string.alert_for_doze_mode_yes, (dialog, which) -> {
+ try {
+ startActivity(new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, Uri.parse("package:" + getPackageName())));
+ } catch (ActivityNotFoundException ignored) {
+ // ActivityNotFoundException on some devices.
+ try {
+ startActivity(new Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS));
+ } catch (Throwable e) {
+ PreferenceManager.getDefaultSharedPreferences(getActivity())
+ .edit().putBoolean(SHOW_DOZE_ALERT_KEY, false).apply();
+ }
+ } catch (Throwable e) {
+ PreferenceManager.getDefaultSharedPreferences(getActivity())
+ .edit().putBoolean(SHOW_DOZE_ALERT_KEY, false).apply();
+ }
+ })
+ .setNegativeButton(R.string.alert_for_doze_mode_no, (dialog, which) ->
+ PreferenceManager.getDefaultSharedPreferences(getActivity())
+ .edit().putBoolean(SHOW_DOZE_ALERT_KEY, false).apply())
+ .create();
+ try {
+ alertDialog.show();
+ } catch (Throwable ignored) {
+ ignored.printStackTrace();
+ }
+ }, 1000);
+ }
+ }
+
+ private void setWallpaper() {
+ File wallpaper = getFileStreamPath(WALLPAPER_FILE_NAME);
+ if (wallpaper == null || !wallpaper.exists() || wallpaper.isDirectory()) {
+ setOurWallpaper(getResources().getDrawable(R.drawable.home_bg));
+ } else {
+ long start = SystemClock.elapsedRealtime();
+ Drawable d;
+ try {
+ d = BitmapDrawable.createFromPath(wallpaper.getPath());
+ } catch (Throwable e) {
+ Toast.makeText(getApplicationContext(), R.string.wallpaper_too_big_tips, Toast.LENGTH_SHORT).show();
+ return;
+ }
+ long cost = SystemClock.elapsedRealtime() - start;
+ if (cost > 200) {
+ Toast.makeText(getApplicationContext(), R.string.wallpaper_too_big_tips, Toast.LENGTH_SHORT).show();
+ }
+ if (d == null) {
+ setOurWallpaper(getResources().getDrawable(R.drawable.home_bg));
+ } else {
+ setOurWallpaper(d);
+ }
+ }
+ }
+
+ private void showMenuKey() {
+ try {
+ Method setNeedsMenuKey = Window.class.getDeclaredMethod("setNeedsMenuKey", int.class);
+ setNeedsMenuKey.setAccessible(true);
+ int value = WindowManager.LayoutParams.class.getField("NEEDS_MENU_SET_TRUE").getInt(null);
+ setNeedsMenuKey.invoke(getWindow(), value);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/home/adapters/AppPagerAdapter.java b/VirtualApp/app/src/main/java/io/virtualapp/home/adapters/AppPagerAdapter.java
index 1bfffd67d..ad152d634 100644
--- a/VirtualApp/app/src/main/java/io/virtualapp/home/adapters/AppPagerAdapter.java
+++ b/VirtualApp/app/src/main/java/io/virtualapp/home/adapters/AppPagerAdapter.java
@@ -9,13 +9,15 @@
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
+import com.lody.virtual.helper.utils.DeviceUtil;
import com.lody.virtual.helper.utils.Reflect;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
-import io.virtualapp.VApp;
+import io.virtualapp.XApp;
+import io.virtualapp.R;
import io.virtualapp.home.ListAppFragment;
/**
@@ -27,10 +29,10 @@ public class AppPagerAdapter extends FragmentPagerAdapter {
public AppPagerAdapter(FragmentManager fm) {
super(fm);
- titles.add("Clone Apps");
+ titles.add(XApp.getApp().getResources().getString(R.string.clone_apps));
dirs.add(null);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- Context ctx = VApp.getApp();
+ Context ctx = XApp.getApp();
StorageManager storage = (StorageManager) ctx.getSystemService(Context.STORAGE_SERVICE);
for (StorageVolume volume : storage.getStorageVolumes()) {
//Why the fuck are getPathFile and getUserLabel hidden?!
@@ -44,10 +46,12 @@ public AppPagerAdapter(FragmentManager fm) {
}
} else {
// Fallback: only support the default storage sources
- File storageFir = Environment.getExternalStorageDirectory();
- if (storageFir.list() != null) {
- titles.add("Ghost Installation");
- dirs.add(storageFir);
+ if (!DeviceUtil.isMeizuBelowN()) {
+ File storageFir = Environment.getExternalStorageDirectory();
+ if (storageFir != null && storageFir.isDirectory()) {
+ titles.add(XApp.getApp().getResources().getString(R.string.external_storage));
+ dirs.add(storageFir);
+ }
}
}
}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/home/adapters/CloneAppListAdapter.java b/VirtualApp/app/src/main/java/io/virtualapp/home/adapters/CloneAppListAdapter.java
index 696ae2e5a..a527ad471 100644
--- a/VirtualApp/app/src/main/java/io/virtualapp/home/adapters/CloneAppListAdapter.java
+++ b/VirtualApp/app/src/main/java/io/virtualapp/home/adapters/CloneAppListAdapter.java
@@ -1,6 +1,7 @@
package io.virtualapp.home.adapters;
import android.content.Context;
+import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.view.LayoutInflater;
@@ -9,10 +10,12 @@
import android.widget.ImageView;
import android.widget.TextView;
+import java.io.File;
import java.util.List;
import io.virtualapp.R;
import io.virtualapp.abs.ui.VUiKit;
+import io.virtualapp.glide.GlideUtils;
import io.virtualapp.home.models.AppInfo;
import io.virtualapp.widgets.DragSelectRecyclerViewAdapter;
import io.virtualapp.widgets.LabelView;
@@ -28,7 +31,13 @@ public class CloneAppListAdapter extends DragSelectRecyclerViewAdapter mAppList;
private ItemEventListener mItemEventListener;
- public CloneAppListAdapter(Context context) {
+ private Context mContext;
+ private File mFrom;
+
+
+ public CloneAppListAdapter(Context context, @Nullable File from) {
+ mContext = context;
+ mFrom = from;
this.mInflater = LayoutInflater.from(context);
mFooterView = new View(context);
StaggeredGridLayoutManager.LayoutParams params = new StaggeredGridLayoutManager.LayoutParams(
@@ -67,8 +76,14 @@ public void onBindViewHolder(ViewHolder holder, int position) {
}
super.onBindViewHolder(holder, position);
AppInfo info = mAppList.get(position);
- holder.iconView.setImageDrawable(info.icon);
- holder.nameView.setText(info.name);
+
+ if (mFrom == null) {
+ GlideUtils.loadInstalledPackageIcon(mContext, info.packageName, holder.iconView, android.R.drawable.sym_def_app_icon);
+ } else {
+ GlideUtils.loadPackageIconFromApkFile(mContext, info.path, holder.iconView, android.R.drawable.sym_def_app_icon);
+ }
+
+ holder.nameView.setText(String.format("%s: %s", info.name, info.version));
if (isIndexSelected(position)) {
holder.iconView.setAlpha(1f);
holder.appCheckView.setImageResource(R.drawable.ic_check);
@@ -83,6 +98,12 @@ public void onBindViewHolder(ViewHolder holder, int position) {
holder.labelView.setVisibility(View.INVISIBLE);
}
+ if (info.path == null) {
+ holder.summaryView.setVisibility(View.GONE);
+ } else {
+ holder.summaryView.setVisibility(View.VISIBLE);
+ holder.summaryView.setText(info.path);
+ }
holder.itemView.setOnClickListener(v -> {
mItemEventListener.onItemClick(info, position);
});
@@ -127,6 +148,7 @@ class ViewHolder extends RecyclerView.ViewHolder {
private TextView nameView;
private ImageView appCheckView;
private LabelView labelView;
+ private TextView summaryView;
ViewHolder(View itemView) {
super(itemView);
@@ -135,6 +157,7 @@ class ViewHolder extends RecyclerView.ViewHolder {
nameView = (TextView) itemView.findViewById(R.id.item_app_name);
appCheckView = (ImageView) itemView.findViewById(R.id.item_app_checked);
labelView = (LabelView) itemView.findViewById(R.id.item_app_clone_count);
+ summaryView = (TextView) itemView.findViewById(R.id.item_app_summary);
}
}
}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/home/adapters/LaunchpadAdapter.java b/VirtualApp/app/src/main/java/io/virtualapp/home/adapters/LaunchpadAdapter.java
deleted file mode 100644
index 3a825d7ea..000000000
--- a/VirtualApp/app/src/main/java/io/virtualapp/home/adapters/LaunchpadAdapter.java
+++ /dev/null
@@ -1,183 +0,0 @@
-package io.virtualapp.home.adapters;
-
-import android.content.Context;
-import android.support.v7.widget.RecyclerView;
-import android.util.SparseIntArray;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-import java.util.List;
-
-import io.virtualapp.R;
-import io.virtualapp.abs.ui.VUiKit;
-import io.virtualapp.home.models.AppData;
-import io.virtualapp.home.models.MultiplePackageAppData;
-import io.virtualapp.widgets.LabelView;
-import io.virtualapp.widgets.LauncherIconView;
-
-/**
- * @author Lody
- */
-public class LaunchpadAdapter extends RecyclerView.Adapter {
-
- private LayoutInflater mInflater;
- private List mList;
- private SparseIntArray mColorArray = new SparseIntArray();
- private OnAppClickListener mAppClickListener;
-
- public LaunchpadAdapter(Context context) {
- mInflater = LayoutInflater.from(context);
- }
-
- public void add(AppData model) {
- mList.add(model);
- notifyItemInserted(mList.size() - 1);
- }
-
- public void replace(int index, AppData data) {
- mList.set(index, data);
- notifyItemChanged(index);
- }
-
- public void remove(AppData data) {
- if (mList.remove(data)) {
- notifyDataSetChanged();
- }
- }
-
- @Override
- public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- return new ViewHolder(mInflater.inflate(R.layout.item_launcher_app, null));
- }
-
- @Override
- public void onBindViewHolder(ViewHolder holder, int position) {
- AppData data = mList.get(position);
- holder.color = getColor(position);
- holder.iconView.setImageDrawable(data.getIcon());
- holder.nameView.setText(data.getName());
- if (data.isFirstOpen() && !data.isLoading()) {
- holder.firstOpenDot.setVisibility(View.VISIBLE);
- } else {
- holder.firstOpenDot.setVisibility(View.INVISIBLE);
- }
- holder.itemView.setBackgroundColor(holder.color);
- holder.itemView.setOnClickListener(v -> {
- if (mAppClickListener != null) {
- mAppClickListener.onAppClick(position, data);
- }
- });
- if (data instanceof MultiplePackageAppData) {
- MultiplePackageAppData multipleData = (MultiplePackageAppData) data;
- holder.spaceLabelView.setVisibility(View.VISIBLE);
- holder.spaceLabelView.setText(multipleData.userId + 1 + "");
- } else {
- holder.spaceLabelView.setVisibility(View.INVISIBLE);
- }
- if (data.isLoading()) {
- startLoadingAnimation(holder.iconView);
- } else {
- holder.iconView.setProgress(100, false);
- }
- }
-
- private void startLoadingAnimation(LauncherIconView iconView) {
- iconView.setProgress(40, true);
- VUiKit.defer().when(() -> {
- try {
- Thread.sleep(900L);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }).done((res) -> iconView.setProgress(80, true));
- }
-
- private int getColor(int position) {
- int color = mColorArray.get(position);
- if (color == 0) {
- int type = position % 3;
- int row = position / 3;
- int rowType = row % 3;
- if (rowType == 0) {
- if (type == 0) {
- color = mInflater.getContext().getResources().getColor(R.color.desktopColorA);
- } else if (type == 1) {
- color = mInflater.getContext().getResources().getColor(R.color.desktopColorB);
- } else {
- color = mInflater.getContext().getResources().getColor(R.color.desktopColorC);
- }
- } else if (rowType == 1) {
- if (type == 0) {
- color = mInflater.getContext().getResources().getColor(R.color.desktopColorB);
- } else if (type == 1) {
- color = mInflater.getContext().getResources().getColor(R.color.desktopColorC);
- } else {
- color = mInflater.getContext().getResources().getColor(R.color.desktopColorA);
- }
- } else {
- if (type == 0) {
- color = mInflater.getContext().getResources().getColor(R.color.desktopColorC);
- } else if (type == 1) {
- color = mInflater.getContext().getResources().getColor(R.color.desktopColorA);
- } else {
- color = mInflater.getContext().getResources().getColor(R.color.desktopColorB);
- }
- }
- mColorArray.put(position, color);
- }
- return color;
- }
-
- @Override
- public int getItemCount() {
- return mList == null ? 0 : mList.size();
- }
-
- public List getList() {
- return mList;
- }
-
- public void setList(List list) {
- this.mList = list;
- notifyDataSetChanged();
- }
-
- public void setAppClickListener(OnAppClickListener mAppClickListener) {
- this.mAppClickListener = mAppClickListener;
- }
-
- public void moveItem(int pos, int targetPos) {
- AppData model = mList.remove(pos);
- mList.add(targetPos, model);
- notifyItemMoved(pos, targetPos);
- }
-
- public void refresh(AppData model) {
- int index = mList.indexOf(model);
- if (index >= 0) {
- notifyItemChanged(index);
- }
- }
-
- public interface OnAppClickListener {
- void onAppClick(int position, AppData model);
- }
-
- public class ViewHolder extends RecyclerView.ViewHolder {
- public int color;
- LauncherIconView iconView;
- TextView nameView;
- LabelView spaceLabelView;
- View firstOpenDot;
-
- ViewHolder(View itemView) {
- super(itemView);
- iconView = (LauncherIconView) itemView.findViewById(R.id.item_app_icon);
- nameView = (TextView) itemView.findViewById(R.id.item_app_name);
- spaceLabelView = (LabelView) itemView.findViewById(R.id.item_app_space_idx);
- firstOpenDot = itemView.findViewById(R.id.item_first_open_dot);
- }
- }
-}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/home/adapters/decorations/ItemOffsetDecoration.java b/VirtualApp/app/src/main/java/io/virtualapp/home/adapters/decorations/ItemOffsetDecoration.java
deleted file mode 100644
index 533a38827..000000000
--- a/VirtualApp/app/src/main/java/io/virtualapp/home/adapters/decorations/ItemOffsetDecoration.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package io.virtualapp.home.adapters.decorations;
-
-import android.content.Context;
-import android.graphics.Rect;
-import android.support.annotation.DimenRes;
-import android.support.annotation.NonNull;
-import android.support.v7.widget.RecyclerView;
-import android.view.View;
-
-public class ItemOffsetDecoration extends RecyclerView.ItemDecoration {
-
- private int mItemOffset;
-
- public ItemOffsetDecoration(int itemOffset) {
- mItemOffset = itemOffset;
- }
-
- public ItemOffsetDecoration(@NonNull Context context, @DimenRes int itemOffsetId) {
- this(context.getResources().getDimensionPixelSize(itemOffsetId));
- }
-
- @Override
- public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
- RecyclerView.State state) {
- outRect.set(mItemOffset, mItemOffset, mItemOffset, mItemOffset);
- }
-}
\ No newline at end of file
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/home/models/AppData.java b/VirtualApp/app/src/main/java/io/virtualapp/home/models/AppData.java
index b2759951f..01ceb30b2 100644
--- a/VirtualApp/app/src/main/java/io/virtualapp/home/models/AppData.java
+++ b/VirtualApp/app/src/main/java/io/virtualapp/home/models/AppData.java
@@ -8,6 +8,8 @@
public interface AppData {
+ boolean isInstalling();
+
boolean isLoading();
boolean isFirstOpen();
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/home/models/AppInfo.java b/VirtualApp/app/src/main/java/io/virtualapp/home/models/AppInfo.java
index bde5666c9..65bffbe2b 100644
--- a/VirtualApp/app/src/main/java/io/virtualapp/home/models/AppInfo.java
+++ b/VirtualApp/app/src/main/java/io/virtualapp/home/models/AppInfo.java
@@ -12,5 +12,7 @@ public class AppInfo {
public boolean fastOpen;
public Drawable icon;
public CharSequence name;
+ public CharSequence version;
public int cloneCount;
+ public boolean disableMultiVersion;
}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/home/models/AppInfoLite.java b/VirtualApp/app/src/main/java/io/virtualapp/home/models/AppInfoLite.java
index b2ad022a6..69ae476ab 100644
--- a/VirtualApp/app/src/main/java/io/virtualapp/home/models/AppInfoLite.java
+++ b/VirtualApp/app/src/main/java/io/virtualapp/home/models/AppInfoLite.java
@@ -23,17 +23,20 @@ public AppInfoLite[] newArray(int size) {
public String packageName;
public String path;
public boolean fastOpen;
+ public boolean disableMultiVersion;
- public AppInfoLite(String packageName, String path, boolean fastOpen) {
+ public AppInfoLite(String packageName, String path, boolean fastOpen, boolean disableMultiVersion) {
this.packageName = packageName;
this.path = path;
this.fastOpen = fastOpen;
+ this.disableMultiVersion = disableMultiVersion;
}
protected AppInfoLite(Parcel in) {
this.packageName = in.readString();
this.path = in.readString();
this.fastOpen = in.readByte() != 0;
+ this.disableMultiVersion = in.readByte() != 0;
}
@Override
@@ -46,5 +49,6 @@ public void writeToParcel(Parcel dest, int flags) {
dest.writeString(this.packageName);
dest.writeString(this.path);
dest.writeByte(this.fastOpen ? (byte) 1 : (byte) 0);
+ dest.writeByte(this.disableMultiVersion ? (byte) 1 : (byte) 0);
}
}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/home/models/EmptyAppData.java b/VirtualApp/app/src/main/java/io/virtualapp/home/models/EmptyAppData.java
deleted file mode 100644
index 6a3b05311..000000000
--- a/VirtualApp/app/src/main/java/io/virtualapp/home/models/EmptyAppData.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package io.virtualapp.home.models;
-
-import android.graphics.drawable.Drawable;
-
-/**
- * @author Lody
- */
-
-public class EmptyAppData implements AppData {
-
- @Override
- public boolean isLoading() {
- return false;
- }
-
- @Override
- public boolean isFirstOpen() {
- return false;
- }
-
- @Override
- public Drawable getIcon() {
- return null;
- }
-
- @Override
- public String getName() {
- return null;
- }
-
- @Override
- public boolean canReorder() {
- return false;
- }
-
- @Override
- public boolean canLaunch() {
- return false;
- }
-
- @Override
- public boolean canDelete() {
- return false;
- }
-
- @Override
- public boolean canCreateShortcut() {
- return false;
- }
-}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/home/models/MultiplePackageAppData.java b/VirtualApp/app/src/main/java/io/virtualapp/home/models/MultiplePackageAppData.java
index 9785be668..f3af71db0 100644
--- a/VirtualApp/app/src/main/java/io/virtualapp/home/models/MultiplePackageAppData.java
+++ b/VirtualApp/app/src/main/java/io/virtualapp/home/models/MultiplePackageAppData.java
@@ -14,6 +14,7 @@ public class MultiplePackageAppData implements AppData {
public InstalledAppInfo appInfo;
public int userId;
public boolean isFirstOpen;
+ public boolean isInstalling;
public boolean isLoading;
public Drawable icon;
public String name;
@@ -31,6 +32,11 @@ public MultiplePackageAppData(PackageAppData target, int userId) {
name = target.name;
}
+ @Override
+ public boolean isInstalling() {
+ return isInstalling;
+ }
+
@Override
public boolean isLoading() {
return isLoading;
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/home/models/PackageAppData.java b/VirtualApp/app/src/main/java/io/virtualapp/home/models/PackageAppData.java
index d65acf3c7..fa06ad426 100644
--- a/VirtualApp/app/src/main/java/io/virtualapp/home/models/PackageAppData.java
+++ b/VirtualApp/app/src/main/java/io/virtualapp/home/models/PackageAppData.java
@@ -18,6 +18,7 @@ public class PackageAppData implements AppData {
public boolean fastOpen;
public boolean isFirstOpen;
public boolean isLoading;
+ public boolean isInstalling;
public PackageAppData(Context context, InstalledAppInfo installedAppInfo) {
this.packageName = installedAppInfo.packageName;
@@ -25,6 +26,11 @@ public PackageAppData(Context context, InstalledAppInfo installedAppInfo) {
loadData(context, installedAppInfo.getApplicationInfo(installedAppInfo.getInstalledUsers()[0]));
}
+ public PackageAppData(Context context, ApplicationInfo appInfo) {
+ this.packageName = appInfo.packageName;
+ loadData(context, appInfo);
+ }
+
private void loadData(Context context, ApplicationInfo appInfo) {
if (appInfo == null) {
return;
@@ -41,6 +47,11 @@ private void loadData(Context context, ApplicationInfo appInfo) {
}
}
+ @Override
+ public boolean isInstalling() {
+ return isInstalling;
+ }
+
@Override
public boolean isLoading() {
return isLoading;
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/home/platform/PlatformInfo.java b/VirtualApp/app/src/main/java/io/virtualapp/home/platform/PlatformInfo.java
deleted file mode 100644
index 44d1417bb..000000000
--- a/VirtualApp/app/src/main/java/io/virtualapp/home/platform/PlatformInfo.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package io.virtualapp.home.platform;
-
-import android.content.pm.PackageInfo;
-
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Set;
-
-/**
- * @author Lody
- */
-public abstract class PlatformInfo {
-
- private final Set platformPkgs = new HashSet<>();
-
- public PlatformInfo(String... pkgs) {
- Collections.addAll(platformPkgs, pkgs);
- }
-
- public abstract boolean relyOnPlatform(PackageInfo info);
-
-}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/home/platform/WechatPlatformInfo.java b/VirtualApp/app/src/main/java/io/virtualapp/home/platform/WechatPlatformInfo.java
deleted file mode 100644
index fab6c2c08..000000000
--- a/VirtualApp/app/src/main/java/io/virtualapp/home/platform/WechatPlatformInfo.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package io.virtualapp.home.platform;
-
-import android.content.pm.ActivityInfo;
-import android.content.pm.PackageInfo;
-
-/**
- * @author Lody
- */
-
-public class WechatPlatformInfo extends PlatformInfo {
-
- public WechatPlatformInfo() {
- super("com.tencent.mm");
- }
-
- @Override
- public boolean relyOnPlatform(PackageInfo info) {
- if (info.activities == null) {
- return false;
- }
- for (ActivityInfo activityInfo : info.activities) {
- if (activityInfo.name.endsWith("WXEntryActivity")) {
- return true;
- }
- }
- return false;
- }
-}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/home/repo/AppRepository.java b/VirtualApp/app/src/main/java/io/virtualapp/home/repo/AppRepository.java
index 383891a81..89063ca2e 100644
--- a/VirtualApp/app/src/main/java/io/virtualapp/home/repo/AppRepository.java
+++ b/VirtualApp/app/src/main/java/io/virtualapp/home/repo/AppRepository.java
@@ -5,8 +5,10 @@
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
+import com.lody.virtual.GmsSupport;
import com.lody.virtual.client.core.InstallStrategy;
import com.lody.virtual.client.core.VirtualCore;
+import com.lody.virtual.helper.utils.DeviceUtil;
import com.lody.virtual.remote.InstallResult;
import com.lody.virtual.remote.InstalledAppInfo;
@@ -16,6 +18,7 @@
import java.text.Collator;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import java.util.Locale;
@@ -25,6 +28,7 @@
import io.virtualapp.home.models.AppInfoLite;
import io.virtualapp.home.models.MultiplePackageAppData;
import io.virtualapp.home.models.PackageAppData;
+import io.virtualapp.utils.HanziToPinyin;
/**
* @author Lody
@@ -42,6 +46,8 @@ public class AppRepository implements AppDataSource {
"pp/downloader/apk",
"pp/downloader/silent/apk");
+ private static final int MAX_SCAN_DEPTH = 2;
+
private Context mContext;
public AppRepository(Context context) {
@@ -49,7 +55,8 @@ public AppRepository(Context context) {
}
private static boolean isSystemApplication(PackageInfo packageInfo) {
- return (packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
+ return (packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0
+ && !GmsSupport.isGmsFamilyPackage(packageInfo.packageName);
}
@Override
@@ -78,12 +85,53 @@ public Promise, Throwable, Void> getVirtualApps() {
@Override
public Promise, Throwable, Void> getInstalledApps(Context context) {
- return VUiKit.defer().when(() -> convertPackageInfoToAppData(context, context.getPackageManager().getInstalledPackages(0), true));
+ return VUiKit.defer().when(() -> convertPackageInfoToAppData(context, context.getPackageManager().getInstalledPackages(PackageManager.GET_META_DATA), true));
}
@Override
public Promise, Throwable, Void> getStorageApps(Context context, File rootDir) {
- return VUiKit.defer().when(() -> convertPackageInfoToAppData(context, findAndParseAPKs(context, rootDir, SCAN_PATH_LIST), false));
+ // return VUiKit.defer().when(() -> convertPackageInfoToAppData(context, findAndParseAPKs(context, rootDir, SCAN_PATH_LIST), false));
+ return VUiKit.defer().when(() -> convertPackageInfoToAppData(context, findAndParseApkRecursively(context, rootDir,null, 0), false));
+ }
+
+ private List findAndParseApkRecursively(Context context, File rootDir, List result, int depth) {
+ if (result == null) {
+ result = new ArrayList<>();
+ }
+
+ if (depth > MAX_SCAN_DEPTH) {
+ return result;
+ }
+
+ File[] dirFiles = rootDir.listFiles();
+
+ if (dirFiles == null) {
+ return Collections.emptyList();
+ }
+
+ for (File f: dirFiles) {
+ if (f.isDirectory()) {
+ List andParseApkRecursively = findAndParseApkRecursively(context, f, new ArrayList<>(), depth + 1);
+ result.addAll(andParseApkRecursively);
+ }
+
+ if (!(f.isFile() && f.getName().toLowerCase().endsWith(".apk"))) {
+ continue;
+ }
+
+ PackageInfo pkgInfo = null;
+ try {
+ pkgInfo = context.getPackageManager().getPackageArchiveInfo(f.getAbsolutePath(), PackageManager.GET_META_DATA);
+ pkgInfo.applicationInfo.sourceDir = f.getAbsolutePath();
+ pkgInfo.applicationInfo.publicSourceDir = f.getAbsolutePath();
+ } catch (Exception e) {
+ // Ignore
+ }
+ if (pkgInfo != null) {
+ result.add(pkgInfo);
+ }
+ }
+ return result;
}
private List findAndParseAPKs(Context context, File rootDir, List paths) {
@@ -121,6 +169,12 @@ private List convertPackageInfoToAppData(Context context, List convertPackageInfoToAppData(Context context, List {
+ HanziToPinyin hanziToPinyin = HanziToPinyin.getInstance();
+ String pinyin1 = hanziToPinyin.toPinyinString(o1.name.toString().trim());
+ String pinyin2 = hanziToPinyin.toPinyinString(o2.name.toString().trim());
+ return pinyin1.compareTo(pinyin2);
+ });
return list;
}
@Override
public InstallResult addVirtualApp(AppInfoLite info) {
- int flags = InstallStrategy.COMPARE_VERSION | InstallStrategy.ART_FLY_MODE;
+ int flags = InstallStrategy.COMPARE_VERSION | InstallStrategy.SKIP_DEX_OPT;
+ info.fastOpen = false; // disable fast open for compile.
+ if (DeviceUtil.isMeizuBelowN()) {
+ info.fastOpen = true;
+ }
if (info.fastOpen) {
flags |= InstallStrategy.DEPEND_SYSTEM_IF_EXIST;
}
+ if (info.disableMultiVersion) {
+ flags |= InstallStrategy.UPDATE_IF_EXIST;
+ }
return VirtualCore.get().installPackage(info.path, flags);
}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/home/repo/PackageAppDataStorage.java b/VirtualApp/app/src/main/java/io/virtualapp/home/repo/PackageAppDataStorage.java
index ea2be27d0..b5f105044 100644
--- a/VirtualApp/app/src/main/java/io/virtualapp/home/repo/PackageAppDataStorage.java
+++ b/VirtualApp/app/src/main/java/io/virtualapp/home/repo/PackageAppDataStorage.java
@@ -1,12 +1,14 @@
package io.virtualapp.home.repo;
+import android.content.pm.ApplicationInfo;
+
import com.lody.virtual.client.core.VirtualCore;
import com.lody.virtual.remote.InstalledAppInfo;
import java.util.HashMap;
import java.util.Map;
-import io.virtualapp.VApp;
+import io.virtualapp.XApp;
import io.virtualapp.abs.Callback;
import io.virtualapp.abs.ui.VUiKit;
import io.virtualapp.home.models.PackageAppData;
@@ -45,7 +47,7 @@ public void acquire(String packageName, Callback callback) {
private PackageAppData loadAppData(String packageName) {
InstalledAppInfo setting = VirtualCore.get().getInstalledAppInfo(packageName, 0);
if (setting != null) {
- PackageAppData data = new PackageAppData(VApp.getApp(), setting);
+ PackageAppData data = new PackageAppData(XApp.getApp(), setting);
synchronized (packageDataMap) {
packageDataMap.put(packageName, data);
}
@@ -54,4 +56,29 @@ private PackageAppData loadAppData(String packageName) {
return null;
}
+ public PackageAppData acquire(ApplicationInfo appInfo) {
+ PackageAppData data;
+ synchronized (packageDataMap) {
+ data = packageDataMap.get(appInfo.packageName);
+ if (data == null) {
+ data = loadAppData(appInfo);
+ }
+ }
+ return data;
+ }
+
+ public void acquire(ApplicationInfo appInfo, Callback callback) {
+ VUiKit.defer()
+ .when(() -> acquire(appInfo))
+ .done(callback::callback);
+ }
+
+ private PackageAppData loadAppData(ApplicationInfo appInfo) {
+ PackageAppData data = new PackageAppData(XApp.getApp(), appInfo);
+ synchronized (packageDataMap) {
+ packageDataMap.put(appInfo.packageName, data);
+ }
+ return data;
+ }
+
}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/settings/AboutActivity.java b/VirtualApp/app/src/main/java/io/virtualapp/settings/AboutActivity.java
new file mode 100644
index 000000000..161c51350
--- /dev/null
+++ b/VirtualApp/app/src/main/java/io/virtualapp/settings/AboutActivity.java
@@ -0,0 +1,184 @@
+package io.virtualapp.settings;
+
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v7.app.AlertDialog;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.Toast;
+
+import java.util.Calendar;
+
+import io.virtualapp.R;
+import io.virtualapp.abs.ui.VActivity;
+import io.virtualapp.update.VAVersionService;
+import mehdi.sakout.aboutpage.AboutPage;
+import mehdi.sakout.aboutpage.Element;
+
+/**
+ * author: weishu on 18/1/12.
+ */
+public class AboutActivity extends VActivity {
+
+ private AboutPage mPage;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mPage = new AboutPage(this)
+ .isRTL(false)
+ .setImage(R.mipmap.ic_launcher)
+ .addItem(getCopyRightsElement())
+ .addItem(getVersionElement())
+ .addItem(getCheckUpdateElement())
+ .addItem(getFeedbackEmailElement())
+ .addItem(getThanksElement())
+ .addItem(getFeedbacTelegramElement())
+ .addItem(getWebsiteElement())
+ .addGitHub("tiann");
+
+ View aboutPage = mPage.create();
+
+ setContentView(aboutPage);
+ }
+
+ Element getCopyRightsElement() {
+ Element copyRightsElement = new Element();
+ final String copyrights = String.format(getString(R.string.copy_right), Calendar.getInstance().get(Calendar.YEAR));
+ copyRightsElement.setTitle(copyrights);
+ copyRightsElement.setGravity(Gravity.START);
+ return copyRightsElement;
+ }
+
+ Element getVersionElement() {
+ Element version = new Element();
+ String versionName = "unknown";
+ try {
+ PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
+ versionName = packageInfo.versionName;
+ } catch (PackageManager.NameNotFoundException ignored) {
+ }
+ version.setTitle(getResources().getString(R.string.about_version_title, versionName));
+
+ final int[] clickCount = {0};
+ version.setOnClickListener(v -> {
+ clickCount[0]++;
+ if (clickCount[0] == 3) {
+ mPage.addItem(getFeedbackQQElement());
+ mPage.addItem(getFeedbackWechatElement());
+ }
+ });
+ return version;
+ }
+
+ Element getFeedbackQQElement() {
+ Element feedback = new Element();
+ final String qqGroup = "597478474";
+ feedback.setTitle(getResources().getString(R.string.about_feedback_qq_title));
+
+ feedback.setOnClickListener(v -> {
+ ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
+ if (clipboardManager != null) {
+ clipboardManager.setPrimaryClip(ClipData.newPlainText(null, qqGroup));
+ }
+ Toast.makeText(v.getContext(), getResources().getString(R.string.about_feedback_tips), Toast.LENGTH_SHORT).show();
+ });
+ return feedback;
+ }
+
+ Element getFeedbackEmailElement() {
+ Element emailElement = new Element();
+ final String email = "virtualxposed@gmail.com";
+ String title = getResources().getString(R.string.about_feedback_title);
+ emailElement.setTitle(title);
+
+ Uri uri = Uri.parse("mailto:" + email);
+ Intent intent = new Intent(Intent.ACTION_SENDTO, uri);
+ intent.putExtra(Intent.EXTRA_SUBJECT, title); // 主题
+
+ String hint = getResources().getString(R.string.about_feedback_hint);
+ intent.putExtra(Intent.EXTRA_TEXT, hint);
+ emailElement.setIntent(intent);
+ return emailElement;
+ }
+
+ Element getFeedbackWechatElement() {
+ Element feedback = new Element();
+ // final String weChatGroup = "CSYJZF";
+ feedback.setTitle(getResources().getString(R.string.about_feedback_wechat_title));
+
+ feedback.setOnClickListener(v -> {
+ ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
+ if (clipboardManager != null) {
+ clipboardManager.setPrimaryClip(ClipData.newPlainText(null, "VirtualXposed"));
+ }
+ Toast.makeText(v.getContext(), getResources().getString(R.string.about_feedback_tips), Toast.LENGTH_SHORT).show();
+ });
+ return feedback;
+ }
+
+ Element getFeedbacTelegramElement() {
+ Element feedback = new Element();
+ final String weChatGroup = "VirtualXposed";
+ feedback.setTitle(getResources().getString(R.string.about_feedback_tel_title, weChatGroup));
+
+ feedback.setOnClickListener(v -> {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse("https://t.me/joinchat/Gtti8Usj1JD4TchHQmy-ew"));
+ try {
+ startActivity(intent);
+ } catch (Throwable ignored) {
+ }
+ });
+ return feedback;
+ }
+
+ Element getWebsiteElement() {
+ Element feedback = new Element();
+ feedback.setTitle(getResources().getString(R.string.about_website_title));
+
+ feedback.setOnClickListener(v -> {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse("http://vxposed.com"));
+ try {
+ startActivity(intent);
+ } catch (Throwable ignored) {
+ }
+ });
+ return feedback;
+ }
+
+ Element getThanksElement() {
+ Element thanks = new Element();
+ thanks.setTitle(getResources().getString(R.string.about_thanks));
+ thanks.setOnClickListener(v -> {
+ AlertDialog alertDialog = new AlertDialog.Builder(this, R.style.Theme_AppCompat_DayNight_Dialog_Alert)
+ .setTitle(R.string.thanks_dialog_title)
+ .setMessage(R.string.thanks_dialog_content)
+ .setPositiveButton(R.string.about_icon_yes, null)
+ .create();
+ try {
+ alertDialog.show();
+ } catch (Throwable ignored) {
+ // BadTokenException.
+ }
+ });
+ return thanks;
+ }
+
+ Element getCheckUpdateElement() {
+ Element checkUpdate = new Element();
+ checkUpdate.setTitle(getResources().getString(R.string.check_update));
+ checkUpdate.setOnClickListener(v -> {
+ VAVersionService.checkUpdateImmediately(getApplicationContext(), true);
+ });
+ return checkUpdate;
+ }
+}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/settings/AppManageActivity.java b/VirtualApp/app/src/main/java/io/virtualapp/settings/AppManageActivity.java
new file mode 100644
index 000000000..5e7f9923a
--- /dev/null
+++ b/VirtualApp/app/src/main/java/io/virtualapp/settings/AppManageActivity.java
@@ -0,0 +1,322 @@
+package io.virtualapp.settings;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.PopupMenu;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.lody.virtual.client.core.VirtualCore;
+import com.lody.virtual.client.ipc.VirtualStorageManager;
+import com.lody.virtual.helper.ArtDexOptimizer;
+import com.lody.virtual.os.VEnvironment;
+import com.lody.virtual.remote.InstalledAppInfo;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import io.virtualapp.R;
+import io.virtualapp.abs.ui.VActivity;
+import io.virtualapp.abs.ui.VUiKit;
+import io.virtualapp.glide.GlideUtils;
+
+/**
+ * @author weishu
+ * @date 18/2/15.
+ */
+
+public class AppManageActivity extends VActivity {
+
+ private ListView mListView;
+ private List mInstalledApps = new ArrayList<>();
+ private AppManageAdapter mAdapter;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_list);
+ mListView = (ListView) findViewById(R.id.list);
+ mAdapter = new AppManageAdapter();
+ mListView.setAdapter(mAdapter);
+
+ mListView.setOnItemClickListener((parent, view, position, id) -> {
+ AppManageInfo appManageInfo = mInstalledApps.get(position);
+ showContextMenu(appManageInfo, view);
+ });
+ loadAsync();
+ }
+
+ private void loadAsync() {
+ VUiKit.defer().when(this::loadApp).done((v) -> mAdapter.notifyDataSetChanged());
+ }
+
+ private void loadApp() {
+
+ List ret = new ArrayList<>();
+ List installedApps = VirtualCore.get().getInstalledApps(0);
+ PackageManager packageManager = getPackageManager();
+ for (InstalledAppInfo installedApp : installedApps) {
+ int[] installedUsers = installedApp.getInstalledUsers();
+ for (int installedUser : installedUsers) {
+ AppManageInfo info = new AppManageInfo();
+ info.userId = installedUser;
+ ApplicationInfo applicationInfo = installedApp.getApplicationInfo(installedUser);
+ info.name = applicationInfo.loadLabel(packageManager);
+// info.icon = applicationInfo.loadIcon(packageManager); //Use Glide to load icon async
+ info.pkgName = installedApp.packageName;
+ info.path = applicationInfo.sourceDir;
+ ret.add(info);
+ }
+ }
+ mInstalledApps.clear();
+ mInstalledApps.addAll(ret);
+ }
+
+ class AppManageAdapter extends BaseAdapter {
+
+ @Override
+ public int getCount() {
+ return mInstalledApps.size();
+ }
+
+ @Override
+ public AppManageInfo getItem(int position) {
+ return mInstalledApps.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return 0;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ ViewHolder holder;
+ if (convertView == null) {
+ holder = new ViewHolder(AppManageActivity.this, parent);
+ convertView = holder.root;
+ convertView.setTag(holder);
+ } else {
+ holder = (ViewHolder) convertView.getTag();
+ }
+
+ AppManageInfo item = getItem(position);
+
+ holder.label.setText(item.getName());
+
+ if (VirtualCore.get().isOutsideInstalled(item.pkgName)) {
+ GlideUtils.loadInstalledPackageIcon(getContext(), item.pkgName, holder.icon, android.R.drawable.sym_def_app_icon);
+ } else {
+ GlideUtils.loadPackageIconFromApkFile(getContext(), item.path, holder.icon, android.R.drawable.sym_def_app_icon);
+ }
+
+ holder.button.setOnClickListener(v -> showContextMenu(item, v));
+
+ return convertView;
+ }
+ }
+
+ private void showContextMenu(AppManageInfo appManageInfo, View anchor) {
+ if (appManageInfo == null) {
+ return;
+ }
+ PopupMenu popupMenu = new PopupMenu(this, anchor);
+ popupMenu.inflate(R.menu.app_manage_menu);
+ MenuItem redirectMenu = popupMenu.getMenu().findItem(R.id.action_redirect);
+
+ try {
+ final String packageName = appManageInfo.pkgName;
+ final int userId = appManageInfo.userId;
+ boolean virtualStorageEnable = VirtualStorageManager.get().isVirtualStorageEnable(packageName, userId);
+ redirectMenu.setTitle(virtualStorageEnable ? R.string.app_manage_redirect_off : R.string.app_manage_redirect_on);
+ } catch (Throwable e) {
+ redirectMenu.setVisible(false);
+ }
+
+ popupMenu.setOnMenuItemClickListener(item -> {
+ switch (item.getItemId()) {
+ case R.id.action_uninstall:
+ showUninstallDialog(appManageInfo, appManageInfo.getName());
+ break;
+ case R.id.action_repair:
+ showRepairDialog(appManageInfo);
+ break;
+ case R.id.action_redirect:
+ showStorageRedirectDialog(appManageInfo);
+ break;
+ }
+ return false;
+ });
+ try {
+ popupMenu.show();
+ } catch (Throwable ignored) {
+ }
+ }
+
+ private void showRepairDialog(AppManageInfo item) {
+ ProgressDialog dialog = new ProgressDialog(this);
+ dialog.setTitle(getResources().getString(R.string.app_manage_repairing));
+ try {
+ dialog.setCancelable(false);
+ dialog.show();
+ } catch (Throwable e) {
+ return;
+ }
+
+ VUiKit.defer().when(() -> {
+ NougatPolicy.fullCompile(getApplicationContext());
+
+ String packageName = item.pkgName;
+ String apkPath = item.path;
+
+ if (TextUtils.isEmpty(packageName) || TextUtils.isEmpty(apkPath)) {
+ return;
+ }
+
+ // 1. kill package
+ VirtualCore.get().killApp(packageName, item.userId);
+
+ // 2. backup the odex file
+ File odexFile = VEnvironment.getOdexFile(packageName);
+ if (odexFile.delete()) {
+ try {
+ ArtDexOptimizer.compileDex2Oat(apkPath, odexFile.getPath());
+ } catch (IOException e) {
+ throw new RuntimeException("compile failed.");
+ }
+ }
+ }).done((v) -> {
+ dismiss(dialog);
+ showAppDetailDialog();
+ }).fail((v) -> {
+ dismiss(dialog);
+ Toast.makeText(this, R.string.app_manage_repair_failed_tips, Toast.LENGTH_SHORT).show();
+ });
+ }
+
+ private void showAppDetailDialog() {
+ AlertDialog alertDialog = new AlertDialog.Builder(AppManageActivity.this)
+ .setTitle(R.string.app_manage_repair_success_title)
+ .setMessage(getResources().getString(R.string.app_manage_repair_success_content))
+ .setPositiveButton(R.string.app_manage_repair_reboot_now, (dialog, which) -> {
+ String packageName = getPackageName();
+ Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
+ Uri.fromParts("package", packageName, null));
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ startActivity(intent);
+ })
+ .create();
+
+ alertDialog.setCancelable(false);
+
+ try {
+ alertDialog.show();
+ } catch (Throwable ignored) {
+ }
+ }
+
+ private void showUninstallDialog(AppManageInfo item, CharSequence name) {
+ AlertDialog alertDialog = new AlertDialog.Builder(AppManageActivity.this)
+ .setTitle(com.android.launcher3.R.string.home_menu_delete_title)
+ .setMessage(getResources().getString(com.android.launcher3.R.string.home_menu_delete_content, name))
+ .setPositiveButton(android.R.string.yes, (dialog, which) -> {
+ VirtualCore.get().uninstallPackageAsUser(item.pkgName, item.userId);
+ loadAsync();
+ })
+ .setNegativeButton(android.R.string.no, null)
+ .create();
+ try {
+ alertDialog.show();
+ } catch (Throwable ignored) {
+ }
+ }
+
+ private void showStorageRedirectDialog(AppManageInfo item) {
+ final String packageName = item.pkgName;
+ final int userId = item.userId;
+ boolean virtualStorageEnable;
+ try {
+ virtualStorageEnable = VirtualStorageManager.get().isVirtualStorageEnable(packageName, userId);
+ } catch (Throwable e) {
+ return;
+ }
+
+ AlertDialog alertDialog = new AlertDialog.Builder(AppManageActivity.this)
+ .setTitle(virtualStorageEnable ? R.string.app_manage_redirect_off : R.string.app_manage_redirect_on)
+ .setMessage(getResources().getString(R.string.app_manage_redirect_desc))
+ .setPositiveButton(virtualStorageEnable ? R.string.app_manage_redirect_off_confirm : R.string.app_manage_redirect_on_confirm,
+ (dialog, which) -> {
+ try {
+ VirtualStorageManager.get().setVirtualStorageState(packageName, userId, !virtualStorageEnable);
+ } catch (Throwable ignored) {
+ }
+ })
+ .setNegativeButton(android.R.string.no, null)
+ .create();
+ try {
+ alertDialog.show();
+ } catch (Throwable ignored) {
+ }
+ }
+
+ static class ViewHolder {
+ ImageView icon;
+ TextView label;
+ ImageView button;
+
+ View root;
+
+ ViewHolder(Context context, ViewGroup parent) {
+ root = LayoutInflater.from(context).inflate(R.layout.item_app_manage, parent, false);
+ icon = root.findViewById(R.id.item_app_icon);
+ label = root.findViewById(R.id.item_app_name);
+ button = root.findViewById(R.id.item_app_button);
+ }
+ }
+
+ static class AppManageInfo {
+ CharSequence name;
+ int userId;
+ Drawable icon;
+ String pkgName;
+ String path;
+
+ CharSequence getName() {
+ if (userId == 0) {
+ return name;
+ } else {
+ return name + "[" + (userId + 1) + "]";
+ }
+ }
+ }
+
+ private static void dismiss(Dialog dialog) {
+ if (dialog == null) {
+ return;
+ }
+ try {
+ dialog.dismiss();
+ } catch (Throwable ignored) {
+ }
+ }
+}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/settings/NougatPolicy.java b/VirtualApp/app/src/main/java/io/virtualapp/settings/NougatPolicy.java
new file mode 100644
index 000000000..92818e4bd
--- /dev/null
+++ b/VirtualApp/app/src/main/java/io/virtualapp/settings/NougatPolicy.java
@@ -0,0 +1,59 @@
+package io.virtualapp.settings;
+
+import android.content.Context;
+import android.os.Build;
+
+import java.lang.reflect.Method;
+
+/**
+ * Android 7.0 全量编译策略
+ * Created by weishu on 17/6/12.
+ */
+
+public class NougatPolicy {
+
+ static boolean fullCompile(Context context) {
+ if (Build.VERSION.SDK_INT < 24) {
+ return true;
+ }
+ try {
+ Object pm = getPackageManagerBinderProxy();
+ if (pm == null) {
+ return false;
+ }
+ /*
+ @Override
+ public boolean performDexOptMode(String packageName,
+ boolean checkProfiles, String targetCompilerFilter, boolean force) {
+ int dexOptStatus = performDexOptTraced(packageName, checkProfiles,
+ targetCompilerFilter, force);
+ return dexOptStatus != PackageDexOptimizer.DEX_OPT_FAILED;
+ */
+
+ final Method performDexOptMode = pm.getClass().getDeclaredMethod("performDexOptMode",
+ String.class, boolean.class, String.class, boolean.class);
+ return (boolean) performDexOptMode.invoke(pm, context.getPackageName(), false, "speed", true);
+ } catch (Throwable e) {
+ return false;
+ }
+ }
+
+ public static boolean clearCompileData(Context context) {
+ boolean ret;
+ try {
+ Object pm = getPackageManagerBinderProxy();
+ final Method performDexOpt = pm.getClass().getDeclaredMethod("performDexOpt", String.class,
+ boolean.class, int.class, boolean.class);
+ ret = (Boolean) performDexOpt.invoke(pm, context.getPackageName(), false, 2 /*install*/, true);
+ } catch (Throwable e) {
+ ret = false;
+ }
+ return ret;
+ }
+
+ private static Object getPackageManagerBinderProxy() throws Exception {
+ Class> activityThread = Class.forName("android.app.ActivityThread");
+ final Method getPackageManager = activityThread.getDeclaredMethod("getPackageManager");
+ return getPackageManager.invoke(null);
+ }
+}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/settings/OnlinePlugin.java b/VirtualApp/app/src/main/java/io/virtualapp/settings/OnlinePlugin.java
new file mode 100644
index 000000000..170c86565
--- /dev/null
+++ b/VirtualApp/app/src/main/java/io/virtualapp/settings/OnlinePlugin.java
@@ -0,0 +1,199 @@
+package io.virtualapp.settings;
+
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.os.SystemClock;
+import android.support.v7.app.AlertDialog;
+import android.text.TextUtils;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.lody.virtual.client.core.InstallStrategy;
+import com.lody.virtual.client.core.VirtualCore;
+import com.lody.virtual.os.VEnvironment;
+import com.lody.virtual.remote.InstallResult;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import io.virtualapp.R;
+import io.virtualapp.gms.FakeGms;
+import io.virtualapp.home.LoadingActivity;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+
+import static io.virtualapp.utils.DialogUtil.showDialog;
+
+/**
+ * @author weishu
+ * @date 2018/7/5.
+ */
+public class OnlinePlugin {
+
+ private static final String TAG = "OnlinePlugin";
+
+ public static final String FILE_MANAGE_PACKAGE = "com.amaze.filemanager";
+ public static final String FILE_MANAGE_URL = "http://vaexposed.weishu.me/amaze.json";
+
+ public static final String PERMISSION_MANAGE_PACKAGE = "eu.faircode.xlua";
+ public static final String PERMISSION_MANAGE_URL = "http://vaexposed.weishu.me/xlua.json";
+
+ public static void openOrDownload(Activity context, String packageName, String url, String tips) {
+ if (context == null || packageName == null) {
+ return;
+ }
+
+ if (VirtualCore.get().isAppInstalled(packageName)) {
+ LoadingActivity.launch(context, packageName, 0);
+ return;
+ }
+
+ AlertDialog failDialog = new AlertDialog.Builder(context, R.style.Theme_AppCompat_DayNight_Dialog_Alert)
+ .setTitle(android.R.string.dialog_alert_title)
+ .setMessage(tips)
+ .setPositiveButton(android.R.string.ok, ((dialog1, which1) -> {
+ ProgressDialog progressDialog = new ProgressDialog(context);
+ progressDialog.setCancelable(false);
+ progressDialog.show();
+
+ Executors.newSingleThreadExecutor().submit(() -> {
+ String error = downloadAndInstall(context, progressDialog, url, packageName);
+ try {
+ progressDialog.dismiss();
+ } catch (Throwable e) {
+ e.printStackTrace();
+ }
+
+ if (error == null) {
+ context.runOnUiThread(() -> {
+ LoadingActivity.launch(context, packageName, 0);
+ });
+ } else {
+ context.runOnUiThread(() -> Toast.makeText(context, error, Toast.LENGTH_SHORT).show());
+ }
+
+ });
+
+ }))
+ .setNegativeButton(android.R.string.cancel, null)
+ .create();
+
+ showDialog(failDialog);
+ }
+
+ private static String downloadAndInstall(Activity activity, ProgressDialog dialog, String url, String packageName) {
+ OkHttpClient client = new OkHttpClient.Builder()
+ .connectTimeout(30, TimeUnit.SECONDS)
+ .readTimeout(30, TimeUnit.SECONDS)
+ .writeTimeout(30, TimeUnit.SECONDS)
+ .build();
+
+ Request request = new Request.Builder()
+ .url(url)
+ .build();
+
+ updateMessage(activity, dialog, "Prepare download...");
+ Response response;
+ try {
+ response = client.newCall(request).execute();
+ } catch (IOException e) {
+ return "Download failed, please check your network, error: 0";
+ }
+
+ if (!response.isSuccessful()) {
+ return "Download failed, please check your network, error: 1";
+ }
+
+ Log.i(TAG, "response success: " + response.code());
+ if (200 != response.code()) {
+ return "Download failed, please check your network, error: 2";
+ }
+
+ updateMessage(activity, dialog, "Parsing config...");
+ ResponseBody body = response.body();
+ if (body == null) {
+ return "Download failed, please check your network, error: 3";
+ }
+
+ String string;
+ try {
+ string = body.string();
+ } catch (IOException e) {
+ return "Download failed, please check your network, error: 4";
+ }
+
+ JSONObject jsonObject;
+ try {
+ jsonObject = new JSONObject(string);
+ } catch (JSONException e) {
+ return "Download failed, please check your network, error: 5";
+ }
+ String downloadLink;
+ boolean isXposed;
+ try {
+ downloadLink = jsonObject.getString("url");
+ isXposed = jsonObject.optBoolean("xposed", false);
+ } catch (JSONException e) {
+ return "Download failed, please check your network, error: 6";
+ }
+
+ File outFile = new File(activity.getCacheDir(), packageName + ".apk");
+ FakeGms.downloadFile(downloadLink, outFile, progress -> updateMessage(activity, dialog, "download " + packageName + "..." + progress + "%"));
+
+ updateMessage(activity, dialog, "installing " + packageName);
+
+ InstallResult installResult = VirtualCore.get().installPackage(outFile.getAbsolutePath(), InstallStrategy.UPDATE_IF_EXIST);
+ if (!installResult.isSuccess) {
+ return "install " + packageName + " failed: " + installResult.error;
+ }
+
+ if (isXposed) {
+ // Enable the Xposed module.
+ updateMessage(activity, dialog, "enable " + packageName + " in Xposed Installer");
+
+ File dataDir = VEnvironment.getDataUserPackageDirectory(0, "de.robv.android.xposed.installer");
+ File modulePath = VEnvironment.getPackageResourcePath(packageName);
+ File configDir = new File(dataDir, "exposed_conf" + File.separator + "modules.list");
+ FileWriter writer = null;
+ try {
+ writer = new FileWriter(configDir, true);
+ writer.append(modulePath.getAbsolutePath());
+ writer.flush();
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ if (writer != null) {
+ try {
+ writer.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ updateMessage(activity, dialog, " install success!!");
+ SystemClock.sleep(300);
+
+ return null;
+ }
+
+ private static void updateMessage(Activity activity, ProgressDialog dialog, String msg) {
+ if (activity == null || dialog == null || TextUtils.isEmpty(msg)) {
+ return;
+ }
+ Log.i(TAG, "update dialog message: " + msg);
+ activity.runOnUiThread(() -> {
+ dialog.setMessage(msg);
+ });
+ }
+}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/settings/RecommendPluginActivity.java b/VirtualApp/app/src/main/java/io/virtualapp/settings/RecommendPluginActivity.java
new file mode 100644
index 000000000..6f7316b2a
--- /dev/null
+++ b/VirtualApp/app/src/main/java/io/virtualapp/settings/RecommendPluginActivity.java
@@ -0,0 +1,177 @@
+package io.virtualapp.settings;
+
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+
+import io.virtualapp.R;
+import io.virtualapp.abs.ui.VActivity;
+
+/**
+ * author: weishu on 2018/5/9.
+ */
+
+public class RecommendPluginActivity extends VActivity {
+
+ private List mData = new ArrayList<>();
+ private PluginAdapter mAdapter;
+ private ProgressDialog mLoadingDialog;
+
+ private static final String ADDRESS = "http://vaexposed.weishu.me/plugin.json";
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_list);
+ mLoadingDialog = new ProgressDialog(this);
+ mLoadingDialog.setTitle("Loading");
+
+ ListView mListView = findViewById(R.id.list);
+ mAdapter = new PluginAdapter();
+ mListView.setAdapter(mAdapter);
+ mListView.setEmptyView(findViewById(R.id.empty_view));
+
+ mListView.setOnItemClickListener((parent, view, position, id) -> {
+ try {
+ PluginInfo item = mAdapter.getItem(position);
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse(item.link));
+ startActivity(intent);
+ } catch (Throwable ignored) {
+ ignored.printStackTrace();
+ }
+ });
+
+ loadRecommend();
+ }
+
+ private void loadRecommend() {
+ try {
+ mLoadingDialog.show();
+ } catch (Throwable ignored) {
+ }
+
+ defer().when(() -> {
+ JSONArray jsonArray = null;
+
+ URL url = new URL(ADDRESS);
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setRequestMethod("GET");
+ connection.setConnectTimeout(30000);
+ connection.setReadTimeout(30000);
+ if(connection.getResponseCode() == 200){
+ BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), "utf-8"));
+ String line;
+ StringBuilder sb = new StringBuilder();
+ while ((line = br.readLine()) != null) {
+ sb.append(line);
+ }
+ jsonArray = new JSONArray(sb.toString());
+ br.close();
+ }
+
+ connection.disconnect();
+ return jsonArray;
+ }).done(jsonArray -> {
+ mLoadingDialog.dismiss();
+
+ if (jsonArray == null) {
+ return;
+ }
+ mData.clear();
+
+ int length = jsonArray.length();
+ for (int i = 0; i < length; i++) {
+ PluginInfo info = new PluginInfo();
+ try {
+ JSONObject jsonObject = jsonArray.getJSONObject(i);
+ info.name = jsonObject.getString("name");
+ info.desc = jsonObject.getString("desc");
+ info.link = jsonObject.getString("link");
+ } catch (JSONException e) {
+ continue;
+ }
+ mData.add(info);
+ }
+
+ mAdapter.notifyDataSetChanged();
+ }).fail((v) -> {
+ mLoadingDialog.dismiss();
+ });
+ }
+
+ static class PluginInfo {
+ String name;
+ String desc;
+ String link;
+ }
+
+ static class ViewHolder {
+ TextView title;
+ TextView summary;
+
+ View root;
+
+ public ViewHolder(Context context, ViewGroup parent) {
+ root = LayoutInflater.from(context).inflate(R.layout.item_plugin_recommend, parent, false);
+ title = root.findViewById(R.id.item_plugin_name);
+ summary = root.findViewById(R.id.item_plugin_summary);
+ }
+ }
+
+ class PluginAdapter extends BaseAdapter {
+
+ @Override
+ public int getCount() {
+ return mData.size();
+ }
+
+ @Override
+ public PluginInfo getItem(int position) {
+ return mData.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return 0;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ ViewHolder holder;
+ if (convertView == null) {
+ holder = new ViewHolder(RecommendPluginActivity.this, parent);
+ convertView = holder.root;
+ convertView.setTag(holder);
+ } else {
+ holder = (ViewHolder) convertView.getTag();
+ }
+
+ PluginInfo info = getItem(position);
+ holder.title.setText(info.name);
+ holder.summary.setText(info.desc);
+
+ return convertView;
+ }
+ }
+}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/settings/SettingsActivity.java b/VirtualApp/app/src/main/java/io/virtualapp/settings/SettingsActivity.java
new file mode 100644
index 000000000..1f8bb443d
--- /dev/null
+++ b/VirtualApp/app/src/main/java/io/virtualapp/settings/SettingsActivity.java
@@ -0,0 +1,330 @@
+package io.virtualapp.settings;
+
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceScreen;
+import android.preference.SwitchPreference;
+import android.widget.Toast;
+
+import com.android.launcher3.LauncherFiles;
+import com.lody.virtual.client.core.VirtualCore;
+import com.lody.virtual.client.env.Constants;
+import com.lody.virtual.client.ipc.VActivityManager;
+
+import java.io.File;
+import java.io.IOException;
+
+import io.virtualapp.R;
+import io.virtualapp.gms.FakeGms;
+import io.virtualapp.home.ListAppActivity;
+import io.virtualapp.utils.Misc;
+
+/**
+ * Settings activity for Launcher. Currently implements the following setting: Allow rotation
+ */
+public class SettingsActivity extends Activity {
+
+ private static final String ADVANCE_SETTINGS_KEY = "settings_advance";
+ private static final String ADD_APP_KEY = "settings_add_app";
+ private static final String MODULE_MANAGE_KEY = "settings_module_manage";
+ private static final String APP_MANAGE_KEY = "settings_app_manage";
+ private static final String TASK_MANAGE_KEY = "settings_task_manage";
+ private static final String DESKTOP_SETTINGS_KEY = "settings_desktop";
+ private static final String FAQ_SETTINGS_KEY = "settings_faq";
+ private static final String DONATE_KEY = "settings_donate";
+ private static final String ABOUT_KEY = "settings_about";
+ private static final String REBOOT_KEY = "settings_reboot";
+ private static final String HIDE_SETTINGS_KEY = "advance_settings_hide_settings";
+ private static final String DISABLE_INSTALLER_KEY = "advance_settings_disable_installer";
+ public static final String ENABLE_LAUNCHER = "advance_settings_enable_launcher";
+ private static final String INSTALL_GMS_KEY = "advance_settings_install_gms";
+ public static final String DIRECTLY_BACK_KEY = "advance_settings_directly_back";
+ private static final String RECOMMEND_PLUGIN = "settings_plugin_recommend";
+ private static final String DISABLE_RESIDENT_NOTIFICATION = "advance_settings_disable_resident_notification";
+ private static final String ALLOW_FAKE_SIGNATURE = "advance_settings_allow_fake_signature";
+ private static final String DISABLE_XPOSED = "advance_settings_disable_xposed";
+ private static final String FILE_MANAGE = "settings_file_manage";
+ private static final String PERMISSION_MANAGE = "settings_permission_manage";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (savedInstanceState == null) {
+ // Display the fragment as the main content.
+ getFragmentManager().beginTransaction()
+ .replace(android.R.id.content, new SettingsFragment())
+ .commit();
+ }
+ }
+
+ /**
+ * This fragment shows the launcher preferences.
+ */
+ public static class SettingsFragment extends PreferenceFragment {
+
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getPreferenceManager().setSharedPreferencesName(LauncherFiles.SHARED_PREFERENCES_KEY);
+ addPreferencesFromResource(R.xml.settings_preferences);
+
+ // Setup allow rotation preference
+
+ Preference addApp = findPreference(ADD_APP_KEY);
+ Preference moduleManage = findPreference(MODULE_MANAGE_KEY);
+ Preference recommend = findPreference(RECOMMEND_PLUGIN);
+ Preference appManage = findPreference(APP_MANAGE_KEY);
+ Preference taskManage = findPreference(TASK_MANAGE_KEY);
+ Preference desktop = findPreference(DESKTOP_SETTINGS_KEY);
+ Preference faq = findPreference(FAQ_SETTINGS_KEY);
+ Preference donate = findPreference(DONATE_KEY);
+ Preference about = findPreference(ABOUT_KEY);
+ Preference reboot = findPreference(REBOOT_KEY);
+ Preference fileMange = findPreference(FILE_MANAGE);
+ Preference permissionManage = findPreference(PERMISSION_MANAGE);
+
+
+ SwitchPreference disableInstaller = (SwitchPreference) findPreference(DISABLE_INSTALLER_KEY);
+ SwitchPreference enableLauncher = (SwitchPreference) findPreference(ENABLE_LAUNCHER);
+ SwitchPreference disableResidentNotification = (SwitchPreference) findPreference(DISABLE_RESIDENT_NOTIFICATION);
+ SwitchPreference allowFakeSignature = (SwitchPreference) findPreference(ALLOW_FAKE_SIGNATURE);
+ SwitchPreference disableXposed = (SwitchPreference) findPreference(DISABLE_XPOSED);
+
+ addApp.setOnPreferenceClickListener(preference -> {
+ ListAppActivity.gotoListApp(getActivity());
+ return false;
+ });
+
+ moduleManage.setOnPreferenceClickListener(preference -> {
+ try {
+ Intent t = new Intent();
+ t.setComponent(new ComponentName("de.robv.android.xposed.installer", "de.robv.android.xposed.installer.WelcomeActivity"));
+ t.putExtra("fragment", 1);
+ int ret = VActivityManager.get().startActivity(t, 0);
+ if (ret < 0) {
+ Toast.makeText(getActivity(), R.string.xposed_installer_not_found, Toast.LENGTH_SHORT).show();
+ }
+ } catch (Throwable ignored) {
+ ignored.printStackTrace();
+ }
+ return false;
+ });
+
+ recommend.setOnPreferenceClickListener(preference -> {
+ startActivity(new Intent(getActivity(), RecommendPluginActivity.class));
+ return false;
+ });
+
+ boolean xposedEnabled = VirtualCore.get().isXposedEnabled();
+ if (!xposedEnabled) {
+ getPreferenceScreen().removePreference(moduleManage);
+ getPreferenceScreen().removePreference(recommend);
+ }
+
+ appManage.setOnPreferenceClickListener(preference -> {
+ startActivity(new Intent(getActivity(), AppManageActivity.class));
+ return false;
+ });
+
+ taskManage.setOnPreferenceClickListener(preference -> {
+ startActivity(new Intent(getActivity(), TaskManageActivity.class));
+ return false;
+ });
+
+ faq.setOnPreferenceClickListener(preference -> {
+ Uri uri = Uri.parse("https://github.com/android-hacker/VAExposed/wiki/FAQ");
+ Intent t = new Intent(Intent.ACTION_VIEW, uri);
+ startActivity(t);
+ return false;
+ });
+
+ desktop.setOnPreferenceClickListener(preference -> {
+ startActivity(new Intent(getActivity(), com.google.android.apps.nexuslauncher.SettingsActivity.class));
+ return false;
+ });
+
+ donate.setOnPreferenceClickListener(preference -> {
+ Misc.showDonate(getActivity());
+ return false;
+ });
+ about.setOnPreferenceClickListener(preference -> {
+ startActivity(new Intent(getActivity(), AboutActivity.class));
+ return false;
+ });
+
+ reboot.setOnPreferenceClickListener(preference -> {
+ android.app.AlertDialog alertDialog = new android.app.AlertDialog.Builder(getActivity())
+ .setTitle(R.string.settings_reboot_title)
+ .setMessage(getResources().getString(R.string.settings_reboot_content))
+ .setPositiveButton(android.R.string.yes, (dialog, which) -> {
+ VirtualCore.get().killAllApps();
+ Toast.makeText(getActivity(), R.string.reboot_tips_1, Toast.LENGTH_SHORT).show();
+ })
+ .setNegativeButton(android.R.string.no, null)
+ .create();
+ try {
+ alertDialog.show();
+ } catch (Throwable ignored) {
+ }
+ return false;
+ });
+
+ disableInstaller.setOnPreferenceChangeListener((preference, newValue) -> {
+ if (!(newValue instanceof Boolean)) {
+ return false;
+ }
+ try {
+ boolean disable = (boolean) newValue;
+ PackageManager packageManager = getActivity().getPackageManager();
+ packageManager.setComponentEnabledSetting(new ComponentName(getActivity().getPackageName(), "vxp.installer"),
+ !disable ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+ PackageManager.DONT_KILL_APP);
+ return true;
+ } catch (Throwable ignored) {
+ return false;
+ }
+ });
+
+ enableLauncher.setOnPreferenceChangeListener((preference, newValue) -> {
+ if (!(newValue instanceof Boolean)) {
+ return false;
+ }
+ try {
+ boolean enable = (boolean) newValue;
+ PackageManager packageManager = getActivity().getPackageManager();
+ packageManager.setComponentEnabledSetting(new ComponentName(getActivity().getPackageName(), "vxp.launcher"),
+ enable ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+ PackageManager.DONT_KILL_APP);
+ return true;
+ } catch (Throwable ignored) {
+ return false;
+ }
+ });
+
+ Preference installGms = findPreference(INSTALL_GMS_KEY);
+ installGms.setOnPreferenceClickListener(preference -> {
+ boolean alreadyInstalled = FakeGms.isAlreadyInstalled(getActivity());
+ if (alreadyInstalled) {
+ FakeGms.uninstallGms(getActivity());
+ } else {
+ FakeGms.installGms(getActivity());
+ }
+ return true;
+ });
+
+ fileMange.setOnPreferenceClickListener(preference -> {
+ OnlinePlugin.openOrDownload(getActivity(), OnlinePlugin.FILE_MANAGE_PACKAGE,
+ OnlinePlugin.FILE_MANAGE_URL, getString(R.string.install_file_manager_tips));
+ return false;
+ });
+
+ permissionManage.setOnPreferenceClickListener(preference -> {
+ OnlinePlugin.openOrDownload(getActivity(), OnlinePlugin.PERMISSION_MANAGE_PACKAGE,
+ OnlinePlugin.PERMISSION_MANAGE_URL, getString(R.string.install_permission_manager_tips));
+ return false;
+ });
+
+ disableXposed.setOnPreferenceChangeListener((preference, newValue) -> {
+
+ if (!(newValue instanceof Boolean)) {
+ return false;
+ }
+
+ boolean on = (boolean) newValue;
+
+ File disableXposedFile = getActivity().getFileStreamPath(".disable_xposed"); // 文件不存在代表是保守模式
+ if (on) {
+ boolean success;
+ try {
+ success = disableXposedFile.createNewFile();
+ } catch (IOException e) {
+ success = false;
+ }
+ return success;
+ } else {
+ return !disableXposedFile.exists() || disableXposedFile.delete();
+ }
+ });
+
+ disableResidentNotification.setOnPreferenceChangeListener(((preference, newValue) -> {
+
+ if (!(newValue instanceof Boolean)) {
+ return false;
+ }
+
+ boolean on = (boolean) newValue;
+
+ File flag = getActivity().getFileStreamPath(Constants.NO_NOTIFICATION_FLAG);
+ if (on) {
+ boolean success;
+ try {
+ success = flag.createNewFile();
+ } catch (IOException e) {
+ success = false;
+ }
+ return success;
+ } else {
+ return !flag.exists() || flag.delete();
+ }
+ }));
+
+ if (android.os.Build.VERSION.SDK_INT < 25) {
+ // Android NR1 below do not need this.
+ PreferenceScreen advance = (PreferenceScreen) findPreference(ADVANCE_SETTINGS_KEY);
+ advance.removePreference(disableResidentNotification);
+ }
+
+ allowFakeSignature.setOnPreferenceChangeListener((preference, newValue) -> {
+ if (!(newValue instanceof Boolean)) {
+ return false;
+ }
+
+ boolean on = (boolean) newValue;
+ File flag = getActivity().getFileStreamPath(Constants.FAKE_SIGNATURE_FLAG);
+ if (on) {
+ boolean success;
+ try {
+ success = flag.createNewFile();
+ } catch (IOException e) {
+ success = false;
+ }
+ return success;
+ } else {
+ return !flag.exists() || flag.delete();
+ }
+ });
+
+ }
+
+ private static void dismiss(ProgressDialog dialog) {
+ try {
+ dialog.dismiss();
+ } catch (Throwable ignored) {
+ }
+ }
+
+ protected int dp2px(float dp) {
+ final float scale = getResources().getDisplayMetrics().density;
+ return (int) (dp * scale + 0.5f);
+ }
+
+ @Override
+ public void startActivity(Intent intent) {
+ try {
+ super.startActivity(intent);
+ } catch (Throwable ignored) {
+ Toast.makeText(getActivity(), "startActivity failed.", Toast.LENGTH_SHORT).show();
+ ignored.printStackTrace();
+ }
+ }
+ }
+}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/settings/TaskManageActivity.java b/VirtualApp/app/src/main/java/io/virtualapp/settings/TaskManageActivity.java
new file mode 100644
index 000000000..f2a63e9ea
--- /dev/null
+++ b/VirtualApp/app/src/main/java/io/virtualapp/settings/TaskManageActivity.java
@@ -0,0 +1,178 @@
+package io.virtualapp.settings;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.lody.virtual.client.core.VirtualCore;
+import com.lody.virtual.client.ipc.VActivityManager;
+import com.lody.virtual.os.VUserHandle;
+import com.lody.virtual.remote.InstalledAppInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.virtualapp.R;
+import io.virtualapp.abs.ui.VActivity;
+import io.virtualapp.glide.GlideUtils;
+
+/**
+ * @author weishu
+ * @date 18/2/15.
+ */
+
+public class TaskManageActivity extends VActivity {
+
+ private ListView mListView;
+ private List mInstalledApps = new ArrayList<>();
+ private AppManageAdapter mAdapter;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_list);
+ mListView = (ListView) findViewById(R.id.list);
+ mAdapter = new AppManageAdapter();
+ mListView.setAdapter(mAdapter);
+
+ loadAsync();
+ }
+
+ private void loadAsync() {
+ defer().when(this::loadApp).done((v) -> mAdapter.notifyDataSetChanged());
+ }
+
+ private void loadApp() {
+
+ ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
+ if (am == null) {
+ return;
+ }
+
+ List ret = new ArrayList<>();
+ List infoList = am.getRunningAppProcesses();
+ if (infoList == null) {
+ return;
+ }
+ List retList = new ArrayList<>();
+ String hostPkg = VirtualCore.get().getHostPkg();
+ for (ActivityManager.RunningAppProcessInfo info : infoList) {
+ if (VActivityManager.get().isAppPid(info.pid)) {
+ List pkgList = VActivityManager.get().getProcessPkgList(info.pid);
+ if (pkgList.contains(hostPkg)) {
+ continue;
+ }
+ String processName = VActivityManager.get().getAppProcessName(info.pid);
+ if (processName != null) {
+ info.processName = processName;
+ }
+ info.pkgList = pkgList.toArray(new String[pkgList.size()]);
+ info.uid = VUserHandle.getAppId(VActivityManager.get().getUidByPid(info.pid));
+ retList.add(info);
+ }
+ }
+
+ for (ActivityManager.RunningAppProcessInfo runningAppProcessInfo : retList) {
+ TaskManageInfo info = new TaskManageInfo();
+ info.name = runningAppProcessInfo.processName;
+ info.pid = runningAppProcessInfo.pid;
+ info.uid = runningAppProcessInfo.uid;
+
+ if (runningAppProcessInfo.pkgList != null) {
+ for (String pkg : runningAppProcessInfo.pkgList) {
+ InstalledAppInfo installedAppInfo = VirtualCore.get().getInstalledAppInfo(pkg, 0);
+ if (installedAppInfo != null) {
+ info.pkgName = installedAppInfo.packageName;
+ info.path = installedAppInfo.apkPath;
+ }
+ }
+ }
+ ret.add(info);
+ }
+ mInstalledApps.clear();
+ mInstalledApps.addAll(ret);
+ }
+
+ class AppManageAdapter extends BaseAdapter {
+
+ @Override
+ public int getCount() {
+ return mInstalledApps.size();
+ }
+
+ @Override
+ public TaskManageInfo getItem(int position) {
+ return mInstalledApps.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return 0;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ ViewHolder holder;
+ if (convertView == null) {
+ holder = new ViewHolder(TaskManageActivity.this, parent);
+ convertView = holder.root;
+ convertView.setTag(holder);
+ } else {
+ holder = (ViewHolder) convertView.getTag();
+ }
+
+ TaskManageInfo item = getItem(position);
+
+ holder.button.setText(R.string.task_manage_uninstall);
+ holder.label.setText(item.name);
+ holder.icon.setImageDrawable(item.icon);
+
+ if (VirtualCore.get().isOutsideInstalled(item.name.toString())) {
+ GlideUtils.loadInstalledPackageIcon(getContext(), item.pkgName, holder.icon, android.R.drawable.sym_def_app_icon);
+ } else {
+ GlideUtils.loadPackageIconFromApkFile(getContext(), item.path, holder.icon, android.R.drawable.sym_def_app_icon);
+ }
+
+ holder.button.setOnClickListener(v -> {
+ VActivityManager.get().killApplicationProcess(item.name.toString(), item.uid);
+ holder.button.postDelayed(TaskManageActivity.this::loadAsync, 300);
+ });
+
+ return convertView;
+ }
+ }
+
+ static class ViewHolder {
+ ImageView icon;
+ TextView label;
+ Button button;
+
+ View root;
+
+ ViewHolder(Context context, ViewGroup parent) {
+ root = LayoutInflater.from(context).inflate(R.layout.item_task_manage, parent, false);
+ icon = root.findViewById(R.id.item_app_icon);
+ label = root.findViewById(R.id.item_app_name);
+ button = root.findViewById(R.id.item_app_button);
+ }
+ }
+
+ static class TaskManageInfo {
+ public String pkgName;
+ public String path;
+ CharSequence name;
+ int uid;
+ int pid;
+ Drawable icon;
+ }
+}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/splash/SplashActivity.java b/VirtualApp/app/src/main/java/io/virtualapp/splash/SplashActivity.java
index 565ce8858..a2406d87b 100644
--- a/VirtualApp/app/src/main/java/io/virtualapp/splash/SplashActivity.java
+++ b/VirtualApp/app/src/main/java/io/virtualapp/splash/SplashActivity.java
@@ -10,11 +10,12 @@
import io.virtualapp.abs.ui.VActivity;
import io.virtualapp.abs.ui.VUiKit;
import io.virtualapp.home.FlurryROMCollector;
-import io.virtualapp.home.HomeActivity;
+import io.virtualapp.home.NewHomeActivity;
import jonathanfinerty.once.Once;
public class SplashActivity extends VActivity {
+
@Override
protected void onCreate(Bundle savedInstanceState) {
@SuppressWarnings("unused")
@@ -24,21 +25,26 @@ protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
VUiKit.defer().when(() -> {
- if (!Once.beenDone("collect_flurry")) {
+ if (!Once.beenDone("collect_fabric")) {
FlurryROMCollector.startCollect();
- Once.markDone("collect_flurry");
+ Once.markDone("collect_fabric");
}
long time = System.currentTimeMillis();
- VirtualCore.get().waitForEngine();
+ doActionInThread();
time = System.currentTimeMillis() - time;
- long delta = 1000L - time;
+ long delta = 100L - time;
if (delta > 0) {
VUiKit.sleep(delta);
}
}).done((res) -> {
- HomeActivity.goHome(this);
+ NewHomeActivity.goHome(this);
finish();
});
}
+ private void doActionInThread() {
+ if (!VirtualCore.get().isEngineLaunched()) {
+ VirtualCore.get().waitForEngine();
+ }
+ }
}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/sys/Installd.java b/VirtualApp/app/src/main/java/io/virtualapp/sys/Installd.java
new file mode 100644
index 000000000..1bb44a0d8
--- /dev/null
+++ b/VirtualApp/app/src/main/java/io/virtualapp/sys/Installd.java
@@ -0,0 +1,273 @@
+package io.virtualapp.sys;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.text.TextUtils;
+import android.widget.Toast;
+
+import com.lody.virtual.GmsSupport;
+import com.lody.virtual.client.core.InstallStrategy;
+import com.lody.virtual.client.core.VirtualCore;
+import com.lody.virtual.helper.utils.DeviceUtil;
+import com.lody.virtual.os.VUserInfo;
+import com.lody.virtual.os.VUserManager;
+import com.lody.virtual.remote.InstallResult;
+import com.lody.virtual.remote.InstalledAppInfo;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import io.virtualapp.R;
+import io.virtualapp.VCommends;
+import io.virtualapp.XApp;
+import io.virtualapp.abs.ui.VUiKit;
+import io.virtualapp.home.models.AppData;
+import io.virtualapp.home.models.AppInfoLite;
+import io.virtualapp.home.models.MultiplePackageAppData;
+import io.virtualapp.home.models.PackageAppData;
+import io.virtualapp.home.repo.PackageAppDataStorage;
+
+/**
+ * author: weishu on 18/3/19.
+ */
+public class Installd {
+
+ public interface UpdateListener {
+ void update(AppData model);
+
+ void fail(String msg);
+ }
+
+ public static void addApp(AppInfoLite info, UpdateListener refreshListener) {
+ class AddResult {
+ private PackageAppData appData;
+ private int userId;
+ private boolean justEnableHidden;
+ }
+ AddResult addResult = new AddResult();
+ VUiKit.defer().when(() -> {
+ InstalledAppInfo installedAppInfo = VirtualCore.get().getInstalledAppInfo(info.packageName, 0);
+ addResult.justEnableHidden = installedAppInfo != null;
+
+ if (info.disableMultiVersion) {
+ addResult.justEnableHidden = false;
+ }
+ if (addResult.justEnableHidden) {
+ int[] userIds = installedAppInfo.getInstalledUsers();
+ int nextUserId = userIds.length;
+ /*
+ Input : userIds = {0, 1, 3}
+ Output: nextUserId = 2
+ */
+ for (int i = 0; i < userIds.length; i++) {
+ if (userIds[i] != i) {
+ nextUserId = i;
+ break;
+ }
+ }
+ addResult.userId = nextUserId;
+
+ if (VUserManager.get().getUserInfo(nextUserId) == null) {
+ // user not exist, create it automatically.
+ String nextUserName = "Space " + (nextUserId + 1);
+ VUserInfo newUserInfo = VUserManager.get().createUser(nextUserName, VUserInfo.FLAG_ADMIN);
+ if (newUserInfo == null) {
+ throw new IllegalStateException();
+ }
+ }
+ boolean success = VirtualCore.get().installPackageAsUser(nextUserId, info.packageName);
+ if (!success) {
+ throw new IllegalStateException();
+ }
+ } else {
+ PackageInfo pkgInfo = null;
+ try {
+ pkgInfo = XApp.getApp().getPackageManager().getPackageArchiveInfo(info.path, 0);
+ pkgInfo.applicationInfo.sourceDir = info.path;
+ pkgInfo.applicationInfo.publicSourceDir = info.path;
+ } catch (Exception e) {
+ }
+ if(pkgInfo != null) {
+ PackageAppData data = PackageAppDataStorage.get().acquire(pkgInfo.applicationInfo);
+ addResult.appData = data;
+ data.isInstalling = true;
+ data.isFirstOpen = false;
+ if (refreshListener != null) {
+ refreshListener.update(data);
+ }
+ }
+
+ InstallResult res = addVirtualApp(info);
+ if (!res.isSuccess) {
+ if (addResult.appData != null) {
+ // mView.removeAppToLauncher(addResult.appData);
+ }
+ throw new IllegalStateException(res.error);
+ }
+ }
+ }).then((res) -> {
+ if (addResult.appData == null) {
+ addResult.appData = PackageAppDataStorage.get().acquire(info.packageName);
+ }
+ }).done(res -> {
+ boolean multipleVersion = addResult.justEnableHidden && addResult.userId != 0;
+ if (!multipleVersion) {
+ PackageAppData data = addResult.appData;
+ data.isInstalling = false;
+ data.isLoading = true;
+
+ if (refreshListener != null) {
+ refreshListener.update(data);
+ }
+ handleOptApp(data, info.packageName, true, refreshListener);
+ } else {
+ MultiplePackageAppData data = new MultiplePackageAppData(addResult.appData, addResult.userId);
+ data.isInstalling = false;
+ data.isLoading = true;
+
+ if (refreshListener != null) {
+ refreshListener.update(data);
+ }
+ handleOptApp(data, info.packageName, false, refreshListener);
+ }
+ }).fail(result -> {
+ if (refreshListener != null) {
+ refreshListener.fail(result.getMessage());
+
+ }
+ });
+ }
+
+
+ private static void handleOptApp(AppData data, String packageName, boolean needOpt, UpdateListener refreshListener) {
+ VUiKit.defer().when(() -> {
+ long time = System.currentTimeMillis();
+ if (needOpt) {
+ try {
+ VirtualCore.get().preOpt(packageName);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ time = System.currentTimeMillis() - time;
+ if (time < 1500L) {
+ try {
+ Thread.sleep(1500L - time);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }).done((res) -> {
+ if (data instanceof PackageAppData) {
+ ((PackageAppData) data).isLoading = false;
+ ((PackageAppData) data).isFirstOpen = true;
+ } else if (data instanceof MultiplePackageAppData) {
+ ((MultiplePackageAppData) data).isLoading = false;
+ ((MultiplePackageAppData) data).isFirstOpen = true;
+ }
+ if (refreshListener != null) {
+ refreshListener.update(data);
+ }
+ });
+ }
+
+ public static InstallResult addVirtualApp(AppInfoLite info) {
+ int flags = InstallStrategy.COMPARE_VERSION | InstallStrategy.SKIP_DEX_OPT;
+ info.fastOpen = false; // disable fast open for compile.
+ if (DeviceUtil.isMeizuBelowN()) {
+ info.fastOpen = true;
+ }
+ if (info.fastOpen) {
+ flags |= InstallStrategy.DEPEND_SYSTEM_IF_EXIST;
+ }
+ if (info.disableMultiVersion) {
+ flags |= InstallStrategy.UPDATE_IF_EXIST;
+ }
+ return VirtualCore.get().installPackage(info.path, flags);
+ }
+
+ private static ArrayList getAppInfoLiteFromPath(Context context, String path) {
+ if (context == null) {
+ return null;
+ }
+ PackageInfo pkgInfo = null;
+ try {
+ pkgInfo = context.getPackageManager().getPackageArchiveInfo(path, PackageManager.GET_META_DATA);
+ pkgInfo.applicationInfo.sourceDir = path;
+ pkgInfo.applicationInfo.publicSourceDir = path;
+ } catch (Exception e) {
+ // Ignore
+ }
+ if (pkgInfo == null) {
+ return null;
+ }
+
+ if (TextUtils.equals(VirtualCore.TAICHI_PACKAGE, pkgInfo.packageName)) {
+ return null;
+ }
+
+ if (VirtualCore.get().getHostPkg().equals(pkgInfo.packageName)) {
+ Toast.makeText(VirtualCore.get().getContext(), R.string.install_self_eggs, Toast.LENGTH_SHORT).show();
+ return null;
+ }
+
+ boolean isXposed = pkgInfo.applicationInfo.metaData != null
+ && pkgInfo.applicationInfo.metaData.containsKey("xposedmodule");
+ AppInfoLite appInfoLite = new AppInfoLite(pkgInfo.packageName, path, false, isXposed);
+ ArrayList dataList = new ArrayList<>();
+ dataList.add(appInfoLite);
+ return dataList;
+ }
+
+ public static void handleRequestFromFile(Context context, String path) {
+
+ ArrayList dataList = getAppInfoLiteFromPath(context, path);
+ if (dataList == null) {
+ return;
+ }
+ startInstallerActivity(context, dataList);
+ }
+
+ public static void startInstallerActivity(Context context, ArrayList data) {
+ if (context == null) {
+ return;
+ }
+ Intent intent = new Intent(context, InstallerActivity.class);
+ intent.putParcelableArrayListExtra(VCommends.EXTRA_APP_INFO_LIST, data);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(intent);
+ }
+
+ public static void addGmsSupport() {
+ List gApps = new ArrayList<>();
+ gApps.addAll(GmsSupport.GOOGLE_APP);
+ gApps.addAll(GmsSupport.GOOGLE_SERVICE);
+
+ VirtualCore core = VirtualCore.get();
+ final int userId = 0;
+
+ ArrayList toInstalled = new ArrayList<>();
+ for (String packageName : gApps) {
+ if (core.isAppInstalledAsUser(userId, packageName)) {
+ continue;
+ }
+ ApplicationInfo info = null;
+ try {
+ info = VirtualCore.get().getUnHookPackageManager().getApplicationInfo(packageName, 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ // Ignore
+ }
+ if (info == null || info.sourceDir == null) {
+ continue;
+ }
+
+ AppInfoLite lite = new AppInfoLite(info.packageName, info.sourceDir, false, true);
+ toInstalled.add(lite);
+ }
+ startInstallerActivity(VirtualCore.get().getContext(), toInstalled);
+ }
+}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/sys/InstallerActivity.java b/VirtualApp/app/src/main/java/io/virtualapp/sys/InstallerActivity.java
new file mode 100644
index 000000000..223e8ee8d
--- /dev/null
+++ b/VirtualApp/app/src/main/java/io/virtualapp/sys/InstallerActivity.java
@@ -0,0 +1,337 @@
+package io.virtualapp.sys;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.lody.virtual.client.core.InstallStrategy;
+import com.lody.virtual.client.core.VirtualCore;
+import com.lody.virtual.helper.utils.EncodeUtils;
+import com.lody.virtual.helper.utils.FileUtils;
+import com.lody.virtual.remote.InstalledAppInfo;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import io.virtualapp.R;
+import io.virtualapp.VCommends;
+import io.virtualapp.abs.ui.VUiKit;
+import io.virtualapp.home.models.AppData;
+import io.virtualapp.home.models.AppInfoLite;
+
+/**
+ * author: weishu on 18/3/19.
+ */
+public class InstallerActivity extends AppCompatActivity {
+
+ private TextView mTips;
+ private Button mLeft;
+ private Button mRight;
+ private ProgressBar mProgressBar;
+ private TextView mProgressText;
+
+ private int mInstallCount = 0;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.activity_install);
+
+ mTips = (TextView) findViewById(R.id.installer_text);
+ mLeft = (Button) findViewById(R.id.installer_left_button);
+ mRight = (Button) findViewById(R.id.installer_right_button);
+ mProgressBar = (ProgressBar) findViewById(R.id.installer_loading);
+ mProgressText = (TextView) findViewById(R.id.installer_progress_text);
+
+ handleIntent(getIntent());
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ handleIntent(intent);
+ }
+
+ @Override
+ public void onBackPressed() {
+ // do nothing.
+ if (mInstallCount > 0) {
+
+ }
+ }
+
+ private void handleIntent(Intent intent) {
+ if (intent == null) {
+ finish();
+ return;
+ }
+
+ ArrayList dataList = intent.getParcelableArrayListExtra(VCommends.EXTRA_APP_INFO_LIST);
+ if (dataList == null) {
+ handleSystemIntent(intent);
+ } else {
+ handleSelfIntent(dataList);
+ }
+ }
+
+ private void handleSelfIntent(ArrayList appList) {
+ if (appList != null) {
+ boolean showTip = false;
+ int size = appList.size();
+ mInstallCount = size;
+
+ if (dealUpdate(appList)) {
+ return;
+ }
+
+ for (int i = 0; i < size; i++) {
+ AppInfoLite info = appList.get(i);
+ if (new File(info.path).length() > 1024 * 1024 * 24) {
+ showTip = true;
+ }
+
+ addApp(info);
+ }
+ if (showTip) {
+ Toast.makeText(this, R.string.large_app_install_tips, Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ }
+
+ private void addApp(AppInfoLite appInfoLite) {
+ Installd.addApp(appInfoLite, new Installd.UpdateListener() {
+ @Override
+ public void update(AppData model) {
+ runOnUiThread(() -> {
+ if (model.isInstalling()) {
+ mProgressText.setVisibility(View.VISIBLE);
+ mProgressBar.setVisibility(View.VISIBLE);
+ mProgressText.setText(getResources().getString(R.string.add_app_installing_tips, model.getName()));
+ } else if (model.isLoading()) {
+ mProgressText.setVisibility(View.VISIBLE);
+ mProgressBar.setVisibility(View.VISIBLE);
+ mProgressText.setText(getResources().getString(R.string.add_app_loading_tips, model.getName()));
+ } else {
+ mInstallCount--;
+ if (mInstallCount <= 0) {
+ mInstallCount = 0;
+ // only dismiss when the app is the last to install.
+ mProgressText.setText(getResources().getString(R.string.add_app_loading_complete, model.getName()));
+ mProgressText.postDelayed(() -> {
+ mProgressBar.setVisibility(View.GONE);
+
+ mRight.setVisibility(View.VISIBLE);
+ mRight.setText(R.string.install_complete);
+ mRight.setOnClickListener((vv) -> finish());
+ }, 500);
+ }
+ }
+ }
+ );
+ }
+
+ @Override
+ public void fail(String msg) {
+ if (msg == null) {
+ msg = "Unknown";
+ }
+
+ mProgressText.setText(getResources().getString(R.string.install_fail, msg));
+ mProgressText.postDelayed(() -> {
+ mProgressBar.setVisibility(View.GONE);
+ mRight.setVisibility(View.VISIBLE);
+ mRight.setText(R.string.install_complete);
+ mRight.setOnClickListener((vv) -> finish());
+ }, 500);
+ }
+ });
+ }
+
+ private boolean dealUpdate(List appList) {
+ if (appList == null || appList.size() != 1) {
+ return false;
+ }
+ AppInfoLite appInfoLite = appList.get(0);
+ if (appInfoLite == null) {
+ return false;
+ }
+
+ List magicApps = Arrays.asList(EncodeUtils.decode("Y29tLmxiZS5wYXJhbGxlbA=="), // com.lbe.parallel
+ EncodeUtils.decode("Y29tLnFpaG9vLm1hZ2lj"), // com.qihoo.magic
+ EncodeUtils.decode("Y29tLmRvdWJsZW9wZW4=")); // com.doubleopen
+
+ if (magicApps.contains(appInfoLite.packageName)) {
+ Toast.makeText(VirtualCore.get().getContext(), R.string.install_self_eggs, Toast.LENGTH_SHORT).show();
+ }
+
+ if (appInfoLite.disableMultiVersion) {
+ return false;
+ }
+ InstalledAppInfo installedAppInfo = VirtualCore.get().getInstalledAppInfo(appInfoLite.packageName, 0);
+ if (installedAppInfo == null) {
+ return false;
+ }
+ String currentVersion;
+ String toInstalledVersion;
+ int currentVersionCode;
+ int toInstalledVersionCode;
+ PackageManager packageManager = getPackageManager();
+ if (packageManager == null) {
+ return false;
+ }
+ try {
+ PackageInfo applicationInfo = installedAppInfo.getPackageInfo(0);
+ currentVersion = applicationInfo.versionName;
+ currentVersionCode = applicationInfo.versionCode;
+
+ PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(appInfoLite.path, 0);
+ toInstalledVersion = packageArchiveInfo.versionName;
+ toInstalledVersionCode = packageArchiveInfo.versionCode;
+
+ String multiVersionUpdate = getResources().getString(currentVersionCode == toInstalledVersionCode ? R.string.multi_version_cover : (
+ currentVersionCode < toInstalledVersionCode ? R.string.multi_version_upgrade : R.string.multi_version_downgrade
+ ));
+ AlertDialog alertDialog = new AlertDialog.Builder(this)
+ .setTitle(R.string.multi_version_tip_title)
+ .setMessage(getResources().getString(R.string.multi_version_tips_content, currentVersion, toInstalledVersion))
+ .setPositiveButton(R.string.multi_version_multi, (dialog, which) -> {
+ addApp(appInfoLite);
+ })
+ .setNegativeButton(multiVersionUpdate, ((dialog, which) -> {
+ appInfoLite.disableMultiVersion = true;
+ addApp(appInfoLite);
+ }))
+ .create();
+ alertDialog.show();
+ } catch (Throwable ignored) {
+ return false;
+ }
+ return true;
+ }
+
+ private void handleSystemIntent(Intent intent) {
+
+ Context context = VirtualCore.get().getContext();
+ String path;
+ try {
+ path = FileUtils.getFileFromUri(context, intent.getData());
+ } catch (Throwable e) {
+ e.printStackTrace();
+ return;
+ }
+ PackageInfo pkgInfo = null;
+ try {
+ pkgInfo = context.getPackageManager().getPackageArchiveInfo(path, PackageManager.GET_META_DATA);
+ pkgInfo.applicationInfo.sourceDir = path;
+ pkgInfo.applicationInfo.publicSourceDir = path;
+ } catch (Exception e) {
+ // Ignore
+ }
+ if (pkgInfo == null) {
+ finish();
+ return;
+ }
+
+ boolean isXposed = pkgInfo.applicationInfo.metaData != null
+ && pkgInfo.applicationInfo.metaData.containsKey("xposedmodule");
+
+ InstalledAppInfo installedAppInfo = VirtualCore.get().getInstalledAppInfo(pkgInfo.packageName, 0);
+
+ String tipsText;
+ String rightString;
+ String leftString = getResources().getString(android.R.string.cancel);
+
+ PackageManager packageManager = getPackageManager();
+ if (packageManager == null) {
+ finish();
+ return;
+ }
+ PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(path, 0);
+ if (packageArchiveInfo == null) {
+ finish();
+ return;
+ }
+ String toInstalledVersion = packageArchiveInfo.versionName;
+ int toInstalledVersionCode = packageArchiveInfo.versionCode;
+ CharSequence label = packageArchiveInfo.packageName;
+
+ if (installedAppInfo != null) {
+ String currentVersion;
+ int currentVersionCode;
+
+ PackageInfo applicationInfo = installedAppInfo.getPackageInfo(0);
+ if (applicationInfo == null) {
+ finish();
+ return;
+ }
+ currentVersion = applicationInfo.versionName;
+ currentVersionCode = applicationInfo.versionCode;
+
+ label = applicationInfo.applicationInfo.loadLabel(packageManager);
+
+ String multiVersionUpdate = getResources().getString(currentVersionCode == toInstalledVersionCode ? R.string.multi_version_cover : (
+ currentVersionCode < toInstalledVersionCode ? R.string.multi_version_upgrade : R.string.multi_version_downgrade
+ ));
+
+ tipsText = getResources().getString(R.string.install_package_version_tips, currentVersion, toInstalledVersion);
+ rightString = multiVersionUpdate;
+
+ } else {
+ tipsText = getResources().getString(R.string.install_package, label);
+ rightString = getResources().getString(R.string.install);
+ }
+
+ final CharSequence apkName = label;
+ mTips.setText(tipsText);
+ mLeft.setText(leftString);
+ mRight.setText(rightString);
+
+ mLeft.setOnClickListener(v -> finish());
+ mRight.setOnClickListener(v -> {
+
+ mProgressBar.setVisibility(View.VISIBLE);
+ mTips.setVisibility(View.GONE);
+ mLeft.setVisibility(View.GONE);
+ mRight.setEnabled(false);
+
+ VUiKit.defer().when(() -> {
+ return VirtualCore.get().installPackage(path, InstallStrategy.UPDATE_IF_EXIST);
+ }).done((res) -> {
+ // install success
+ mTips.setVisibility(View.GONE);
+ mProgressText.setVisibility(View.VISIBLE);
+ mProgressText.setText(getResources().getString(R.string.add_app_loading_complete, apkName));
+ mProgressBar.setVisibility(View.GONE);
+ mRight.setEnabled(true);
+ mRight.setText(res.isSuccess ? getResources().getString(R.string.install_complete) :
+ getResources().getString(R.string.install_fail, res.error));
+ mRight.setOnClickListener((vv) -> finish());
+ }).fail((res) -> {
+ String msg = res.getMessage();
+ if (msg == null) {
+ msg = "Unknown";
+ }
+ mProgressText.setVisibility(View.VISIBLE);
+ mProgressText.setText(getResources().getString(R.string.install_fail, msg));
+ mRight.setEnabled(true);
+ mProgressBar.setVisibility(View.GONE);
+ mRight.setText(android.R.string.ok);
+ mRight.setOnClickListener((vv) -> finish());
+ });
+ });
+ }
+}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/sys/ShareBridgeActivity.java b/VirtualApp/app/src/main/java/io/virtualapp/sys/ShareBridgeActivity.java
new file mode 100644
index 000000000..f7379402d
--- /dev/null
+++ b/VirtualApp/app/src/main/java/io/virtualapp/sys/ShareBridgeActivity.java
@@ -0,0 +1,140 @@
+package io.virtualapp.sys;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v7.app.AppCompatActivity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.lody.virtual.client.ipc.VActivityManager;
+import com.lody.virtual.client.ipc.VPackageManager;
+
+import java.util.List;
+
+import io.virtualapp.R;
+
+/**
+ * author: weishu on 18/3/16.
+ */
+public class ShareBridgeActivity extends AppCompatActivity {
+ private SharedAdapter mAdapter;
+ private List mShareComponents;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Intent intent = getIntent();
+ String action = intent.getAction();
+ String type = intent.getType();
+
+ intent.setComponent(null);
+
+ if (!Intent.ACTION_SEND.equals(action)) {
+ finish();
+ return;
+ }
+
+ try {
+ mShareComponents = VPackageManager.get().
+ queryIntentActivities(new Intent(Intent.ACTION_SEND), type, 0, 0); // multi-user?
+ } catch (Throwable ignored) {
+ }
+
+ if (mShareComponents == null || mShareComponents.size() == 0) {
+ finish();
+ return;
+ }
+
+ setContentView(R.layout.activity_list);
+ ListView mListView = (ListView) findViewById(R.id.list);
+ mAdapter = new SharedAdapter();
+ mListView.setAdapter(mAdapter);
+
+ mListView.setOnItemClickListener((parent, view, position, id) -> {
+ try {
+ ResolveInfo item = mAdapter.getItem(position);
+ Intent t = new Intent(intent);
+ t.setComponent(new ComponentName(item.activityInfo.packageName, item.activityInfo.name));
+ VActivityManager.get().startActivity(t, 0);
+ } catch (Throwable e) {
+ Toast.makeText(getApplicationContext(), R.string.shared_to_vxp_failed, Toast.LENGTH_SHORT).show();
+ }
+ finish();
+ });
+ }
+
+ private class SharedAdapter extends BaseAdapter {
+
+ @Override
+ public int getCount() {
+ return mShareComponents.size();
+ }
+
+ @Override
+ public ResolveInfo getItem(int position) {
+ return mShareComponents.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return 0;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ ViewHolder holder;
+ if (convertView == null) {
+ holder = new ViewHolder(getActivity(), parent);
+ convertView = holder.root;
+ convertView.setTag(holder);
+ } else {
+ holder = (ViewHolder) convertView.getTag();
+ }
+
+ ResolveInfo item = getItem(position);
+ PackageManager packageManager = getPackageManager();
+ try {
+ holder.label.setText(item.loadLabel(packageManager));
+ } catch (Throwable e) {
+ holder.label.setText(R.string.package_state_unknown);
+ }
+ try {
+ holder.icon.setImageDrawable(item.loadIcon(packageManager));
+ } catch (Throwable e) {
+ holder.icon.setImageDrawable(getResources().getDrawable(android.R.drawable.sym_def_app_icon));
+ }
+
+ return convertView;
+ }
+ }
+
+ static class ViewHolder {
+ ImageView icon;
+ TextView label;
+
+ View root;
+
+ ViewHolder(Context context, ViewGroup parent) {
+ root = LayoutInflater.from(context).inflate(R.layout.item_share, parent, false);
+ icon = root.findViewById(R.id.item_share_icon);
+ label = root.findViewById(R.id.item_share_name);
+ }
+ }
+
+ private Activity getActivity() {
+ return this;
+ }
+}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/update/VAVersionService.java b/VirtualApp/app/src/main/java/io/virtualapp/update/VAVersionService.java
new file mode 100644
index 000000000..849a541d2
--- /dev/null
+++ b/VirtualApp/app/src/main/java/io/virtualapp/update/VAVersionService.java
@@ -0,0 +1,111 @@
+package io.virtualapp.update;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.allenliu.versionchecklib.core.AVersionService;
+import com.allenliu.versionchecklib.core.AllenChecker;
+import com.allenliu.versionchecklib.core.VersionParams;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.concurrent.TimeUnit;
+
+import io.virtualapp.R;
+import io.virtualapp.VCommends;
+
+/**
+ * @author weishu
+ * @date 18/1/4.
+ */
+
+public class VAVersionService extends AVersionService {
+ private static final String TAG = "VAVersionService";
+
+ private static final long CHECK_INTERVAL = TimeUnit.HOURS.toMillis(1);
+
+ private static final String KEY_SHOW_TIP = "show_tips";
+
+ private static long sLastCheckTime;
+
+ static {
+ AllenChecker.init(false);
+ }
+
+ public static final String CHECK_VERION_URL = "http://vaexposed.weishu.me/update.json";
+
+ @Override
+ public void onResponses(AVersionService service, String response) {
+ try {
+ JSONObject versionInfo = new JSONObject(response);
+// {
+// url: "download url",
+// versionCode: 3,
+// updateMessage: "Android 7.0"
+// }
+ String url = versionInfo.getString("url");
+ int versionCode = versionInfo.getInt("versionCode");
+ String updateMessage = versionInfo.getString("updateMessage");
+
+ int currentVersion = getCurrentVersionCode(this);
+ if (currentVersion < versionCode) {
+ showVersionDialog(url, getResources().getString(R.string.new_version_detected), updateMessage);
+ } else {
+ boolean showTip = versionParams != null && versionParams.getParamBundle() != null
+ && versionParams.getParamBundle().getBoolean(KEY_SHOW_TIP, false);
+ if (showTip) {
+ Toast.makeText(getApplicationContext(), R.string.version_is_latest, Toast.LENGTH_SHORT).show();
+ }
+ }
+ new Thread(() -> {
+ VCommends.c(getApplicationContext());
+ }).start();
+
+ } catch (JSONException e) {
+ Log.e(TAG, "version info parse error!!", e);
+ } catch (Throwable e) {
+ Log.e(TAG, "check version failed:", e);
+ } finally {
+ stopSelf();
+ }
+ }
+
+ public static void checkUpdateImmediately(Context context, boolean showTip) {
+ Bundle bundle = new Bundle();
+ bundle.putBoolean(KEY_SHOW_TIP, showTip);
+
+ VersionParams.Builder builder = new VersionParams.Builder()
+ .setRequestUrl(CHECK_VERION_URL)
+ .setShowDownloadingDialog(false)
+ .setParamBundle(bundle)
+ .setService(VAVersionService.class);
+
+ AllenChecker.startVersionCheck(context, builder.build());
+ }
+
+ public static void checkUpdate(Context context, boolean showTip) {
+ long now = SystemClock.elapsedRealtime();
+ if (now - sLastCheckTime > CHECK_INTERVAL) {
+ checkUpdateImmediately(context, showTip);
+ sLastCheckTime = now;
+ }
+ }
+
+ private static int getCurrentVersionCode(Context context) {
+ try {
+ // ---get the package info---
+ PackageManager pm = context.getPackageManager();
+ PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0);
+ return pi.versionCode;
+ } catch (Exception e) {
+ Log.e("VersionInfo", "Exception", e);
+ }
+ return -1;
+ }
+}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/utils/DialogUtil.java b/VirtualApp/app/src/main/java/io/virtualapp/utils/DialogUtil.java
new file mode 100644
index 000000000..b872a7016
--- /dev/null
+++ b/VirtualApp/app/src/main/java/io/virtualapp/utils/DialogUtil.java
@@ -0,0 +1,20 @@
+package io.virtualapp.utils;
+
+import android.support.v7.app.AlertDialog;
+
+/**
+ * @author weishu
+ * @date 2018/7/5.
+ */
+public class DialogUtil {
+ public static void showDialog(AlertDialog dialog) {
+ if (dialog == null) {
+ return;
+ }
+ try {
+ dialog.show();
+ } catch (Throwable e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/VirtualApp/app/src/main/java/io/virtualapp/utils/HanziToPinyin.java b/VirtualApp/app/src/main/java/io/virtualapp/utils/HanziToPinyin.java
new file mode 100644
index 000000000..b626e9a06
--- /dev/null
+++ b/VirtualApp/app/src/main/java/io/virtualapp/utils/HanziToPinyin.java
@@ -0,0 +1,576 @@
+package io.virtualapp.utils;
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Locale;
+
+/**
+ * An object to convert Chinese character to its corresponding pinyin string. For characters with
+ * multiple possible pinyin string, only one is selected according to collator. Polyphone is not
+ * supported in this implementation. This class is implemented to achieve the best runtime
+ * performance and minimum runtime resources with tolerable sacrifice of accuracy. This
+ * implementation highly depends on zh_CN ICU collation data and must be always synchronized with
+ * ICU.
+ *
+ * Currently this file is aligned to zh.txt in ICU 4.6
+ */
+public class HanziToPinyin {
+ private static final String TAG = "HanziToPinyin";
+
+ // Turn on this flag when we want to check internal data structure.
+ private static final boolean DEBUG = false;
+
+ /**
+ * Unihans array.
+ *