├── .gitignore
├── README.md
├── build.sbt
├── project
├── build.properties
└── plugins.sbt
├── screencast
├── main.png
├── new.png
├── new1.png
└── record.mp4
├── src
├── main
│ ├── AndroidManifest.xml
│ ├── assets
│ │ └── xposed_init
│ ├── java
│ │ └── im
│ │ │ └── xun
│ │ │ └── shelldroid
│ │ │ └── App.java
│ ├── res
│ │ ├── drawable
│ │ │ ├── app_icon.png
│ │ │ ├── github.png
│ │ │ ├── ic_add_circle_black_24dp.png
│ │ │ ├── ic_add_white_3x.png
│ │ │ ├── ic_attachment.png
│ │ │ ├── ic_done.png
│ │ │ ├── ic_emoticon.png
│ │ │ ├── ic_image.png
│ │ │ ├── ic_info_black_24dp.xml
│ │ │ ├── ic_menu.png
│ │ │ ├── ic_menu_camera.xml
│ │ │ ├── ic_menu_gallery.xml
│ │ │ ├── ic_menu_manage.xml
│ │ │ ├── ic_menu_send.xml
│ │ │ ├── ic_menu_share.xml
│ │ │ ├── ic_menu_slideshow.xml
│ │ │ ├── ic_notifications_black_24dp.xml
│ │ │ ├── ic_place.png
│ │ │ ├── ic_sync_black_24dp.xml
│ │ │ └── side_nav_bar.xml
│ │ ├── layout
│ │ │ ├── app_bar_main.xml
│ │ │ ├── card.xml
│ │ │ ├── content_main.xml
│ │ │ ├── main_layout.xml
│ │ │ ├── nav_header_main.xml
│ │ │ ├── new_layout.xml
│ │ │ └── spinner_item.xml
│ │ ├── menu
│ │ │ ├── activity_main_drawer.xml
│ │ │ └── setting.xml
│ │ └── values
│ │ │ ├── colors.xml
│ │ │ ├── dimens.xml
│ │ │ ├── strings.xml
│ │ │ └── style.xml
│ └── scala
│ │ └── im
│ │ └── xun
│ │ └── shelldroid
│ │ ├── model
│ │ ├── Env.scala
│ │ └── EnvManager.scala
│ │ ├── ui
│ │ ├── DataAdapter.scala
│ │ ├── MainActivity.scala
│ │ ├── NewActivity.scala
│ │ └── SpinnerAdapter.scala
│ │ ├── utils
│ │ ├── AndroidUtils.scala
│ │ └── Log.scala
│ │ └── xposed
│ │ └── XposedMod.scala
└── test
│ └── scala
│ └── im
│ └── xun
│ └── shelldroid
│ ├── EnvManagerSpec.scala
│ └── EnvSerializeSpec.scala
└── target
└── android
└── output
└── shelldroid-debug.apk
/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | .idea
3 | .idea_modules
4 | proguard-sbt.txt
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### What is ShellDroid
2 |
3 | ShellDroid is an Android App that help you to manage multi account of any App on the same devices. For example, you can use it to switch WeChat or Whatsapp account without reenter your username and password.
4 |
5 | The mechanism behind ShellDroid is very simple: Every App on Android has a data folder, when you use that App( login or change some setting) all those information are saved in that folder, ShellDroid manage App's account by manage App's application data. which is simply backup and restore data folder. And that's why it need root privilege.
6 |
7 | Some Apps(like Wechat) will try to bind your account with your devices. Those Apps usually use your device's IMEI,phone number, or phone model to identify you. Thanks to [Xposed](http://repo.xposed.info/), ShellDroid can fake those information to make a "portable" application data.
8 |
9 | To demonstrate that, I create an [App](https://github.com/wuhx/phoneinfo) which simply call some Android API to identify your devices. If run it directly, It will display your devices's IMEI, phone model, phone brand etc.(assume you are not using any other privacy protection Apps of cause)
10 |
11 | But if run it from ShellDroid, those information will be replaced by your Virtual Environment settings.
12 |
13 | Basically it's a simplified [XPrivacy](https://github.com/M66B/XPrivacy) which not only restrict privacy information access by per App's setting, but also base on the account in that App. Which means different Account of the same App can have different privacy restriction.
14 |
15 | This App is not very complete, but the idea is well proved.
16 |
17 | Any issues, fork, pull requests will be welcome.
18 |
19 | ### Screencast
20 |
21 | Download [Video ScreenCast](screencast/record.mp4?raw=true)
22 |
23 |
24 | 
25 | 
26 | 
27 |
28 | ### How to build
29 |
30 | Install Android SDK(default version 24.4.1), or change the build.sbt to match your SDK version. Then run:
31 |
32 | `sbt apk`
33 |
34 | A prebuilt version can be download [here](target/android/output/shelldroid-debug.apk?raw=true)
35 |
36 | ### Notice
37 |
38 | 1. Your device need to be rooted.
39 | 2. Xposed framework need to be installed.
40 | 3. SeLinux will be disabled after ShellDroid get running.
41 | 4. Only test on Nexus 5X with Android 6.0. Use at your own risk.
42 |
43 | ### More info:
44 |
45 | [http://xun.im/2016/05/30/shelldroid-a-virtual-environments-for-android-apps/](http://xun.im/2016/05/30/shelldroid-a-virtual-environments-for-android-apps/)
46 |
47 | [http://xun.im/2016/05/30/shelldroid-on-nexus5x/](http://xun.im/2016/05/30/shelldroid-on-nexus5x/)
48 |
49 | [http://xun.im/2016/05/30/understand-android-rooting/](http://xun.im/2016/05/30/understand-android-rooting/)
50 |
51 |
52 |
--------------------------------------------------------------------------------
/build.sbt:
--------------------------------------------------------------------------------
1 | import android.Keys._
2 | import android.Dependencies.{LibraryDependency, aar}
3 | import sbt.Keys._
4 | enablePlugins(AndroidApp)
5 |
6 | name := "shelldroid"
7 |
8 | organization := "im.xun"
9 |
10 | scalaVersion := "2.11.8"
11 |
12 | platformTarget in Android := "android-23"
13 |
14 | version := "0.0.2"
15 |
16 | val supportVersion="23.1.1"
17 | //~ protify
18 |
19 | val cleanResourceTask = TaskKey[Unit]("cleanResource", "clean .DS_Store file in res folder, which will cause error: TR.scala:164: illegal start of simple pattern")
20 | cleanResourceTask := {
21 | println("Clean .DS_Store")
22 | "find . -name .DS_Store" #| "xargs rm -fr" !
23 | }
24 |
25 |
26 | javacOptions ++= Seq("-source", "1.7", "-target", "1.7")
27 | scalacOptions ++= Seq("-feature", "-deprecation", "-target:jvm-1.7")
28 |
29 | resolvers += Resolver.bintrayRepo("rovo89", "de.robv.android.xposed")
30 |
31 | libraryDependencies ++= Seq(
32 | "de.robv.android.xposed" % "api" % "82" % "provided" withSources(),
33 | "com.lihaoyi" %% "upickle" % "0.4.1",
34 | "org.scalatest" %% "scalatest" % "3.0.1" % "test",
35 | aar("com.android.support" % "design" % supportVersion),
36 | aar("com.android.support" % "cardview-v7" % supportVersion),
37 | aar("com.android.support" % "support-v4" % supportVersion),
38 | aar("com.android.support" % "appcompat-v7" % supportVersion),
39 | aar("com.android.support" % "recyclerview-v7" % supportVersion)
40 | )
41 |
42 | proguardScala in Android := true
43 |
44 | //dexMulti in Android := true
45 |
46 | proguardOptions in Android ++= Seq(
47 | "-ignorewarnings",
48 | "-keep class scala.Dynamic",
49 | "-keep class im.xun.shelldroid.** { *; }",
50 | "-keep interface im.xun.shelldroid.** { *; }",
51 | "-keep class de.robv.android.xposed.** { *; }",
52 | "-keep interface de.robv.android.xposed.** { *; }"
53 | )
54 | //protifySettings
55 |
56 | addCommandAlias("apk", ";cleanResource;android:run")
57 |
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=0.13.13
2 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("org.scala-android" % "sbt-android" % "1.7.2")
2 |
3 |
--------------------------------------------------------------------------------
/screencast/main.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wuhx/shelldroid/83c2d1d7e23d790c3b8bada7a2c738bb0ea3fd24/screencast/main.png
--------------------------------------------------------------------------------
/screencast/new.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wuhx/shelldroid/83c2d1d7e23d790c3b8bada7a2c738bb0ea3fd24/screencast/new.png
--------------------------------------------------------------------------------
/screencast/new1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wuhx/shelldroid/83c2d1d7e23d790c3b8bada7a2c738bb0ea3fd24/screencast/new1.png
--------------------------------------------------------------------------------
/screencast/record.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wuhx/shelldroid/83c2d1d7e23d790c3b8bada7a2c738bb0ea3fd24/screencast/record.mp4
--------------------------------------------------------------------------------
/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
9 |
10 |
15 |
16 |
19 |
22 |
25 |
26 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/main/assets/xposed_init:
--------------------------------------------------------------------------------
1 | im.xun.shelldroid.XposedMod
2 |
--------------------------------------------------------------------------------
/src/main/java/im/xun/shelldroid/App.java:
--------------------------------------------------------------------------------
1 | package im.xun.shelldroid;
2 |
3 | import android.app.Application;
4 | import android.content.Context;
5 |
6 | public class App extends Application {
7 | public static Context context;
8 |
9 | @Override public void onCreate() {
10 | super.onCreate();
11 | context = getApplicationContext();
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/res/drawable/app_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wuhx/shelldroid/83c2d1d7e23d790c3b8bada7a2c738bb0ea3fd24/src/main/res/drawable/app_icon.png
--------------------------------------------------------------------------------
/src/main/res/drawable/github.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wuhx/shelldroid/83c2d1d7e23d790c3b8bada7a2c738bb0ea3fd24/src/main/res/drawable/github.png
--------------------------------------------------------------------------------
/src/main/res/drawable/ic_add_circle_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wuhx/shelldroid/83c2d1d7e23d790c3b8bada7a2c738bb0ea3fd24/src/main/res/drawable/ic_add_circle_black_24dp.png
--------------------------------------------------------------------------------
/src/main/res/drawable/ic_add_white_3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wuhx/shelldroid/83c2d1d7e23d790c3b8bada7a2c738bb0ea3fd24/src/main/res/drawable/ic_add_white_3x.png
--------------------------------------------------------------------------------
/src/main/res/drawable/ic_attachment.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wuhx/shelldroid/83c2d1d7e23d790c3b8bada7a2c738bb0ea3fd24/src/main/res/drawable/ic_attachment.png
--------------------------------------------------------------------------------
/src/main/res/drawable/ic_done.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wuhx/shelldroid/83c2d1d7e23d790c3b8bada7a2c738bb0ea3fd24/src/main/res/drawable/ic_done.png
--------------------------------------------------------------------------------
/src/main/res/drawable/ic_emoticon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wuhx/shelldroid/83c2d1d7e23d790c3b8bada7a2c738bb0ea3fd24/src/main/res/drawable/ic_emoticon.png
--------------------------------------------------------------------------------
/src/main/res/drawable/ic_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wuhx/shelldroid/83c2d1d7e23d790c3b8bada7a2c738bb0ea3fd24/src/main/res/drawable/ic_image.png
--------------------------------------------------------------------------------
/src/main/res/drawable/ic_info_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/src/main/res/drawable/ic_menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wuhx/shelldroid/83c2d1d7e23d790c3b8bada7a2c738bb0ea3fd24/src/main/res/drawable/ic_menu.png
--------------------------------------------------------------------------------
/src/main/res/drawable/ic_menu_camera.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
13 |
--------------------------------------------------------------------------------
/src/main/res/drawable/ic_menu_gallery.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/src/main/res/drawable/ic_menu_manage.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
--------------------------------------------------------------------------------
/src/main/res/drawable/ic_menu_send.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/src/main/res/drawable/ic_menu_share.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/src/main/res/drawable/ic_menu_slideshow.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/src/main/res/drawable/ic_notifications_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/src/main/res/drawable/ic_place.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wuhx/shelldroid/83c2d1d7e23d790c3b8bada7a2c738bb0ea3fd24/src/main/res/drawable/ic_place.png
--------------------------------------------------------------------------------
/src/main/res/drawable/ic_sync_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
--------------------------------------------------------------------------------
/src/main/res/drawable/side_nav_bar.xml:
--------------------------------------------------------------------------------
1 |
3 |
9 |
--------------------------------------------------------------------------------
/src/main/res/layout/app_bar_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
14 |
15 |
21 |
22 |
23 |
24 |
25 |
26 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/main/res/layout/card.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
15 |
20 |
21 |
26 |
27 |
28 |
29 |
37 |
38 |
53 |
54 |
66 |
67 |
77 |
78 |
87 |
88 |
89 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/src/main/res/layout/content_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/main/res/layout/main_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
15 |
16 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/main/res/layout/nav_header_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
20 |
21 |
27 |
28 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/main/res/layout/new_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
16 |
17 |
26 |
27 |
32 |
33 |
34 |
35 |
39 |
40 |
43 |
49 |
50 |
51 |
56 |
57 |
58 |
61 |
62 |
69 |
70 |
71 |
72 |
75 |
76 |
83 |
84 |
85 |
86 |
89 |
90 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
--------------------------------------------------------------------------------
/src/main/res/layout/spinner_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
14 |
15 |
23 |
24 |
--------------------------------------------------------------------------------
/src/main/res/menu/activity_main_drawer.xml:
--------------------------------------------------------------------------------
1 |
2 |
18 |
--------------------------------------------------------------------------------
/src/main/res/menu/setting.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
9 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #ff4081
6 |
7 | #f10052
8 |
9 | #333333
10 | #555555
11 | #888888
12 |
13 | #1E9618
14 | #801E9618
15 | #146310
16 |
--------------------------------------------------------------------------------
/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 160dp
5 |
6 | 16dp
7 | 16dp
8 | 16dp
9 |
10 | 16dp
11 | 8dp
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | ShellDroid
3 | TestActivity
4 |
5 | Open navigation drawer
6 | Close navigation drawer
7 |
8 | Setting
9 | Create new Virtual Environment
10 |
11 |
12 | System sync settings
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/main/res/values/style.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/main/scala/im/xun/shelldroid/model/Env.scala:
--------------------------------------------------------------------------------
1 | package im.xun.shelldroid.model
2 |
3 | import android.graphics.drawable.Drawable
4 |
5 | case class AppInfo(appName: String, pkgName: String, icon: Drawable)
6 | case class Location(latitude: Double, longitude: Double)
7 | case class Env(id: String,
8 | envName: String,
9 | appName: String,
10 | pkgName: String,
11 | active: Boolean = false,
12 | deviceId: String= "",
13 | phoneNumber: String= "",
14 | networkCountryIso: String= "",
15 | networkOperator: String= "",
16 | simSerialNumber: String= "",
17 | buildBoard: String= "",
18 | buildModel: String= "",
19 | buildManufacturer: String= "",
20 | buildId: String= "",
21 | buildDevice: String= "",
22 | buildSerial: String= "",
23 | buildBrand: String= "",
24 | androidId: String = "",
25 | location: Option[Location]=None
26 | )
27 |
28 |
--------------------------------------------------------------------------------
/src/main/scala/im/xun/shelldroid/model/EnvManager.scala:
--------------------------------------------------------------------------------
1 | package im.xun.shelldroid
2 |
3 | import java.io.File
4 |
5 | import im.xun.shelldroid.utils.Log._
6 | import im.xun.shelldroid.model.Env
7 |
8 | import scala.io.Source
9 | import scala.util.{Success, Try}
10 |
11 | object EnvManager {
12 |
13 | val envRepoPath = App.context.getFilesDir.toString + "/ENV_REPO"
14 |
15 | val envRepo = {
16 | val repo = new File(envRepoPath)
17 | if(! repo.exists()) {
18 | repo.mkdirs()
19 | }
20 | envRepoPath
21 | }
22 |
23 | def readEnv(filepath: String): Try[Env] = {
24 | Try{
25 | val json = Source.fromFile(filepath,"utf8").mkString
26 | upickle.default.read[Env](json)
27 | }
28 | }
29 | def saveEnv(env: Env, filepath: String ) = {
30 | d(s"save Env: $env to $filepath ")
31 | val out = new java.io.PrintWriter(filepath, "utf8")
32 | try{
33 | val jsonString = upickle.default.write(env)
34 | out.print(jsonString)
35 | } catch {
36 | case exp: Throwable =>
37 | e(s"fail to save Env:$env with exception: $exp")
38 | } finally { out.close() }
39 | }
40 |
41 | /*
42 | * 扫描Env配置
43 | * */
44 | def scanEnvs: Seq[Env] = {
45 |
46 | val root = new File(envRepo)
47 |
48 | val appCurrentEnv = root.listFiles().map(app => app.toString + "/.RUNNING")
49 |
50 | val appEnvs = for{
51 | apps <- root.listFiles.filter(_.isDirectory)
52 | envs <- apps.listFiles.filter(_.isDirectory)
53 | } yield envs.toString + "/.ENV"
54 |
55 | val allEnvs = appCurrentEnv ++ appEnvs
56 | for(env <- allEnvs) {
57 | d(s"Find Env: $env")
58 | }
59 | allEnvs map readEnv filter(_.isSuccess) map(_.get)
60 |
61 | }
62 |
63 | val ensureSelinuxPermissive = {
64 | d("ensureSelinuxPermissive")
65 | try {
66 | val proc = Runtime.getRuntime.exec("su -c setenforce 0")
67 | proc.waitFor()
68 |
69 | } catch {
70 | case _: Throwable =>
71 | e("Fail to disable selinux")
72 | }
73 | }
74 |
75 | def doRoot(cmd: String) = {
76 | d("doRoot: "+cmd)
77 | try {
78 | val proc = Runtime.getRuntime.exec("su -c " + cmd)
79 | proc.waitFor()
80 | } catch {
81 | case _: Throwable =>
82 | e("Fail to run cmd: "+cmd)
83 | }
84 | }
85 |
86 | def killApp(env: Env) = {
87 | val cmd = "am force-stop %s".format(env.pkgName)
88 | doRoot(cmd)
89 | }
90 |
91 | def startApp(env: Env) = {
92 | val cmd = "monkey -p %s -c android.intent.category.LAUNCHER 1".format(env.pkgName)
93 | doRoot(cmd)
94 | }
95 |
96 | def updateAppLastRunning(env: Env): Unit = {
97 | d("update last running: " + env)
98 | val filepath = getAppRepoDir(env) + "/.RUNNING"
99 | saveEnv(env.copy(active=true), filepath)
100 | }
101 |
102 | def appLastRunning(env: Env): Option[Env] = {
103 | val filepath = getAppRepoDir(env) + "/.RUNNING"
104 | Try{
105 | val lastString= scala.io.Source.fromFile(filepath,"utf8").mkString
106 | upickle.default.read[Env](lastString)
107 | } match {
108 | case Success(value) =>
109 | Some(value)
110 | case exp =>
111 | e(s"fail to get last running! $exp")
112 | None
113 | }
114 | }
115 |
116 | def getAppRepoDir(env: Env) = {
117 | envRepo + "/" + env.pkgName
118 | }
119 |
120 | def getEnvDir(env: Env) = {
121 | getAppRepoDir(env) + "/" + env.id
122 | }
123 |
124 | def getAppDir(env: Env) = {
125 | "/data/data/" + env.pkgName
126 | }
127 |
128 | def envDirExist(env: Env) = {
129 | new File(getEnvDir(env)).exists()
130 | }
131 |
132 | def envDirBuild(env: Env) = {
133 | Seq(
134 | "mkdir -p %s".format(getEnvDir(env)),
135 | "cp -a %s/lib %s".format(getAppDir(env), getEnvDir(env)),
136 | "chmod 777 %s -R".format(getAppRepoDir(env))
137 | ).map(doRoot)
138 |
139 | val envFile = getEnvDir(env) + "/.ENV"
140 | saveEnv(env, envFile)
141 | Seq(
142 | "chmod 777 %s".format(envFile)
143 | ).map(doRoot)
144 | }
145 |
146 | def switchEnv(env: Env, lastEnv: Env) = {
147 | killApp(env)
148 | Seq(
149 | "mv %s %s".format(getAppDir(env), getEnvDir(lastEnv)),
150 | "mv %s %s".format(getEnvDir(env), getAppDir(env))
151 | ) map doRoot
152 | }
153 |
154 | def delete(env: Env) = {
155 | val cmd = "rm -fr %s".format(getEnvDir(env))
156 | doRoot(cmd)
157 | }
158 |
159 | def active(env: Env) = {
160 | d(s"active env:\n$env")
161 | if(appLastRunning(env).isEmpty) {
162 | if(!envDirExist(env)) {
163 | envDirBuild(env)
164 | }
165 | switchEnv(env, env.copy(id= "pre-shelldroid-data"))
166 | } else if(appLastRunning(env).get.id != env.id) {
167 | val last = appLastRunning(env).get
168 | d(s"last env:\n$last")
169 | if(!envDirExist(env)) {
170 | envDirBuild(env)
171 | }
172 | switchEnv(env,last)
173 | }
174 | updateAppLastRunning(env)
175 | startApp(env)
176 | }
177 |
178 | }
179 |
--------------------------------------------------------------------------------
/src/main/scala/im/xun/shelldroid/ui/DataAdapter.scala:
--------------------------------------------------------------------------------
1 | package im.xun.shelldroid
2 |
3 | import android.app.ProgressDialog
4 | import android.content.{Intent, DialogInterface}
5 | import android.content.DialogInterface.OnClickListener
6 | import android.support.v4.content.{ContextCompat, IntentCompat}
7 | import android.support.v7.app.AlertDialog
8 | import android.support.v7.widget.{CardView, RecyclerView}
9 | import android.view.View.OnLongClickListener
10 | import android.view.{View, LayoutInflater, ViewGroup}
11 | import android.widget.{ImageView, Button, TextView}
12 | import im.xun.shelldroid.model.Env
13 | import im.xun.shelldroid.utils.Log._
14 | import utils.AndroidUtils._
15 | import scala.concurrent.Future
16 |
17 | class ViewHolder(val card: CardView, val envs: Seq[Env] ) extends RecyclerView.ViewHolder(card) {
18 | val ivIcon = card.findViewById(R.id.icon).asInstanceOf[ImageView]
19 | val textName = card.findViewById(R.id.envName).asInstanceOf[TextView]
20 | val textAppName = card.findViewById(R.id.appName).asInstanceOf[TextView]
21 | val textImei = card.findViewById(R.id.imei).asInstanceOf[TextView]
22 |
23 | val btn = card.findViewById(R.id.my_button).asInstanceOf[Button]
24 |
25 | card.setOnLongClickListener( new OnLongClickListener {
26 | override def onLongClick(v: View): Boolean = {
27 | val env = envs(ViewHolder.this.getAdapterPosition)
28 | if(!env.active){
29 | // val red = ContextCompat.getColor(card.getContext,R.color.warn_red)
30 | // btn.setBackgroundColor(red)
31 | btn.setText("Delete")
32 | }
33 | true
34 | }
35 | })
36 |
37 | card.setOnClickListener(new View.OnClickListener {
38 | override def onClick(v: View): Unit = {
39 | if(btn.getText.toString.toLowerCase.contains("delete")) {
40 | btn.setText("Start")
41 | }
42 | }
43 | })
44 |
45 | lazy val switchDialog = new AlertDialog.Builder(card.getContext)
46 | .setTitle("Switching Environment")
47 | .setMessage("Jump to selected App with new Virtual Environment!")
48 | .setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
49 | def onClick(interface: DialogInterface, i: Int) = {
50 | interface.cancel()
51 | }
52 | })
53 | .setPositiveButton("OK",
54 | new OnClickListener {
55 | override def onClick(dialog: DialogInterface, which: Int): Unit = {
56 | val processDialog = new ProgressDialog(card.getContext)
57 | processDialog.setTitle("Jump to App...")
58 | processDialog.setMessage("please wait...")
59 | processDialog.setIndeterminate(true)
60 | processDialog.setCancelable(false)
61 | processDialog.show()
62 |
63 | Future {
64 | val env = envs(ViewHolder.this.getAdapterPosition)
65 | d("env clicked!:"+env.toString)
66 | EnvManager.active(env)
67 | } onComplete {
68 | case _ =>
69 | processDialog.dismiss()
70 | }
71 | }
72 | })
73 |
74 |
75 | def refresh() = {
76 | val intent = new Intent(App.context, classOf[MainActivity])
77 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |IntentCompat.FLAG_ACTIVITY_CLEAR_TASK);
78 | App.context.getApplicationContext.startActivity(intent)
79 | }
80 |
81 | lazy val deleteDialog = new AlertDialog.Builder(card.getContext)
82 | .setTitle("Delete")
83 | .setMessage("Delete Virtual Environment!")
84 | .setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
85 | def onClick(interface: DialogInterface, i: Int) = {
86 | interface.cancel()
87 | }
88 | })
89 | .setPositiveButton("OK",
90 | new OnClickListener {
91 | override def onClick(dialog: DialogInterface, which: Int): Unit = {
92 | val processDialog = new ProgressDialog(card.getContext)
93 | processDialog.setTitle("Delete")
94 | processDialog.setMessage("please wait...")
95 | processDialog.setIndeterminate(true)
96 | processDialog.setCancelable(false)
97 | processDialog.show()
98 |
99 | Future {
100 | val env = envs(ViewHolder.this.getAdapterPosition)
101 | EnvManager.delete(env)
102 | } onComplete {
103 | case _ =>
104 | processDialog.dismiss()
105 | refresh()
106 | }
107 | }
108 | })
109 |
110 | btn.setOnClickListener(new View.OnClickListener {
111 | override def onClick(v: View): Unit = {
112 | if(btn.getText.toString.toLowerCase.contains("delete")) {
113 | deleteDialog.create().show()
114 | } else {
115 | switchDialog.create().show()
116 | }
117 | }
118 | })
119 |
120 | }
121 |
122 | class DataAdapter extends RecyclerView.Adapter[ViewHolder]{
123 | lazy val envs = EnvManager.scanEnvs
124 |
125 | override def getItemCount = {
126 | envs.size
127 | }
128 |
129 | override def onCreateViewHolder(vg: ViewGroup, pos: Int) = {
130 | //e("onCreateViewHolder pos:"+pos)
131 | val card = LayoutInflater.from(vg.getContext).inflate(R.layout.card, vg, false).asInstanceOf[CardView]
132 | new ViewHolder(card, envs)
133 | }
134 |
135 | override def onBindViewHolder(vh: ViewHolder, pos: Int) = {
136 | //e(s"onBindViewHolder: $pos")
137 | val env = envs(pos)
138 | vh.ivIcon.setImageDrawable(getIcon(env.pkgName))
139 | vh.textName.setText(env.envName)
140 | vh.textAppName.setText(env.appName)
141 | vh.textImei.setText(env.deviceId)
142 | }
143 |
144 | }
145 |
--------------------------------------------------------------------------------
/src/main/scala/im/xun/shelldroid/ui/MainActivity.scala:
--------------------------------------------------------------------------------
1 | package im.xun.shelldroid
2 |
3 | import android.content.{DialogInterface, Intent}
4 | import android.net.Uri
5 | import android.support.design.widget.{Snackbar, NavigationView}
6 | import android.support.design.widget.NavigationView.OnNavigationItemSelectedListener
7 | import android.support.v4.view.GravityCompat
8 | import android.support.v7.app.{AlertDialog, ActionBarDrawerToggle, AppCompatActivity}
9 | import android.os.Bundle
10 | import android.support.v7.widget.LinearLayoutManager
11 | import android.view.{Menu, MenuItem, View}
12 | import android.view.View.OnClickListener
13 | import android.widget.Toast
14 | import im.xun.shelldroid.utils.Log._
15 |
16 | // mix in Contexts for Activity
17 | class MainActivity
18 | extends AppCompatActivity
19 | with TypedFindView {
20 |
21 | lazy val toolbar = findView(TR.toolbar)
22 | lazy val fab = findView(TR.fab)
23 | lazy val drawer = findView(TR.drawer_layout)
24 | lazy val navigationView = findView(TR.nav_view)
25 | lazy val recyclerView = findView(TR.rv)
26 |
27 | lazy val dataAdapter = new DataAdapter
28 |
29 | override def onActivityResult(requestCode: Int, resultCode: Int, intent: Intent) = {
30 | recyclerView.setAdapter(new DataAdapter)
31 | }
32 |
33 | override def onCreate(savedInstanceState: Bundle) = {
34 | super.onCreate(savedInstanceState)
35 | setContentView(R.layout.main_layout)
36 |
37 | setSupportActionBar(toolbar)
38 |
39 | fab.setOnClickListener(new OnClickListener {
40 | override def onClick(v: View): Unit = {
41 | val intent = new Intent(MainActivity.this, classOf[NewActivity])
42 | startActivityForResult(intent,0)
43 | }
44 | })
45 |
46 | //toolbar上的菜单图标唤起右边栏
47 | val toggle = new ActionBarDrawerToggle(
48 | this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close)
49 | drawer.setDrawerListener(toggle)
50 | toggle.syncState()
51 |
52 |
53 | //设置右边栏的菜单点击
54 | navigationView.setNavigationItemSelectedListener( new OnNavigationItemSelectedListener {
55 | override def onNavigationItemSelected(menuItem: MenuItem): Boolean = {
56 | menuItem.setChecked(true)
57 |
58 | val title = menuItem.getTitle.toString.toLowerCase()
59 | d(s"select title: $title")
60 |
61 | if(title.contains("github")){
62 | val browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/wuhx/shelldroid"))
63 | startActivity(browserIntent);
64 | }
65 | if(title.contains("issues")){
66 | val intent = new Intent(Intent.ACTION_SENDTO)
67 | val uriText = "mailto:i@xun.im?subject=About ShellDroid&body=Hi~"
68 | intent.setData(Uri.parse(uriText))
69 | // intent.putExtra(Intent.EXTRA_EMAIL, "i@xun.im")
70 | // intent.putExtra(Intent.EXTRA_SUBJECT, "About Shelldroid")
71 | startActivity(Intent.createChooser(intent, "Send Email"))
72 | }
73 |
74 | drawer.closeDrawer(GravityCompat.START)
75 | // Toast.makeText(MainActivity.this, menuItem.getTitle, Toast.LENGTH_LONG).show()
76 | true
77 | }
78 | })
79 |
80 | //设置主页面
81 | recyclerView.setLayoutManager(new LinearLayoutManager(this))
82 | recyclerView.setAdapter(dataAdapter)
83 |
84 |
85 | }
86 |
87 | //显示toolbar上的设置按钮
88 | override def onCreateOptionsMenu(menu: Menu) = {
89 | getMenuInflater.inflate(R.menu.setting, menu)
90 | true
91 | }
92 |
93 | //响应toolbar上的设置按钮
94 | override def onOptionsItemSelected(item: MenuItem) = {
95 | // Handle action bar item clicks here. The action bar will
96 | // automatically handle clicks on the Home/Up button, so long
97 | // as you specify a parent activity in AndroidManifest.xml.
98 | val id = item.getItemId
99 |
100 | //noinspection SimplifiableIfStatement
101 | if (id == R.id.action_settings) {
102 | d("options item selected!")
103 | }
104 |
105 | super.onOptionsItemSelected(item);
106 | }
107 |
108 | }
109 |
--------------------------------------------------------------------------------
/src/main/scala/im/xun/shelldroid/ui/NewActivity.scala:
--------------------------------------------------------------------------------
1 | package im.xun.shelldroid
2 |
3 | import android.graphics.Color
4 | import android.os.Bundle
5 | import android.support.v7.app.AppCompatActivity
6 | import android.view.{ViewParent, View}
7 | import android.view.View.OnClickListener
8 | import android.widget.AdapterView.OnItemSelectedListener
9 | import android.widget.{AdapterView, ArrayAdapter}
10 | import im.xun.shelldroid.model._
11 | import utils.Log._
12 | import utils.AndroidUtils._
13 | import scala.collection.JavaConversions._
14 |
15 |
16 | class NewActivity
17 | extends AppCompatActivity
18 | with TypedFindView {
19 |
20 |
21 | lazy val toolbar = findView(TR.my_toolbar2)
22 | lazy val btn = findView(TR.btn)
23 | lazy val textName = findView(TR.textName)
24 | lazy val textPhoneModel = findView(TR.textPhoneModel)
25 | lazy val textPhoneBrand = findView(TR.textPhoneBrand)
26 | lazy val textImei = findView(TR.textImei)
27 |
28 | lazy val spinner = findView(TR.spinner)
29 |
30 |
31 | def save(env: Env) = {
32 | d("Save env: "+env)
33 | EnvManager.envDirBuild(env)
34 | }
35 |
36 | def quit(): Unit = {
37 | setResult(0)
38 | super.finish()
39 | }
40 |
41 | override def onCreate(savedInstanceState: Bundle) {
42 |
43 | super.onCreate(savedInstanceState)
44 | setContentView(R.layout.new_layout)
45 |
46 | setSupportActionBar(toolbar)
47 | toolbar.setTitleTextColor(Color.WHITE)
48 |
49 | val spAdapter = new SpinnerAdapter
50 | spinner.setAdapter(spAdapter)
51 |
52 | btn.setOnClickListener(new OnClickListener {
53 | override def onClick(v: View): Unit = {
54 | val appInfo = spinner.getSelectedItem.asInstanceOf[AppInfo]
55 | val env = Env(
56 | java.util.UUID.randomUUID().toString,
57 | textName,appInfo.appName ,
58 | appInfo.pkgName,
59 | active=false,
60 | textImei,
61 | buildModel= textPhoneModel,
62 | buildManufacturer = textPhoneBrand,
63 | buildBrand=textPhoneBrand)
64 | save(env)
65 | quit()
66 | }
67 | })
68 |
69 | }
70 | }
--------------------------------------------------------------------------------
/src/main/scala/im/xun/shelldroid/ui/SpinnerAdapter.scala:
--------------------------------------------------------------------------------
1 | package im.xun.shelldroid
2 |
3 | import android.view.{LayoutInflater, View, ViewGroup}
4 | import android.widget.{ImageView, TextView, BaseAdapter}
5 | import im.xun.shelldroid.model.AppInfo
6 | import im.xun.shelldroid.utils.AndroidUtils._
7 |
8 |
9 | class SpinnerAdapter extends BaseAdapter{
10 |
11 | lazy val data= getInstalledAppInfo
12 |
13 | override def getCount = data.size
14 |
15 | override def getItem(pos: Int) = data(pos)
16 |
17 | override def getItemId(pos: Int) = 0
18 |
19 | override def getView(pos: Int, view: View, vg: ViewGroup) = {
20 | val v= LayoutInflater.from(vg.getContext).inflate(R.layout.spinner_item, vg, false)
21 | if( v!=null){
22 | val iv = v.findViewById(R.id.itemIcon).asInstanceOf[ImageView]
23 | val tv = v.findViewById(R.id.itemName).asInstanceOf[TextView]
24 | val app = data(pos)
25 | iv.setImageDrawable(app.icon)
26 | tv.setText(app.appName)
27 | }
28 | v
29 | }
30 |
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/scala/im/xun/shelldroid/utils/AndroidUtils.scala:
--------------------------------------------------------------------------------
1 | package im.xun.shelldroid.utils
2 |
3 | import android.content.pm.{ApplicationInfo, PackageManager}
4 | import android.graphics.drawable.Drawable
5 | import android.os.AsyncTask
6 | import android.support.design.R
7 | import im.xun.shelldroid.App
8 | import im.xun.shelldroid.model.AppInfo
9 | import im.xun.shelldroid.utils.Log._
10 |
11 | import scala.concurrent.ExecutionContext
12 | import scala.language.implicitConversions
13 | import android.widget.TextView
14 | import scala.collection.JavaConversions._
15 |
16 | import scala.util.Try
17 |
18 |
19 | object AndroidUtils {
20 |
21 | implicit val exec = ExecutionContext.fromExecutor(
22 | AsyncTask.THREAD_POOL_EXECUTOR)
23 |
24 | implicit def textViewToString(tv: TextView): String = {
25 | tv.getText.toString
26 | }
27 |
28 | lazy val pm = App.context.getApplicationContext.getPackageManager
29 |
30 | def getIcon(pkgName: String): Drawable = {
31 | Try{
32 | pm.getApplicationIcon(pkgName)
33 | } getOrElse App.context.getDrawable(R.drawable.abc_btn_check_material)
34 | }
35 |
36 | def getDataDir(pkgName: String) = {
37 | pm.getApplicationInfo(pkgName,0).dataDir
38 | }
39 |
40 | def getInstalledAppInfo: Seq[AppInfo] = {
41 | for{
42 | pkg <- pm.getInstalledApplications(PackageManager.GET_META_DATA)
43 | if (pkg.flags & (ApplicationInfo.FLAG_SYSTEM | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)) == 0
44 | appName = pm.getApplicationLabel(pkg).toString
45 | } yield AppInfo(appName, pkg.packageName, getIcon(pkg.packageName))
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/src/main/scala/im/xun/shelldroid/utils/Log.scala:
--------------------------------------------------------------------------------
1 | package im.xun.shelldroid.utils
2 |
3 | /**
4 | * Created by wuhx on 1/28/16.
5 | */
6 | object Log {
7 |
8 | val TAG="SHELLDROID:"
9 | def d(msg: String) = {
10 | android.util.Log.d(TAG, msg)
11 | }
12 |
13 | def i(msg: String) = {
14 | android.util.Log.i(TAG, msg)
15 | }
16 |
17 | def e(msg: String) = {
18 | android.util.Log.e(TAG, msg)
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/scala/im/xun/shelldroid/xposed/XposedMod.scala:
--------------------------------------------------------------------------------
1 | package im.xun.shelldroid
2 |
3 | import android.location.{LocationManager, Location, LocationListener}
4 | import de.robv.android.xposed.IXposedHookZygoteInit.StartupParam
5 | import de.robv.android.xposed._
6 | import de.robv.android.xposed.callbacks.XC_LoadPackage
7 | import de.robv.android.xposed.XposedHelpers.findAndHookMethod
8 | import im.xun.shelldroid.model.Env
9 | import im.xun.shelldroid.utils.Log._
10 |
11 | import scala.io.Source
12 |
13 |
14 | class XposedMod extends IXposedHookLoadPackage with IXposedHookZygoteInit {
15 |
16 | var pkgName: String = _
17 |
18 | def checkEnv(pkg: String): Boolean = {
19 | val filepath = "/data/data/"+pkg+"/.ENV"
20 | new java.io.File(filepath).exists()
21 | }
22 |
23 | def getEnvFromConfigFile(app: String): Option[Env] = {
24 | val filepath = "/data/data/"+app+"/.ENV"
25 | try {
26 | d("Load config file from: " + filepath)
27 | val json = Source.fromFile(filepath, "utf8").mkString
28 | d(json)
29 | val env = upickle.default.read[Env](json)
30 | Some(env)
31 | } catch {
32 | case exp: Throwable =>
33 | e(s"Fail to parse env : $exp")
34 | None
35 | }
36 | }
37 |
38 | def hookBuildProperty(env: Env) = {
39 | val cls = XposedHelpers.findClass("android.os.Build", ClassLoader.getSystemClassLoader)
40 | if(env.buildBoard.nonEmpty) {
41 | d(s"Build property hook: Board ${env.buildBoard}")
42 | XposedHelpers.setStaticObjectField(cls, "BOARD", env.buildBoard)
43 | }
44 | if(env.buildManufacturer.nonEmpty) {
45 | d(s"Build property hook: MANUFACTURER ${env.buildManufacturer}")
46 | XposedHelpers.setStaticObjectField(cls, "MANUFACTURER", env.buildManufacturer)
47 | }
48 | if(env.buildSerial.nonEmpty) {
49 | d(s"Build property hook: SERIAL ${env.buildSerial}")
50 | XposedHelpers.setStaticObjectField(cls, "SERIAL", env.buildSerial)
51 | }
52 | if(env.buildModel.nonEmpty) {
53 | d(s"Build property hook: MODEL ${env.buildModel}")
54 | XposedHelpers.setStaticObjectField(cls, "MODEL", env.buildModel)
55 | }
56 | if(env.buildBrand.nonEmpty) {
57 | d(s"Build property hook: BRAND ${env.buildBrand}")
58 | XposedHelpers.setStaticObjectField(cls, "BRAND", env.buildBrand)
59 | }
60 |
61 | }
62 |
63 | def locationHook(env: Env, classLoader: ClassLoader): Unit = {
64 | //location hook
65 | findAndHookMethod("android.telephony.TelephonyManager", classLoader, "getCellLocation", new XC_MethodHook() {
66 | protected override def afterHookedMethod(param: XC_MethodHook.MethodHookParam) {
67 | //伪造空的基站列表,强制fallback到我们的gps hook
68 | d(s"Fake empty cell location for $pkgName")
69 | d(s"real result: ${param.getResult}")
70 | param.setResult(null)
71 | }
72 | })
73 |
74 | findAndHookMethod("android.telephony.TelephonyManager", classLoader, "getNeighboringCellInfo", new XC_MethodHook() {
75 | protected override def afterHookedMethod(param: XC_MethodHook.MethodHookParam) {
76 | //伪造空的基站列表
77 | d(s"Fake empty neighbor cell location for $pkgName")
78 | d(s"real result: ${param.getResult}")
79 | param.setResult(null)
80 | }
81 | })
82 |
83 | findAndHookMethod("android.net.wifi.WifiManager", classLoader, "getScanResults", new XC_MethodHook() {
84 | protected override def afterHookedMethod(param: XC_MethodHook.MethodHookParam) {
85 | //伪造空的wifi热点扫描结果
86 | d(s"Fake empty wifi scan results for $pkgName")
87 | d(s"real result: ${param.getResult}")
88 | param.setResult(null)
89 | }
90 | })
91 |
92 | //gps hook
93 | //https://developer.android.com/guide/topics/location/strategies.html
94 | //https://www.ibm.com/developerworks/cn/opensource/os-cn-android-location/
95 | //TODO: gps数据获取是通过回调函数实现的,这里不能直接修改, 考虑直接hook binder, 待实现.
96 | // findAndHookMethod("android.location.LocationManager", classLoader, "requestLocationUpdates", new XC_MethodHook() {
97 | // protected override def beforeHookedMethod(param: XC_MethodHook.MethodHookParam) {
98 | // //处理多种重载方式https://developer.android.com/reference/android/location/LocationManager.html#requestLocationUpdates(long, float, android.location.Criteria, android.app.PendingIntent)
99 | // for(arg <- param.args if arg.isInstanceOf[LocationListener]){
100 | // val listener = arg.asInstanceOf[LocationListener]
101 | // d(s"requestLocationUpdates listener hook: $listener")
102 | // val fakeLoc = new Location(LocationManager.GPS_PROVIDER)
103 | // fakeLoc.setLatitude(env.location.get.latitude)
104 | // fakeLoc.setLongitude(env.location.get.longitude)
105 | // listener.onLocationChanged(fakeLoc)
106 | // }
107 | // }
108 | // })
109 |
110 | }
111 | def setupEnv(env: Env, classLoader: ClassLoader) = {
112 | //getDeviceId
113 | findAndHookMethod("android.telephony.TelephonyManager", classLoader, "getDeviceId", new XC_MethodHook() {
114 | protected override def afterHookedMethod(param: XC_MethodHook.MethodHookParam) {
115 | d(s"Fake deviceid ${env.deviceId} for $pkgName")
116 | param.setResult(env.deviceId)
117 | }
118 | })
119 |
120 | //location hook
121 | if(env.location.nonEmpty) {
122 | locationHook(env,classLoader)
123 | }
124 |
125 | //system properties hook
126 | hookBuildProperty(env)
127 | }
128 |
129 |
130 | def initZygote(startupParam: StartupParam ) = {
131 | d("initZygote with module path: " + startupParam.modulePath)
132 | }
133 |
134 | def handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
135 |
136 | pkgName = lpparam.packageName
137 |
138 | if(checkEnv(pkgName)) {
139 | getEnvFromConfigFile(pkgName) match {
140 | case Some(env) =>
141 | d("setup env:"+env)
142 | setupEnv(env,lpparam.classLoader)
143 |
144 | case None =>
145 | e(".ENV file damaged! "+pkgName)
146 | }
147 | }
148 |
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/src/test/scala/im/xun/shelldroid/EnvManagerSpec.scala:
--------------------------------------------------------------------------------
1 | package im.xun.shelldroid
2 |
3 | import org.scalatest.FlatSpec
4 |
5 | class EnvManagerSpec extends FlatSpec {
6 |
7 | }
8 |
--------------------------------------------------------------------------------
/src/test/scala/im/xun/shelldroid/EnvSerializeSpec.scala:
--------------------------------------------------------------------------------
1 | package im.xun.shelldroid
2 |
3 | import im.xun.shelldroid.model.{Location, Env}
4 | import im.xun.shelldroid.utils.Log._
5 | import org.scalatest.{Matchers, FlatSpec}
6 |
7 | class EnvSerializeSpec extends FlatSpec with Matchers{
8 | it should "serialize" in {
9 | val location = Location(100.11,200.22)
10 | val env = Env("001","test1","weixin","com.tencent.mm",false,"2500",""
11 | ,"cn","46001","1234","bullhead","Nexus 191","ZTE","2344","Xiaomi","0010","Nubia","",Some(location)
12 | )
13 | val json = upickle.default.write(env,2)
14 | println(s"serialized json: \n$json")
15 | val newEnv = upickle.default.read[Env](json)
16 | println(s"deserialized env: \n$newEnv")
17 | newEnv shouldBe env
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/target/android/output/shelldroid-debug.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wuhx/shelldroid/83c2d1d7e23d790c3b8bada7a2c738bb0ea3fd24/target/android/output/shelldroid-debug.apk
--------------------------------------------------------------------------------