├── LICENSE
├── README.md
├── agent
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── hanhuy
│ │ └── android
│ │ └── protify
│ │ └── agent
│ │ ├── Protify.java
│ │ ├── ProtifyApplication.java
│ │ └── internal
│ │ ├── DexExtractor.java
│ │ ├── DexLoader.java
│ │ ├── LifecycleListener.java
│ │ ├── ProtifyActivity.java
│ │ ├── ProtifyReceiver.java
│ │ ├── ProtifyResources.java
│ │ ├── Restarter.java
│ │ └── ZipUtil.java
│ └── res
│ ├── drawable-hdpi
│ └── protify_internal_ic_notification_loading.png
│ ├── drawable-mdpi
│ └── protify_internal_ic_notification_loading.png
│ ├── drawable-xhdpi
│ └── protify_internal_ic_notification_loading.png
│ ├── values-v14
│ └── styles.xml
│ └── values
│ └── styles.xml
├── android
├── project.properties
└── src
│ ├── androidTest
│ ├── assets
│ │ └── resources-release.ap_
│ ├── java
│ │ └── com
│ │ │ └── hanhuy
│ │ │ └── android
│ │ │ └── keepshare
│ │ │ └── R.java
│ └── scala
│ │ └── com
│ │ └── hanhuy
│ │ └── android
│ │ └── protify
│ │ └── ResourcesTest.scala
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── hanhuy
│ │ └── android
│ │ └── protify
│ │ └── RTxtLoaderBase.java
│ ├── res
│ ├── drawable-hdpi
│ │ └── protify_ic_launcher_internal.png
│ ├── drawable-ldpi
│ │ └── protify_ic_launcher_internal.png
│ ├── drawable-mdpi
│ │ └── protify_ic_launcher_internal.png
│ ├── drawable-xhdpi
│ │ └── protify_ic_launcher_internal.png
│ ├── layout
│ │ └── main.xml
│ └── values
│ │ └── strings.xml
│ └── scala
│ └── com
│ └── hanhuy
│ └── android
│ └── protify
│ ├── DexActivity.scala
│ ├── LayoutActivity.scala
│ ├── MainActivity.scala
│ └── receivers.scala
├── build.sbt
├── common
└── src
│ └── main
│ └── java
│ └── com
│ └── hanhuy
│ └── android
│ └── protify
│ └── Intents.java
├── lib
└── src
│ └── main
│ └── java
│ └── com
│ └── hanhuy
│ └── android
│ └── protify
│ └── ActivityProxy.java
├── project
├── build.properties
└── plugins.sbt
└── sbt-plugin
└── src
├── main
└── scala
│ └── plugin.scala
└── sbt-test
└── protify
└── layout
├── build.sbt
├── project.properties
├── project
├── build.scala
└── plugins.sbt
├── src
├── androidTest
│ └── java
│ │ └── com
│ │ └── example
│ │ └── protify
│ │ └── MainActivityTest.java
└── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── example
│ │ └── protify
│ │ └── MainActivity.java
│ └── res
│ ├── drawable-hdpi
│ └── ic_launcher.png
│ ├── drawable-ldpi
│ └── ic_launcher.png
│ ├── drawable-mdpi
│ └── ic_launcher.png
│ ├── drawable-xhdpi
│ └── ic_launcher.png
│ ├── layout
│ └── main.xml
│ └── values
│ └── strings.xml
└── test
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
203 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # protify: instantaneous, on-device development for Android
2 |
3 | [](https://bintray.com/pfn/sbt-plugins/sbt-android-protify)
4 | [](https://gitter.im/scala-android/sbt-android?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
5 |
6 | NOTE: 1.2.0 is the last version published using
7 | `addSbtPlugin("com.hanhuy.sbt" % "android-protify" % "1.2.0")`,
8 | all future updates can be accessed by using
9 | `addSbtPlugin("org.scala-android" % "sbt-android-protify" % VERSION)`
10 |
11 | ## Features:
12 |
13 | * Multiple language support: Java (including retrolambda), Scala, Kotlin
14 | * No code changes to app required!
15 | * Support for appcompat-v7, design and support-v4 libraries
16 | * Works with most existing android projects
17 | * Android devices api level 5+
18 | * Dex sharding for near-instant deployment
19 |
20 | ## Demo:
21 |
22 | [](http://www.youtube.com/watch?v=LJLLyua0bYA)
23 |
24 | ## Getting started:
25 |
26 | ### Android Studio / Gradle Quick Start
27 |
28 | 1. Install sbt from http://scala-sbt.org, homebrew, ports, or your
29 | package manager of choice
30 | 2. Optionally add `idea-sbt-plugin` to run SBT inside of Android Studio
31 | 3. Install the
32 | [`sbt-android-gradle`](https://github.com/scala-android/sbt-android/blob/master/GRADLE.md)
33 | SBT plugin to automatically load your gradle build. From the base of your
34 | Android project, do:
35 | * `mkdir project`
36 | * `echo 'addSbtPlugin("org.scala-android" % "sbt-android-gradle" % "1.3.2")' > project/plugins.sbt`
37 | * `echo >> project/plugins.sbt`
38 | * If you have any flavors or build types that must be loaded:
39 | * `echo 'android.Plugin.withVariant("PROJECT-NAME (e.g. app)", Some("BUILD-TYPE"), Some("FLAVOR"))' > build.sbt`
40 | * `echo >> build.sbt`
41 | * Replace `Some(...)` with `None` if you don't have a flavor or build type to apply
42 | 4. Install the `sbt-android-protify` plugin, also from the project base, do:
43 | * `echo 'addSbtPlugin("org.scala-android" % "sbt-android-protify" % "1.4.4")' >> project/plugins.sbt`
44 | * For every application sub-project, do: `echo 'enablePlugins(AndroidProtify)' > APP-PROJECT-DIR/protify.sbt`
45 | 5. Launch SBT, `sbt` (first time's gonna take a while, downloading the internet and all)
46 | 5. Build and install the application normally, at least once:
47 | * `PROJECT-NAME/android:install` (or `run` instead of `install`) -- the first
48 | time will take a while too, since it will download the parts of the
49 | internet that your app requires
50 | 6. Thereafter: `PROJECT-NAME/protify`, do `~PROJECT-NAME/protify` to
51 | automatically trigger on all source changes
52 |
53 | ### Everyone else:
54 |
55 | 1. Install sbt from http://scala-sbt.org, homebrew, ports, or your
56 | package manager of choice
57 | 2. Install [sbt-android](https://github.com/scala-android/sbt-android):
58 | `echo 'addSbtPlugin("org.scala-android" % "sbt-android" % "1.7.7")' > ~/.sbt/0.13/plugins/android.sbt`
59 | 3. Start from an existing or new project (for trivial projects):
60 | `sbt "gen-android ..."` to create a new project, `sbt gen-android-sbt` to
61 | generate sbt files in an existing project. Non-trivial projects will need
62 | more advanced sbt configuration.
63 | * Alternatively, use `sbt-android-gradle` when working with an existing gradle project:
64 | * `echo 'addSbtPlugin("org.scala-android" % "sbt-android-gradle" % "1.3.2")' > project/plugins.sbt`
65 | 4. Add the protify plugin:
66 | `echo 'addSbtPlugin("org.scala-android" % "sbt-android-protify" % "1.4.4")' >> project/plugins.sbt`
67 | 5. Add `AndroidProtify`: `echo enablePlugins(AndroidProtify) >> build.sbt`
68 | 6. Run SBT
69 | 7. Select device to run on by using the `devices` and `device` commands. Run
70 | on all devices by executing `set allDevices in Android := true`
71 | 8. `android:run`, and `~protify`
72 | * Alternatively, high speed turnaround can be achieved with `protify:install`
73 | and `protify:run` to pretend the app is getting updated, rather than
74 | using the live-code mechanism.
75 | 9. Enjoy
76 |
77 | ### Full IntelliJ IDEA & Android Studio integration
78 |
79 | To make it so that protify can be run when triggered in the IDE with a keystroke,
80 | do the following
81 |
82 | 1. Install [Scala plugin](https://plugins.jetbrains.com/plugin/1347?pr=idea) in IDE
83 | 2. Install [SBT plugin](https://plugins.jetbrains.com/plugin/5007?pr=idea) in IDE
84 | 3. Create a new `Run` configuration, press the dropdown next to the play button,
85 | select `Edit Configurations`
86 | * Press `+` -> `Android Application`
87 | * Name the configuration `protify`, `instant run`, or whatever you like
88 | * Select the `app` module
89 | * Select `Do not deploy anything`
90 | * Select `Do not launch activity`
91 | * For target device, select anything that will not prompt
92 | * Uncheck `Activate tool window`
93 | * Remove `Make` from `Before launch`
94 | * `Before launch` -> `+` -> `Add New Configuration` -> `SBT`
95 | * In the drop down, edit the text to say `protify`
96 | 4. You can now invoke `protify` directly as a run configuration and see changes
97 | instantly (FSVO instant) appear on-device.
98 |
99 | ### Vim, etc.
100 |
101 | 1. Just do any of the above getting started steps and follow your own workflow.
102 |
103 | #### LIMITATIONS
104 | * Manifest changes will require `android:install` again (i.e.
105 | adding/removing: activities, services, permissions, receivers, etc).
106 | Incremental deployment cannot modify manifest.
107 | * No longer true with build-tools 24.0.0 and newer: Deleting a
108 | constant value from `R` classes (removing resources) will require
109 | running `protify:clean` or else the build will break
110 | * Object instance state, including singleton and static, will not be restored
111 | upon deploying new dex code (or resources when on device api level <14).
112 | All Android `Bundle`d state will be restored in all situations
113 | * Android instrumented tests are not supported. They will fail to run
114 | because of the sharded dex and re-located resource files.
115 | * NDK is not supported at the moment (initial install works, no `protify`
116 | updates when jni code changes)
117 |
118 | #### TODO (volunteers wanted)
119 | * Support NDK. This means delivering the `.so` libraries from the build
120 | environment to the device, placing them into the correct location and
121 | triggering an app restart
122 | * Support instrumented testing. This would need implementation similar to
123 | `MultiDexTestRunner`
124 |
--------------------------------------------------------------------------------
/agent/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/agent/src/main/java/com/hanhuy/android/protify/agent/Protify.java:
--------------------------------------------------------------------------------
1 | package com.hanhuy.android.protify.agent;
2 |
3 | import android.app.Application;
4 | import android.os.Build;
5 | import com.hanhuy.android.protify.agent.internal.*;
6 |
7 | /**
8 | * @author pfnguyen
9 | */
10 | public class Protify {
11 |
12 | static boolean installed;
13 |
14 | /**
15 | * Would be nice, but no, Protify cannot be installed inside of an Activity.
16 | * It must occur during Application.onCreate or Application.attachBaseContext
17 | *
18 | * This no longer needs to be called manually, unless one wants to build
19 | * with the IDE or gradle and not sbt.
20 | *
21 | * @deprecated 1.0.0: use automatic installation instead
22 | */
23 | @SuppressWarnings("unused")
24 | @Deprecated
25 | public static void install(Application app) {
26 | if (installed) return;
27 | installed = true;
28 | if (Build.VERSION.SDK_INT >= 14)
29 | app.registerActivityLifecycleCallbacks(LifecycleListener.getInstance());
30 | ProtifyApplication.installExternalResources(app);
31 | DexLoader.install(app);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/agent/src/main/java/com/hanhuy/android/protify/agent/ProtifyApplication.java:
--------------------------------------------------------------------------------
1 | package com.hanhuy.android.protify.agent;
2 |
3 | import android.annotation.TargetApi;
4 | import android.app.Application;
5 | import android.app.Notification;
6 | import android.app.NotificationManager;
7 | import android.app.PendingIntent;
8 | import android.content.Context;
9 | import android.content.ContextWrapper;
10 | import android.content.Intent;
11 | import android.content.pm.ApplicationInfo;
12 | import android.content.pm.PackageManager;
13 | import android.content.res.AssetManager;
14 | import android.content.res.Resources;
15 | import android.os.Build;
16 | import android.util.Log;
17 | import android.util.Pair;
18 | import com.hanhuy.android.protify.agent.internal.*;
19 |
20 | import java.io.ByteArrayOutputStream;
21 | import java.io.File;
22 | import java.io.IOException;
23 | import java.io.InputStream;
24 | import java.lang.ref.WeakReference;
25 | import java.lang.reflect.Constructor;
26 | import java.lang.reflect.Field;
27 | import java.lang.reflect.InvocationTargetException;
28 | import java.lang.reflect.Method;
29 | import java.util.Collection;
30 | import java.util.List;
31 | import java.util.Map;
32 |
33 | /**
34 | * some of this is straight up ripped off from bazelbuild's StubApplication
35 | * https://github.com/bazelbuild/bazel/blob/3eb0687fde3745cf52bbbb513f7769ecb9d004e4/src/tools/android/java/com/google/devtools/build/android/incrementaldeployment/StubApplication.java
36 | * @author pfnguyen
37 | */
38 | @SuppressWarnings("unused")
39 | public class ProtifyApplication extends Application {
40 | private final static String TAG = "ProtifyApplication";
41 | private final String realApplicationClass;
42 | private Application realApplication;
43 | private final static int NOTIFICATION_ID = 0x70726f74; // = "prot"
44 |
45 | public ProtifyApplication() {
46 | String[] applicationInfo = getResourceAsString("protify_application_info.txt").split("\n");
47 | realApplicationClass = applicationInfo[0].trim();
48 | Log.d(TAG, "Real application class: [" + realApplicationClass + "]");
49 | Protify.installed = true;
50 | }
51 |
52 | @SuppressWarnings("deprecation")
53 | private static Notification loadingNotification(Context c, String text) {
54 | final Notification n;
55 | // R is filtered out of DEX, find the resource manually
56 | int icon = c.getResources().getIdentifier(
57 | "protify_internal_ic_notification_loading", "drawable", c.getPackageName());
58 | if (icon == 0)
59 | throw new IllegalStateException(
60 | "protify_internal_ic_notification_loading not found");
61 | if (Build.VERSION.SDK_INT >= 14) {
62 | final Notification.Builder nb = new Notification.Builder(c);
63 | nb
64 | .setContentTitle(text)
65 | .setSmallIcon(icon)
66 | .setProgress(100, 0, true)
67 | .setOngoing(true);
68 | n = nb.getNotification();
69 | } else {
70 | n = new Notification();
71 | n.icon = icon;
72 | n.flags = Notification.FLAG_ONGOING_EVENT;
73 | n.tickerText = text;
74 | n.setLatestEventInfo(c, text, null,
75 | PendingIntent.getBroadcast(c, 0, new Intent(
76 | "com.hanhuy.android.protify.internal.action.NOOP"),
77 | 0));
78 | }
79 | n.when = System.currentTimeMillis();
80 | return n;
81 | }
82 |
83 | private Object stashedContentProviders;
84 | private static Field getField(Object instance, String fieldName)
85 | throws ClassNotFoundException {
86 | for (Class> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
87 | try {
88 | Field field = clazz.getDeclaredField(fieldName);
89 | field.setAccessible(true);
90 | return field;
91 | } catch (NoSuchFieldException e) {
92 | // IllegalStateException will be thrown below
93 | }
94 | }
95 |
96 | throw new IllegalStateException("Field '" + fieldName + "' not found");
97 | }
98 |
99 | private void enableContentProviders() {
100 | Log.v(TAG, "enableContentProviders");
101 | try {
102 | Class> activityThread = Class.forName("android.app.ActivityThread");
103 | Method mCurrentActivityThread = activityThread.getMethod("currentActivityThread");
104 | mCurrentActivityThread.setAccessible(true);
105 | Object currentActivityThread = mCurrentActivityThread.invoke(null);
106 | Object boundApplication = getField(
107 | currentActivityThread, "mBoundApplication").get(currentActivityThread);
108 | getField(boundApplication, "providers").set(boundApplication, stashedContentProviders);
109 | if (stashedContentProviders != null) {
110 | Method mInstallContentProviders = activityThread.getDeclaredMethod(
111 | "installContentProviders", Context.class, List.class);
112 | mInstallContentProviders.setAccessible(true);
113 | mInstallContentProviders.invoke(
114 | currentActivityThread, realApplication, stashedContentProviders);
115 | stashedContentProviders = null;
116 | }
117 | } catch (Exception e) {
118 | if (stashedContentProviders != null) {
119 | Log.e(TAG, "ContentProviders stashed, but unable to restore");
120 | throw new IllegalStateException(e);
121 | }
122 | }
123 | }
124 | private void disableContentProviders() {
125 | Log.v(TAG, "disableContentProviders");
126 | try {
127 | Class> activityThread = Class.forName("android.app.ActivityThread");
128 | Method mCurrentActivityThread = activityThread.getMethod("currentActivityThread");
129 | mCurrentActivityThread.setAccessible(true);
130 | Object currentActivityThread = mCurrentActivityThread.invoke(null);
131 | Object boundApplication = getField(
132 | currentActivityThread, "mBoundApplication").get(currentActivityThread);
133 | Field fProviders = getField(boundApplication, "providers");
134 |
135 | stashedContentProviders = fProviders.get(boundApplication);
136 | fProviders.set(boundApplication, null);
137 | } catch (Exception e) {
138 | Log.e(TAG, "Unable to inject Application for ContentProviders");
139 | }
140 | }
141 |
142 | @Override
143 | protected void attachBaseContext(Context base) {
144 | NotificationManager nm = (NotificationManager) base.getSystemService(
145 | NOTIFICATION_SERVICE);
146 | CharSequence name;
147 | try {
148 | name = base.getResources().getText(base.getApplicationInfo().labelRes);
149 | } catch (Resources.NotFoundException e) {
150 | name = base.getPackageName();
151 | }
152 | nm.notify(NOTIFICATION_ID, loadingNotification(
153 | base, "Protifying DEX for " + name));
154 | DexLoader.install(base);
155 | nm.cancel(NOTIFICATION_ID);
156 |
157 | createRealApplication();
158 | super.attachBaseContext(base);
159 |
160 | try {
161 | Method attachBaseContext = ContextWrapper.class.getDeclaredMethod(
162 | "attachBaseContext", Context.class);
163 | attachBaseContext.setAccessible(true);
164 | attachBaseContext.invoke(realApplication, base);
165 |
166 | disableContentProviders();
167 | } catch (Exception e) {
168 | throw new IllegalStateException(e);
169 | }
170 |
171 | if (Build.VERSION.SDK_INT >= 14) {
172 | realApplication.registerActivityLifecycleCallbacks(
173 | LifecycleListener.getInstance());
174 | }
175 | }
176 |
177 | @Override
178 | public void onCreate() {
179 | installRealApplication();
180 | installExternalResources(this);
181 | enableContentProviders();
182 | super.onCreate();
183 | realApplication.onCreate();
184 | }
185 |
186 | @SuppressWarnings("unchecked")
187 | private void installRealApplication() {
188 | // StubApplication is created by reflection in Application#handleBindApplication() ->
189 | // LoadedApk#makeApplication(), and its return value is used to set the Application field in all
190 | // sorts of Android internals.
191 | //
192 | // Fortunately, Application#onCreate() is called quite soon after, so what we do is monkey
193 | // patch in the real Application instance in StubApplication#onCreate().
194 | //
195 | // A few places directly use the created Application instance (as opposed to the fields it is
196 | // eventually stored in). Fortunately, it's easy to forward those to the actual real
197 | // Application class.
198 | try {
199 | // Find the ActivityThread instance for the current thread
200 | Class> activityThread = Class.forName("android.app.ActivityThread");
201 | Method m = activityThread.getMethod("currentActivityThread");
202 | m.setAccessible(true);
203 | Object currentActivityThread = m.invoke(null);
204 |
205 | // Find the mInitialApplication field of the ActivityThread to the real application
206 | Field mInitialApplication = activityThread.getDeclaredField("mInitialApplication");
207 | mInitialApplication.setAccessible(true);
208 | Application initialApplication = (Application) mInitialApplication.get(currentActivityThread);
209 | if (initialApplication == this) {
210 | mInitialApplication.set(currentActivityThread, realApplication);
211 | }
212 |
213 | // Replace all instance of the stub application in ActivityThread#mAllApplications with the
214 | // real one
215 | Field mAllApplications = activityThread.getDeclaredField("mAllApplications");
216 | mAllApplications.setAccessible(true);
217 | List allApplications = (List) mAllApplications
218 | .get(currentActivityThread);
219 | for (int i = 0; i < allApplications.size(); i++) {
220 | if (allApplications.get(i) == this) {
221 | allApplications.set(i, realApplication);
222 | }
223 | }
224 |
225 | // Figure out how loaded APKs are stored.
226 |
227 | // API version 8 has PackageInfo, 10 has LoadedApk. 9, I don't know.
228 | Class> loadedApkClass;
229 | try {
230 | loadedApkClass = Class.forName("android.app.LoadedApk");
231 | } catch (ClassNotFoundException e) {
232 | loadedApkClass = Class.forName("android.app.ActivityThread$PackageInfo");
233 | }
234 | Field mApplication = loadedApkClass.getDeclaredField("mApplication");
235 | mApplication.setAccessible(true);
236 | Field mResDir = loadedApkClass.getDeclaredField("mResDir");
237 | mResDir.setAccessible(true);
238 |
239 | // 10 doesn't have this field, 14 does. Fortunately, there are not many Honeycomb devices
240 | // floating around.
241 | Field mLoadedApk = null;
242 | try {
243 | mLoadedApk = Application.class.getDeclaredField("mLoadedApk");
244 | } catch (NoSuchFieldException e) {
245 | // According to testing, it's okay to ignore this.
246 | }
247 |
248 | // Enumerate all LoadedApk (or PackageInfo) fields in ActivityThread#mPackages and
249 | // ActivityThread#mResourcePackages and do two things:
250 | // - Replace the Application instance in its mApplication field with the real one
251 | // - Replace mResDir to point to the external resource file instead of the .apk. This is
252 | // used as the asset path for new Resources objects.
253 | // - Set Application#mLoadedApk to the found LoadedApk instance
254 | for (String fieldName : new String[] { "mPackages", "mResourcePackages" }) {
255 | Field field = activityThread.getDeclaredField(fieldName);
256 | field.setAccessible(true);
257 | Object value = field.get(currentActivityThread);
258 |
259 | for (Map.Entry> entry :
260 | ((Map>) value).entrySet()) {
261 | Object loadedApk = entry.getValue().get();
262 | if (loadedApk == null) {
263 | continue;
264 | }
265 |
266 | if (mApplication.get(loadedApk) == this) {
267 | File externalResourceFile = ProtifyResources.getResourcesFile(this);
268 | mApplication.set(loadedApk, realApplication);
269 | ApplicationInfo info = getApplicationInfo();
270 | if (info != null && new File(info.sourceDir).lastModified() > externalResourceFile.lastModified()) {
271 | Log.v(TAG, "Deleting out of date external resources");
272 | externalResourceFile.delete();
273 | }
274 | if (externalResourceFile.isFile()) {
275 | Log.v(TAG, "Setting mResDir to: " + externalResourceFile.getAbsolutePath());
276 | mResDir.set(loadedApk, externalResourceFile.getAbsolutePath());
277 | }
278 |
279 | if (mLoadedApk != null) {
280 | mLoadedApk.set(realApplication, loadedApk);
281 | }
282 | }
283 | }
284 | }
285 | } catch (Exception e) {
286 | throw new IllegalStateException(e);
287 | }
288 | }
289 |
290 | public static void installExternalResources(Context context) {
291 | File f = ProtifyResources.getResourcesFile(context);
292 | ApplicationInfo info = context.getApplicationInfo();
293 | if (info != null && new File(info.sourceDir).lastModified() > f.lastModified()) {
294 | Log.v(TAG, "Deleting outdated external resources");
295 | f.delete();
296 | }
297 | if (f.isFile() && f.length() > 0) {
298 | Log.v(TAG, "Installing external resource file: " + f);
299 | if (Build.VERSION.SDK_INT >= 24)
300 | V24Resources.install(f.getAbsolutePath());
301 | else if (Build.VERSION.SDK_INT >= 18)
302 | V19Resources.install(f.getAbsolutePath());
303 | else
304 | V4Resources.install(f.getAbsolutePath());
305 | resourceInstallTime = System.currentTimeMillis();
306 | }
307 | }
308 |
309 | private static long resourceInstallTime = System.currentTimeMillis();
310 |
311 | public static long getResourceInstallTime() {
312 | return resourceInstallTime;
313 | }
314 |
315 | private String getResourceAsString(String resource) {
316 | InputStream resourceStream = null;
317 | // try-with-resources would be much nicer, but that requires SDK level 19, and we want this code
318 | // to be compatible with earlier Android versions
319 | try {
320 | resourceStream = getClass().getClassLoader().getResourceAsStream(resource);
321 | ByteArrayOutputStream baos = new ByteArrayOutputStream();
322 | byte[] buffer = new byte[1024];
323 | int length;
324 | while ((length = resourceStream.read(buffer)) != -1) {
325 | baos.write(buffer, 0, length);
326 | }
327 |
328 | return new String(baos.toByteArray(), "UTF-8");
329 | } catch (IOException e) {
330 | throw new IllegalStateException(e);
331 | } finally {
332 | if (resourceStream != null) {
333 | try {
334 | resourceStream.close();
335 | } catch (IOException e) {
336 | // Not much we can do here
337 | }
338 | }
339 | }
340 | }
341 |
342 | private void createRealApplication() {
343 | try {
344 | @SuppressWarnings("unchecked")
345 | Class extends Application> realClass =
346 | (Class extends Application>) Class.forName(realApplicationClass);
347 | Constructor extends Application> ctor = realClass.getConstructor();
348 | realApplication = ctor.newInstance();
349 | } catch (Exception e) {
350 | throw new IllegalStateException(e);
351 | }
352 | }
353 |
354 | private static class V24Resources {
355 | @TargetApi(24)
356 | static void install(String externalResourceFile) {
357 | try {
358 | AssetManager newAssetManager = createAssetManager(externalResourceFile);
359 |
360 | // Find the singleton instance of ResourcesManager
361 | Class> clazz = Class.forName("android.app.ResourcesManager");
362 | Method mGetInstance = clazz.getDeclaredMethod("getInstance");
363 | mGetInstance.setAccessible(true);
364 | Object resourcesManager = mGetInstance.invoke(null);
365 |
366 | // Iterate over all known Resources objects
367 | Field mResourceReferences = clazz.getDeclaredField("mResourceReferences");
368 | mResourceReferences.setAccessible(true);
369 | @SuppressWarnings("unchecked")
370 | Collection> references =
371 | (Collection>) mResourceReferences.get(resourcesManager);
372 |
373 | setAssetManager(references, newAssetManager);
374 | } catch (IllegalAccessException | NoSuchFieldException | NoSuchMethodException |
375 | ClassNotFoundException | InvocationTargetException | InstantiationException e) {
376 | throw new IllegalStateException(e);
377 | }
378 | }
379 | }
380 | private static class V19Resources {
381 | @TargetApi(19)
382 | static void install(String externalResourceFile) {
383 | try {
384 | AssetManager newAssetManager = createAssetManager(externalResourceFile);
385 |
386 | // Find the singleton instance of ResourcesManager
387 | Class> clazz = Class.forName("android.app.ResourcesManager");
388 | Method mGetInstance = clazz.getDeclaredMethod("getInstance");
389 | mGetInstance.setAccessible(true);
390 | Object resourcesManager = mGetInstance.invoke(null);
391 |
392 | // Iterate over all known Resources objects
393 | Field fMActiveResources = clazz.getDeclaredField("mActiveResources");
394 | fMActiveResources.setAccessible(true);
395 | @SuppressWarnings("unchecked")
396 | Map, WeakReference> arrayMap =
397 | (Map, WeakReference>) fMActiveResources.get(resourcesManager);
398 | setAssetManager(arrayMap, newAssetManager);
399 | } catch (IllegalAccessException | NoSuchFieldException | NoSuchMethodException |
400 | ClassNotFoundException | InvocationTargetException | InstantiationException e) {
401 | throw new IllegalStateException(e);
402 | }
403 | }
404 | }
405 | private static class V4Resources {
406 | static void install(String externalResourceFile) {
407 | try {
408 | AssetManager newAssetManager = createAssetManager(externalResourceFile);
409 |
410 | // Find the singleton instance of ResourcesManager
411 | Class> clazz = Class.forName("android.app.ActivityThread");
412 | Method mGetInstance = clazz.getDeclaredMethod("currentActivityThread");
413 | mGetInstance.setAccessible(true);
414 | Object resourcesManager = mGetInstance.invoke(null);
415 |
416 | // Iterate over all known Resources objects
417 | Field fMActiveResources = clazz.getDeclaredField("mActiveResources");
418 | fMActiveResources.setAccessible(true);
419 | @SuppressWarnings("unchecked")
420 | Map, WeakReference> arrayMap =
421 | (Map, WeakReference>) fMActiveResources.get(resourcesManager);
422 | setAssetManager(arrayMap, newAssetManager);
423 | } catch (Exception e) {
424 | throw new IllegalStateException(e);
425 | }
426 | }
427 |
428 | }
429 |
430 | private static AssetManager createAssetManager(String externalResourceFile)
431 | throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, InstantiationException {
432 | // Create a new AssetManager instance and point it to the resources installed under
433 | // /sdcard
434 | AssetManager newAssetManager = AssetManager.class.getConstructor().newInstance();
435 | Method mAddAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
436 | mAddAssetPath.setAccessible(true);
437 | if (((int) mAddAssetPath.invoke(newAssetManager, externalResourceFile)) == 0) {
438 | throw new IllegalStateException("Could not create new AssetManager");
439 | }
440 |
441 | // Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
442 | // in L, so we do it unconditionally.
443 | Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks");
444 | mEnsureStringBlocks.setAccessible(true);
445 | mEnsureStringBlocks.invoke(newAssetManager);
446 | return newAssetManager;
447 | }
448 |
449 | private static void setAssetManager(Map,WeakReference> arrayMap, AssetManager newAssetManager) throws IllegalAccessException, NoSuchFieldException {
450 | setAssetManager(arrayMap.values(), newAssetManager);
451 | }
452 |
453 | private static void setAssetManager(Collection> ress, AssetManager newAssetManager) throws IllegalAccessException, NoSuchFieldException {
454 | for (WeakReference wr : ress) {
455 | Resources resources = wr.get();
456 | // Set the AssetManager of the Resources instance to our brand new one
457 | if (resources != null) {
458 | setAssetsField(resources, newAssetManager);
459 | resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
460 | }
461 | }
462 | }
463 |
464 | private static void setAssetsField(Resources resources, AssetManager newAssetManager) {
465 | Field mAssets;
466 | try {
467 | if (Build.VERSION.SDK_INT >= 24) {
468 | Field mResourcesImplField;
469 | mResourcesImplField = Resources.class.getDeclaredField("mResourcesImpl");
470 | mResourcesImplField.setAccessible(true);
471 | Object mResourceImpl = mResourcesImplField.get(resources);
472 | mAssets = mResourceImpl.getClass().getDeclaredField("mAssets");
473 | mAssets.setAccessible(true);
474 | mAssets.set(mResourceImpl, newAssetManager);
475 | } else {
476 | mAssets = Resources.class.getDeclaredField("mAssets");
477 | mAssets.setAccessible(true);
478 | mAssets.set(resources, newAssetManager);
479 | }
480 | } catch (Exception e) {
481 | throw new IllegalStateException(e);
482 | }
483 | }
484 | }
485 |
--------------------------------------------------------------------------------
/agent/src/main/java/com/hanhuy/android/protify/agent/internal/DexExtractor.java:
--------------------------------------------------------------------------------
1 | package com.hanhuy.android.protify.agent.internal;
2 |
3 | import android.content.Context;
4 | import android.content.SharedPreferences;
5 | import android.content.pm.ApplicationInfo;
6 | import android.os.Build;
7 | import android.util.Log;
8 |
9 | import java.io.*;
10 | import java.lang.reflect.InvocationTargetException;
11 | import java.lang.reflect.Method;
12 | import java.nio.channels.FileChannel;
13 | import java.nio.channels.FileLock;
14 | import java.util.*;
15 | import java.util.zip.ZipEntry;
16 | import java.util.zip.ZipFile;
17 | import java.util.zip.ZipOutputStream;
18 |
19 | /**
20 | * @author pfnguyen
21 | */
22 | public class DexExtractor {
23 | private static final String TAG = DexLoader.TAG;
24 |
25 | private static final String PROTIFY_DEX_PREFIX = "protify-dex/";
26 | private static final String DEX_SUFFIX = ".dex";
27 |
28 | static final String ZIP_SUFFIX = ".zip";
29 | private static final int MAX_EXTRACT_ATTEMPTS = 3;
30 |
31 | private static final String PREFS_FILE = "protify.version";
32 | private static final String KEY_TIME_STAMP = "timestamp";
33 | private static final String KEY_CRC = "crc";
34 | private static final String LOCK_FILE = "protify.extraction.lock";
35 |
36 | /**
37 | * Size of reading buffers.
38 | */
39 | private static final int BUFFER_SIZE = 0x4000;
40 | /* Keep value away from 0 because it is a too probable time stamp value */
41 | private static final long NO_VALUE = -1L;
42 |
43 | /**
44 | * Extracts application secondary dexes into files in the application data
45 | * directory.
46 | *
47 | * @return a list of files that were created. The list may be empty if there
48 | * are no secondary dex files.
49 | * @throws IOException if encounters a problem while reading or writing
50 | * secondary dex files
51 | */
52 | static List load(Context context, ApplicationInfo applicationInfo, File dexDir) throws IOException {
53 | return load(context, applicationInfo, dexDir, false);
54 | }
55 | static List load(final Context context, final ApplicationInfo applicationInfo, final File dexDir, final boolean force) throws IOException {
56 | Log.i(TAG, "DexExtractor.load(" + applicationInfo.sourceDir + ")");
57 | final File sourceApk = new File(applicationInfo.sourceDir);
58 |
59 | return withBlockingLock(dexDir, sourceApk, new RunnableIO>() {
60 | @Override
61 | public List run(boolean dirty) throws IOException {
62 | List files;
63 | if (!force && !dirty) {
64 | files = loadExistingExtractions(dexDir);
65 | if (files.isEmpty())
66 | files = performExtractions(sourceApk, dexDir);
67 | } else {
68 | Log.i(TAG, "Detected that extraction must be performed.");
69 | files = performExtractions(sourceApk, dexDir);
70 | }
71 |
72 | Log.i(TAG, "load found " + files.size() + " secondary dex files");
73 | return files;
74 | }
75 | });
76 | }
77 |
78 | interface RunnableIO {
79 | T run(boolean dirty) throws IOException;
80 | }
81 |
82 | private static T withBlockingLock(File dexDir, File sourceApk, RunnableIO r) throws IOException {
83 | final long currentCrc = getZipCrc(sourceApk);
84 | File lockFile = new File(dexDir, LOCK_FILE);
85 | RandomAccessFile lockRaf = new RandomAccessFile(lockFile, "rw");
86 | FileChannel lockChannel = null;
87 | FileLock cacheLock = null;
88 | try {
89 | lockChannel = lockRaf.getChannel();
90 | Log.v(TAG, "Waiting for lock on: " + lockFile.getAbsolutePath());
91 | cacheLock = lockChannel.lock();
92 | Log.v(TAG, "Locked " + lockFile.getPath());
93 | long l = lockRaf.length() >= (Long.SIZE / 8) ? lockRaf.readLong() : 0;
94 | long c = lockRaf.length() >= 2 * (Long.SIZE / 8) ? lockRaf.readLong() : 0;
95 | return r.run(c != currentCrc || l != getTimeStamp(sourceApk));
96 | } finally {
97 | lockRaf.seek(0);
98 | lockRaf.writeLong(getTimeStamp(sourceApk));
99 | lockRaf.writeLong(currentCrc);
100 | if (cacheLock != null)
101 | cacheLock.release();
102 | if (lockChannel != null)
103 | lockChannel.close();
104 | lockRaf.close();
105 | Log.v(TAG, "Unlocked " + lockFile.getPath());
106 | }
107 | }
108 |
109 | private static List loadExistingExtractions(File dexDir) {
110 | Log.i(TAG, "loading existing secondary dex files");
111 |
112 | File[] files = dexDir.listFiles(new FilenameFilter() {
113 | @Override
114 | public boolean accept(File file, String s) {
115 | return Build.VERSION.SDK_INT >= 14 ?
116 | s.endsWith(DEX_SUFFIX) : s.endsWith(ZIP_SUFFIX);
117 | }
118 | });
119 | Arrays.sort(files, new Comparator() {
120 | @Override
121 | public int compare(File f1, File f2) {
122 | return (int) (f2.lastModified() - f1.lastModified());
123 | }
124 | });
125 |
126 | if (files == null) files = new File[0];
127 |
128 | return Arrays.asList(files);
129 | }
130 |
131 | private static long getTimeStamp(File archive) {
132 | long timeStamp = archive.lastModified();
133 | if (timeStamp == NO_VALUE) {
134 | // never return NO_VALUE
135 | timeStamp--;
136 | }
137 | return timeStamp;
138 | }
139 |
140 |
141 | private static long getZipCrc(File archive) throws IOException {
142 | long computedValue = ZipUtil.getZipCrc(archive);
143 | if (computedValue == NO_VALUE) {
144 | // never return NO_VALUE
145 | computedValue--;
146 | }
147 | return computedValue;
148 | }
149 |
150 | private static List performExtractions(File sourceApk, File dexDir)
151 | throws IOException {
152 |
153 | // Ensure that whatever deletions happen in prepareDexDir only happen if the zip that
154 | // contains a secondary dex file in there is not consistent with the latest apk. Otherwise,
155 | // multi-process race conditions can cause a crash loop where one process deletes the zip
156 | // while another had created it.
157 | prepareDexDir(dexDir);
158 |
159 | List files = new ArrayList();
160 |
161 | final ZipFile apk = new ZipFile(sourceApk);
162 | try {
163 | for (Enumeration extends ZipEntry> e = apk.entries(); e.hasMoreElements();) {
164 | ZipEntry entry = e.nextElement();
165 | String name = entry.getName();
166 | if (name.startsWith(PROTIFY_DEX_PREFIX) && name.endsWith(DEX_SUFFIX)) {
167 | String fname = name.substring(name.lastIndexOf("/") + 1);
168 | File extractedFile = new File(dexDir,
169 | Build.VERSION.SDK_INT < 14 ? fname + ZIP_SUFFIX : fname);
170 | if (Build.VERSION.SDK_INT < 14) {
171 | extractV4(apk, entry, extractedFile, "protify-extraction");
172 | } else
173 | extractV14(apk, entry, extractedFile, "protify-extraction");
174 | files.add(extractedFile);
175 | }
176 | }
177 | } finally {
178 | try {
179 | apk.close();
180 | } catch (IOException e) {
181 | Log.w(TAG, "Failed to close resource", e);
182 | }
183 | }
184 |
185 | return files;
186 | }
187 |
188 | // TODO use FileObserver to lock on this directory if necessary
189 | private static void prepareDexDir(File dexDir) {
190 | File[] files = dexDir.listFiles();
191 | if (files == null) {
192 | Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
193 | return;
194 | }
195 | for (File oldFile : files) {
196 | if (!LOCK_FILE.equals(oldFile.getName())) {
197 | if (!oldFile.delete()) {
198 | Log.w(TAG, "Failed to delete old file " + oldFile.getPath());
199 | }
200 | }
201 | }
202 | }
203 |
204 | private static void extractV14(ZipFile apk, ZipEntry dexFile, File extractTo,
205 | String extractedFilePrefix) throws IOException {
206 |
207 | InputStream in = apk.getInputStream(dexFile);
208 | File tmp = File.createTempFile(extractedFilePrefix, DEX_SUFFIX,
209 | extractTo.getParentFile());
210 | try {
211 | OutputStream out = new BufferedOutputStream(new FileOutputStream(tmp));
212 | try {
213 | byte[] buffer = new byte[BUFFER_SIZE];
214 | int length = in.read(buffer);
215 | while (length != -1) {
216 | out.write(buffer, 0, length);
217 | length = in.read(buffer);
218 | }
219 | } finally {
220 | out.close();
221 | }
222 | if (!tmp.renameTo(extractTo)) {
223 | throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() +
224 | "\" to \"" + extractTo.getAbsolutePath() + "\"");
225 | }
226 | Log.i(TAG, "Extracted " + extractTo.getPath());
227 | } finally {
228 | closeQuietly(in);
229 | tmp.delete(); // return status ignored
230 | }
231 | if (!extractTo.isFile()) {
232 | throw new IOException("Failed to extract to: " + extractTo);
233 | }
234 | }
235 | private static void extractV4(ZipFile apk, ZipEntry dexFile, File extractTo,
236 | String extractedFilePrefix) throws IOException, FileNotFoundException {
237 |
238 | InputStream in = apk.getInputStream(dexFile);
239 | ZipOutputStream out = null;
240 | File tmp = File.createTempFile(extractedFilePrefix, ZIP_SUFFIX,
241 | extractTo.getParentFile());
242 | Log.i(TAG, "Extracting " + tmp.getPath());
243 | try {
244 | out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp)));
245 | try {
246 | ZipEntry classesDex = new ZipEntry("classes.dex");
247 | // keep zip entry time since it is the criteria used by Dalvik
248 | classesDex.setTime(dexFile.getTime());
249 | out.putNextEntry(classesDex);
250 |
251 | byte[] buffer = new byte[BUFFER_SIZE];
252 | int length = in.read(buffer);
253 | while (length != -1) {
254 | out.write(buffer, 0, length);
255 | length = in.read(buffer);
256 | }
257 | out.closeEntry();
258 | } finally {
259 | out.close();
260 | }
261 | Log.i(TAG, "Renaming to " + extractTo.getPath());
262 | if (!tmp.renameTo(extractTo)) {
263 | throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() +
264 | "\" to \"" + extractTo.getAbsolutePath() + "\"");
265 | }
266 | } finally {
267 | closeQuietly(in);
268 | tmp.delete(); // return status ignored
269 | }
270 | }
271 |
272 |
273 | /**
274 | * Closes the given {@code Closeable}. Suppresses any IO exceptions.
275 | */
276 | private static void closeQuietly(Closeable closeable) {
277 | try {
278 | closeable.close();
279 | } catch (IOException e) {
280 | Log.w(TAG, "Failed to close resource", e);
281 | }
282 | }
283 | }
284 |
285 |
--------------------------------------------------------------------------------
/agent/src/main/java/com/hanhuy/android/protify/agent/internal/DexLoader.java:
--------------------------------------------------------------------------------
1 | package com.hanhuy.android.protify.agent.internal;
2 |
3 | import android.content.Context;
4 | import android.content.pm.ApplicationInfo;
5 | import android.content.pm.PackageManager;
6 | import android.os.Build;
7 | import android.util.Log;
8 | import dalvik.system.DexFile;
9 |
10 | import java.io.File;
11 | import java.io.IOException;
12 | import java.lang.reflect.Array;
13 | import java.lang.reflect.Field;
14 | import java.lang.reflect.InvocationTargetException;
15 | import java.lang.reflect.Method;
16 | import java.util.*;
17 | import java.util.zip.ZipFile;
18 |
19 | /**
20 | * @author pfnguyen
21 | */
22 | public class DexLoader {
23 | final static String TAG = "ProtifyDexLoader";
24 | private static final int MIN_SDK_VERSION = 4;
25 | private static final String PROTIFY_DEX_FOLDER_NAME = "protify-dexes";
26 | private static final String CODE_CACHE_NAME = "protify-code-cache";
27 | private static final String CODE_CACHE_PROTIFY_FOLDER_NAME = PROTIFY_DEX_FOLDER_NAME;
28 |
29 | public static void install(Context context) {
30 | Log.i(TAG, "install");
31 | long start = System.currentTimeMillis();
32 |
33 | if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {
34 | throw new RuntimeException("Multi dex installation failed. SDK " + Build.VERSION.SDK_INT
35 | + " is unsupported. Min SDK version is " + MIN_SDK_VERSION + ".");
36 | }
37 |
38 | try {
39 | ApplicationInfo applicationInfo = getApplicationInfo(context);
40 | if (applicationInfo == null) {
41 | // Looks like running on a test Context, so just return without patching.
42 | return;
43 | }
44 |
45 | /* The patched class loader is expected to be a descendant of
46 | * dalvik.system.BaseDexClassLoader. We modify its
47 | * dalvik.system.DexPathList pathList field to append additional DEX
48 | * file entries.
49 | */
50 | ClassLoader loader;
51 | try {
52 | loader = context.getClassLoader();
53 | } catch (RuntimeException e) {
54 | /* Ignore those exceptions so that we don't break tests relying on Context like
55 | * a android.test.mock.MockContext or a android.content.ContextWrapper with a
56 | * null base Context.
57 | */
58 | Log.w(TAG, "Failure while trying to obtain Context class loader. " +
59 | "Must be running in test mode. Skip patching.", e);
60 | return;
61 | }
62 | if (loader == null) {
63 | // Note, the context class loader is null when running Robolectric tests.
64 | Log.e(TAG,
65 | "Context class loader is null. Must be running in test mode. "
66 | + "Skip patching.");
67 | return;
68 | }
69 |
70 | File dexDir = getDexDir(context, applicationInfo);
71 | File extractDir = getDexExtractionDir(context);
72 | List dexes = DexExtractor.load(context, applicationInfo, extractDir);
73 | // nothing to install if not present
74 | if (!dexes.isEmpty()) {
75 | Log.v(TAG, "Loading secondary dexes");
76 | installSecondaryDexes(loader, dexDir, dexes);
77 | }
78 |
79 | } catch (Exception e) {
80 | Log.e(TAG, "Protify classloader installation failure", e);
81 | throw new RuntimeException("Protify classloader installation failed (" + e.getMessage() + ").", e);
82 | }
83 | long elapsed = System.currentTimeMillis() - start;
84 | Log.i(TAG, "install done " + elapsed + "ms");
85 | }
86 |
87 | public static File getDexExtractionDir(Context context)
88 | throws IOException, PackageManager.NameNotFoundException {
89 | ApplicationInfo applicationInfo = getApplicationInfo(context);
90 | File cache = new File(applicationInfo.dataDir, PROTIFY_DEX_FOLDER_NAME);
91 | try {
92 | mkdirChecked(cache);
93 | } catch (IOException e) {
94 | /* If we can't emulate code_cache, then store to filesDir. This means abandoning useless
95 | * files on disk if the device ever updates to android 5+. But since this seems to
96 | * happen only on some devices running android 2, this should cause no pollution.
97 | */
98 | cache = new File(context.getFilesDir(), PROTIFY_DEX_FOLDER_NAME);
99 | mkdirChecked(cache);
100 | }
101 | return cache;
102 | }
103 |
104 | // slightly modified from MultiDex.java, prepend instead of append
105 | /**
106 | * Replace the value of a field containing a non null array, by a new array containing the
107 | * elements of the original array plus the elements of extraElements.
108 | * @param instance the instance whose field is to be modified.
109 | * @param fieldName the field to modify.
110 | * @param extraElements elements to prepend to the array.
111 | */
112 | private static void expandFieldArray(Object instance, String fieldName,
113 | Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException,
114 | IllegalAccessException {
115 | Field jlrField = findField(instance, fieldName);
116 | Object[] original = (Object[]) jlrField.get(instance);
117 | Object[] combined = (Object[]) Array.newInstance(
118 | original.getClass().getComponentType(), original.length + extraElements.length);
119 | System.arraycopy(extraElements, 0, combined, 0 , extraElements.length);
120 | System.arraycopy(original, 0, combined, extraElements.length, original.length);
121 | jlrField.set(instance, combined);
122 | }
123 |
124 | // everything below copied implementation from support-multidex's MultiDex.java
125 |
126 | private static File getDexDir(Context context, ApplicationInfo applicationInfo)
127 | throws IOException {
128 | File cache = new File(applicationInfo.dataDir, CODE_CACHE_NAME);
129 | try {
130 | mkdirChecked(cache);
131 | } catch (IOException e) {
132 | /* If we can't emulate code_cache, then store to filesDir. This means abandoning useless
133 | * files on disk if the device ever updates to android 5+. But since this seems to
134 | * happen only on some devices running android 2, this should cause no pollution.
135 | */
136 | cache = new File(context.getFilesDir(), CODE_CACHE_NAME);
137 | mkdirChecked(cache);
138 | }
139 | File dexDir = new File(cache, CODE_CACHE_PROTIFY_FOLDER_NAME);
140 | mkdirChecked(dexDir);
141 | return dexDir;
142 | }
143 |
144 | private static void mkdirChecked(File dir) throws IOException {
145 | dir.mkdir();
146 | if (!dir.isDirectory()) {
147 | File parent = dir.getParentFile();
148 | if (parent == null) {
149 | Log.e(TAG, "Failed to create dir " + dir.getPath() + ". Parent file is null.");
150 | } else {
151 | Log.e(TAG, "Failed to create dir " + dir.getPath() +
152 | ". parent file is a dir " + parent.isDirectory() +
153 | ", a file " + parent.isFile() +
154 | ", exists " + parent.exists() +
155 | ", readable " + parent.canRead() +
156 | ", writable " + parent.canWrite());
157 | }
158 | throw new IOException("Failed to create directory " + dir.getPath());
159 | }
160 | }
161 |
162 | private static ApplicationInfo getApplicationInfo(Context context)
163 | throws PackageManager.NameNotFoundException {
164 | PackageManager pm;
165 | String packageName;
166 | try {
167 | pm = context.getPackageManager();
168 | packageName = context.getPackageName();
169 | } catch (RuntimeException e) {
170 | /* Ignore those exceptions so that we don't break tests relying on Context like
171 | * a android.test.mock.MockContext or a android.content.ContextWrapper with a null
172 | * base Context.
173 | */
174 | Log.w(TAG, "Failure while trying to obtain ApplicationInfo from Context. " +
175 | "Must be running in test mode. Skip patching.", e);
176 | return null;
177 | }
178 | if (pm == null || packageName == null) {
179 | // This is most likely a mock context, so just return without patching.
180 | return null;
181 | }
182 | ApplicationInfo applicationInfo =
183 | pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA);
184 | return applicationInfo;
185 | }
186 |
187 | private static void installSecondaryDexes(ClassLoader loader, File dexDir, List files)
188 | throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
189 | InvocationTargetException, NoSuchMethodException, IOException {
190 | if (!files.isEmpty()) {
191 | if (Build.VERSION.SDK_INT >= 19) {
192 | V19.install(loader, files, dexDir);
193 | } else if (Build.VERSION.SDK_INT >= 14) {
194 | V14.install(loader, files, dexDir);
195 | } else {
196 | V4.install(loader, files);
197 | }
198 | }
199 | }
200 |
201 | /**
202 | * Locates a given field anywhere in the class inheritance hierarchy.
203 | *
204 | * @param instance an object to search the field into.
205 | * @param name field name
206 | * @return a field object
207 | * @throws NoSuchFieldException if the field cannot be located
208 | */
209 | private static Field findField(Object instance, String name) throws NoSuchFieldException {
210 | for (Class> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
211 | try {
212 | Field field = clazz.getDeclaredField(name);
213 |
214 |
215 | if (!field.isAccessible()) {
216 | field.setAccessible(true);
217 | }
218 |
219 | return field;
220 | } catch (NoSuchFieldException e) {
221 | // ignore and search next
222 | }
223 | }
224 |
225 | throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
226 | }
227 |
228 | /**
229 | * Locates a given method anywhere in the class inheritance hierarchy.
230 | *
231 | * @param instance an object to search the method into.
232 | * @param name method name
233 | * @param parameterTypes method parameter types
234 | * @return a method object
235 | * @throws NoSuchMethodException if the method cannot be located
236 | */
237 | private static Method findMethod(Object instance, String name, Class>... parameterTypes)
238 | throws NoSuchMethodException {
239 | for (Class> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
240 | try {
241 | Method method = clazz.getDeclaredMethod(name, parameterTypes);
242 |
243 |
244 | if (!method.isAccessible()) {
245 | method.setAccessible(true);
246 | }
247 |
248 | return method;
249 | } catch (NoSuchMethodException e) {
250 | // ignore and search next
251 | }
252 | }
253 |
254 | throw new NoSuchMethodException("Method " + name + " with parameters " +
255 | Arrays.asList(parameterTypes) + " not found in " + instance.getClass());
256 | }
257 |
258 | private static final class V19 {
259 |
260 | private static void install(ClassLoader loader, List additionalClassPathEntries,
261 | File optimizedDirectory)
262 | throws IllegalArgumentException, IllegalAccessException,
263 | NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
264 | /* The patched class loader is expected to be a descendant of
265 | * dalvik.system.BaseDexClassLoader. We modify its
266 | * dalvik.system.DexPathList pathList field to append additional DEX
267 | * file entries.
268 | */
269 | Field pathListField = findField(loader, "pathList");
270 | Object dexPathList = pathListField.get(loader);
271 | ArrayList suppressedExceptions = new ArrayList();
272 | expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
273 | new ArrayList(additionalClassPathEntries), optimizedDirectory,
274 | suppressedExceptions));
275 | if (suppressedExceptions.size() > 0) {
276 | for (IOException e : suppressedExceptions) {
277 | Log.w(TAG, "Exception in makeDexElement", e);
278 | }
279 | Field suppressedExceptionsField =
280 | findField(loader, "dexElementsSuppressedExceptions");
281 | IOException[] dexElementsSuppressedExceptions =
282 | (IOException[]) suppressedExceptionsField.get(loader);
283 |
284 | if (dexElementsSuppressedExceptions == null) {
285 | dexElementsSuppressedExceptions =
286 | suppressedExceptions.toArray(
287 | new IOException[suppressedExceptions.size()]);
288 | } else {
289 | IOException[] combined =
290 | new IOException[suppressedExceptions.size() +
291 | dexElementsSuppressedExceptions.length];
292 | suppressedExceptions.toArray(combined);
293 | System.arraycopy(dexElementsSuppressedExceptions, 0, combined,
294 | suppressedExceptions.size(), dexElementsSuppressedExceptions.length);
295 | dexElementsSuppressedExceptions = combined;
296 | }
297 |
298 | suppressedExceptionsField.set(loader, dexElementsSuppressedExceptions);
299 | }
300 | }
301 |
302 | // hacked to work on v23+
303 | /**
304 | * A wrapper around
305 | * {@code private static final dalvik.system.DexPathList#makeDexElements}.
306 | */
307 | private static Object[] makeDexElements(
308 | Object dexPathList, ArrayList files, File optimizedDirectory,
309 | ArrayList suppressedExceptions)
310 | throws IllegalAccessException, InvocationTargetException,
311 | NoSuchMethodException {
312 | boolean v23 = Build.VERSION.SDK_INT >= 23;
313 | Class> listType = v23 ? List.class : ArrayList.class;
314 | Method makeDexElements =
315 | findMethod(dexPathList, v23 ? "makePathElements" : "makeDexElements",
316 | listType, File.class, listType);
317 |
318 | return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,
319 | suppressedExceptions);
320 | }
321 | }
322 |
323 | /**
324 | * Installer for platform versions 14, 15, 16, 17 and 18.
325 | */
326 | private static final class V14 {
327 |
328 | private static void install(ClassLoader loader, List additionalClassPathEntries,
329 | File optimizedDirectory)
330 | throws IllegalArgumentException, IllegalAccessException,
331 | NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
332 | /* The patched class loader is expected to be a descendant of
333 | * dalvik.system.BaseDexClassLoader. We modify its
334 | * dalvik.system.DexPathList pathList field to append additional DEX
335 | * file entries.
336 | */
337 | Field pathListField = findField(loader, "pathList");
338 | Object dexPathList = pathListField.get(loader);
339 | expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
340 | new ArrayList(additionalClassPathEntries), optimizedDirectory));
341 | }
342 |
343 | /**
344 | * A wrapper around
345 | * {@code private static final dalvik.system.DexPathList#makeDexElements}.
346 | */
347 | private static Object[] makeDexElements(
348 | Object dexPathList, ArrayList files, File optimizedDirectory)
349 | throws IllegalAccessException, InvocationTargetException,
350 | NoSuchMethodException {
351 | Method makeDexElements =
352 | findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class);
353 |
354 | return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory);
355 | }
356 | }
357 |
358 | /**
359 | * Installer for platform versions 4 to 13.
360 | */
361 | private static final class V4 {
362 | private static void install(ClassLoader loader, List additionalClassPathEntries)
363 | throws IllegalArgumentException, IllegalAccessException,
364 | NoSuchFieldException, IOException {
365 | /* The patched class loader is expected to be a descendant of
366 | * dalvik.system.DexClassLoader. We modify its
367 | * fields mPaths, mFiles, mZips and mDexs to append additional DEX
368 | * file entries.
369 | */
370 | int extraSize = additionalClassPathEntries.size();
371 |
372 | Field pathField = findField(loader, "path");
373 |
374 | StringBuilder path = new StringBuilder((String) pathField.get(loader));
375 | String[] extraPaths = new String[extraSize];
376 | File[] extraFiles = new File[extraSize];
377 | ZipFile[] extraZips = new ZipFile[extraSize];
378 | DexFile[] extraDexs = new DexFile[extraSize];
379 | for (ListIterator iterator = additionalClassPathEntries.listIterator();
380 | iterator.hasNext();) {
381 | File additionalEntry = iterator.next();
382 | String entryPath = additionalEntry.getAbsolutePath();
383 | path.append(':').append(entryPath);
384 | int index = iterator.previousIndex();
385 | extraPaths[index] = entryPath;
386 | extraFiles[index] = additionalEntry;
387 | extraZips[index] = new ZipFile(additionalEntry);
388 | extraDexs[index] = DexFile.loadDex(entryPath, entryPath + ".dex", 0);
389 | }
390 |
391 | pathField.set(loader, path.toString());
392 | expandFieldArray(loader, "mPaths", extraPaths);
393 | expandFieldArray(loader, "mFiles", extraFiles);
394 | expandFieldArray(loader, "mZips", extraZips);
395 | expandFieldArray(loader, "mDexs", extraDexs);
396 | }
397 | }
398 | }
399 |
--------------------------------------------------------------------------------
/agent/src/main/java/com/hanhuy/android/protify/agent/internal/LifecycleListener.java:
--------------------------------------------------------------------------------
1 | package com.hanhuy.android.protify.agent.internal;
2 |
3 | import android.annotation.TargetApi;
4 | import android.app.Activity;
5 | import android.app.Application;
6 | import android.content.Context;
7 | import android.os.Bundle;
8 | import android.util.Log;
9 | import com.hanhuy.android.protify.agent.ProtifyApplication;
10 |
11 | import java.io.File;
12 | import java.lang.ref.WeakReference;
13 | import java.util.IdentityHashMap;
14 | import java.util.Map;
15 |
16 | /**
17 | * @author pfnguyen
18 | */
19 | @TargetApi(14)
20 | public class LifecycleListener implements Application.ActivityLifecycleCallbacks {
21 | private final Map activities = new IdentityHashMap();
22 | private final static String TAG = "LifecycleListener";
23 | private LifecycleListener() { }
24 |
25 | private final static LifecycleListener INSTANCE = new LifecycleListener();
26 | public static LifecycleListener getInstance() {
27 | return INSTANCE;
28 | }
29 | // Sometimes, resuming an activity from the background results in
30 | // onActivityPaused being called immediately, and we lose our 'top'
31 | // if we null it out then. stash it away until onDestroy
32 | private Activity top = null;
33 |
34 | @Override public void onActivityCreated(Activity activity, Bundle bundle) {
35 | activities.put(activity, System.currentTimeMillis());
36 | }
37 | @Override public void onActivityStarted(Activity activity) { }
38 | @Override public void onActivityStopped(Activity activity) { }
39 | @Override public void onActivitySaveInstanceState(Activity activity, Bundle bundle) { }
40 | @Override public void onActivityDestroyed(Activity activity) {
41 | if (activity == top) top = null;
42 | activities.remove(activity);
43 | }
44 | // no corollary onActivityPaused because resuming from home results in weird behavior
45 | @Override public void onActivityPaused(Activity activity) { }
46 |
47 | @Override
48 | public void onActivityResumed(Activity activity) {
49 | top = activity;
50 | // should never be null
51 | long l = activities.get(activity);
52 | if (l < ProtifyApplication.getResourceInstallTime())
53 | activity.recreate();
54 | }
55 |
56 |
57 | public Activity getTopActivity() {
58 | return top;
59 | }
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/agent/src/main/java/com/hanhuy/android/protify/agent/internal/ProtifyActivity.java:
--------------------------------------------------------------------------------
1 | package com.hanhuy.android.protify.agent.internal;
2 |
3 | import android.app.Activity;
4 | import android.app.KeyguardManager;
5 | import android.content.Context;
6 | import android.os.Build;
7 | import android.os.Bundle;
8 | import android.os.Handler;
9 | import android.os.PowerManager;
10 | import android.os.Process;
11 | import android.widget.TextView;
12 |
13 | /**
14 | * android recreates the previous activity (with saved state) in the task stack
15 | * when an application crashes. create a new activity and "crash" it, forcing
16 | * the underlying activity to be recreated
17 | * @author pfnguyen
18 | */
19 | public class ProtifyActivity extends Activity {
20 | private final static String STATE_SAVED = "protify.state.SAVED";
21 | private int asDp(int px, float d) {
22 | return (int) (px * d);
23 | }
24 |
25 | @Override
26 | protected void onCreate(Bundle state) {
27 | super.onCreate(state);
28 | PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
29 | KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
30 | boolean isOn = Build.VERSION.SDK_INT < 7 || pm.isScreenOn();
31 | boolean isKG = km.inKeyguardRestrictedInputMode();
32 | if (!isOn || isKG) {
33 | finish();
34 | return;
35 | }
36 | TextView tv = new TextView(this);
37 | tv.setText("Protifying code...");
38 | float d = getResources().getDisplayMetrics().density;
39 | tv.setPadding(asDp(8, d), asDp(8, d), asDp(8, d), asDp(8, d));
40 | setContentView(tv);
41 | if (Build.VERSION.SDK_INT >= 11 &&
42 | (state == null || !state.getBoolean(STATE_SAVED, false))) {
43 | recreate();
44 | } else {
45 | new Handler().post(new Runnable() {
46 | @Override
47 | public void run() {
48 | finish();
49 | }
50 | });
51 | }
52 | }
53 |
54 | /*
55 | @Override
56 | protected void onPostResume() {
57 | super.onPostResume();
58 | // TODO find the correct event where we don't have to hack a delay
59 | new Handler().postDelayed(new Runnable() {
60 | @Override
61 | public void run() {
62 | Process.killProcess(Process.myPid());
63 | }
64 | }, 1000);
65 | }
66 | */
67 |
68 | @Override
69 | protected void onSaveInstanceState(Bundle state) {
70 | // ensure state so that the framework will restore our activity/task stack
71 | state.putBoolean(STATE_SAVED, true);
72 | super.onSaveInstanceState(state);
73 | new Handler().post(new Runnable() {
74 | @Override
75 | public void run() {
76 | Process.killProcess(Process.myPid());
77 | }
78 | });
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/agent/src/main/java/com/hanhuy/android/protify/agent/internal/ProtifyReceiver.java:
--------------------------------------------------------------------------------
1 | package com.hanhuy.android.protify.agent.internal;
2 |
3 | import android.annotation.TargetApi;
4 | import android.app.Activity;
5 | import android.content.BroadcastReceiver;
6 | import android.content.ComponentName;
7 | import android.content.Context;
8 | import android.content.Intent;
9 | import android.content.pm.*;
10 | import android.os.Build;
11 | import android.os.Bundle;
12 | import android.os.Process;
13 | import android.util.Log;
14 | import com.hanhuy.android.protify.Intents;
15 | import com.hanhuy.android.protify.agent.ProtifyApplication;
16 |
17 | import java.io.*;
18 | import java.nio.channels.FileChannel;
19 | import java.util.List;
20 | import java.util.zip.ZipEntry;
21 | import java.util.zip.ZipOutputStream;
22 |
23 | /**
24 | * @author pfnguyen
25 | */
26 | public class ProtifyReceiver extends BroadcastReceiver {
27 | private final static String TAG = "ProtifyReceiver";
28 | @Override
29 | public void onReceive(Context context, Intent intent) {
30 | final String action = intent == null ? null : intent.getAction();
31 | Log.v(TAG, "Received action: " + action);
32 | boolean ltV14 = Build.VERSION.SDK_INT < 14;
33 | Activity top = ltV14 ? null : LifecycleListener.getInstance().getTopActivity();
34 | if (Intents.PROTIFY_INTENT.equals(action)) {
35 | InstallState result = install(intent.getExtras(), context);
36 | if (result.dex || (result.resources && ltV14)) {
37 | Log.v(TAG, "Updated dex, restarting process, top non-null: " +
38 | (top != null));
39 | if (top != null || ltV14) {
40 | restartApp(context);
41 | } else {
42 | Process.killProcess(Process.myPid());
43 | }
44 | } else if (result.resources) {
45 | Log.v(TAG, "Updated resources, recreating activities");
46 | recreateActivity(top);
47 | if (top == null) {
48 | ApplicationInfo info = context.getApplicationInfo();
49 | PackageManager pm = context.getPackageManager();
50 | Intent mainIntent = new Intent(Intent.ACTION_MAIN);
51 | mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
52 | List activities = pm.queryIntentActivities(mainIntent, 0);
53 | for (ResolveInfo ri : activities) {
54 | if (info.packageName.equals(ri.activityInfo.packageName)) {
55 | Intent main = new Intent();
56 | main.setComponent(new ComponentName(info.packageName, ri.activityInfo.name));
57 | main.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
58 | context.startActivity(main);
59 | break;
60 | }
61 | }
62 | } else {
63 | Intent bringToFront = (Intent) top.getIntent().clone();
64 | bringToFront.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
65 | context.startActivity(bringToFront);
66 | }
67 | }
68 | } else if (Intents.INSTALL_INTENT.equals(action)) {
69 | InstallState result = install(intent.getExtras(), context);
70 | if (result.dex || result.resources) {
71 | Log.v(TAG, "Installed new resources or dex, restarting process");
72 | Process.killProcess(Process.myPid());
73 | }
74 | } else if (Intents.CLEAN_INTENT.equals(action)) {
75 | Log.v(TAG, "Clearing resources and dex from cache");
76 | try {
77 | ProtifyResources.getResourcesFile(context).delete();
78 | File[] files = DexLoader.getDexExtractionDir(context).listFiles();
79 | if (files != null) {
80 | for (File f : files) {
81 | f.delete();
82 | }
83 | }
84 | } catch (Throwable t) {
85 | // noop don't care
86 | }
87 | if (top != null) {
88 | restartApp(context);
89 | } else {
90 | Process.killProcess(Process.myPid());
91 | }
92 | }
93 | }
94 |
95 | private void restartApp(Context context) {
96 | // Intent reset = new Intent(context, ProtifyActivity.class);
97 | // reset.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
98 | // context.startActivity(reset);
99 | Context application = context.getApplicationContext();
100 | Restarter.restartApp(
101 | application, Restarter.getActivities(application, false), true);
102 | }
103 |
104 | private InstallState install(Bundle extras, Context context) {
105 | if (extras != null) {
106 | String resources = extras.getString(Intents.EXTRA_RESOURCES);
107 | String dexInfo = extras.getString(Intents.EXTRA_DEX_INFO);
108 | File dexInfoFile = dexInfo == null ? null : new File(dexInfo);
109 | boolean hasDex = dexInfoFile != null && dexInfoFile.isFile() && dexInfoFile.length() > 0;
110 | boolean hasRes = ProtifyResources.updateResourcesFile(context, resources);
111 | if (hasRes) ProtifyApplication.installExternalResources(context);
112 | if (hasDex) {
113 | try {
114 | StringWriter sw = new StringWriter();
115 | BufferedReader r = new BufferedReader(
116 | new InputStreamReader(new FileInputStream(dexInfoFile), "utf-8"));
117 | String line;
118 | while ((line = r.readLine()) != null) {
119 | sw.write(line + "\n");
120 | }
121 | String[] lines = sw.toString().split("\n");
122 | for (String l : lines) {
123 | String[] ls = l.split(":");
124 | String dex = ls[0];
125 | String dexName = ls[1];
126 | Log.v(TAG, "Loading DEX from " + dex + " to " + dexName);
127 | File dexDir = DexLoader.getDexExtractionDir(context);
128 | if (Build.VERSION.SDK_INT >= 14) {
129 | File dexfile = new File(dex);
130 | FileChannel ch = new FileInputStream(dexfile).getChannel();
131 | File dest = new File(dexDir, dexName);
132 | FileChannel ch2 = new FileOutputStream(dest, false).getChannel();
133 | ch.transferTo(0, dexfile.length(), ch2);
134 | ch.close();
135 | ch2.close();
136 | } else {
137 | File dexfile = new File(dex);
138 | File dest = new File(dexDir, dexName + DexExtractor.ZIP_SUFFIX);
139 | ZipOutputStream zout = new ZipOutputStream(
140 | new BufferedOutputStream(new FileOutputStream(dest)));
141 | FileInputStream fin = new FileInputStream(dexfile);
142 | try {
143 | ZipEntry classesDex = new ZipEntry("classes.dex");
144 | classesDex.setTime(dexfile.lastModified());
145 | zout.putNextEntry(classesDex);
146 | byte[] buffer = new byte[0x4000];
147 | int read;
148 | while ((read = fin.read(buffer)) != -1) {
149 | zout.write(buffer, 0, read);
150 | }
151 | zout.closeEntry();
152 | } finally {
153 | fin.close();
154 | zout.close();
155 | }
156 | }
157 | }
158 | r.close();
159 | } catch (Exception e) {
160 | throw new RuntimeException("Cannot copy DEX: " + e.getMessage(), e);
161 | }
162 | }
163 | return new InstallState(hasRes, hasDex);
164 | }
165 | return InstallState.NONE;
166 | }
167 |
168 | final static class InstallState {
169 | public final static InstallState NONE = new InstallState(false, false);
170 | public final boolean resources;
171 | public final boolean dex;
172 |
173 | public InstallState(boolean resources, boolean dex) {
174 | this.resources = resources;
175 | this.dex = dex;
176 | }
177 | }
178 |
179 | @TargetApi(11)
180 | private static void recreateActivity(Activity a) {
181 | if (a != null) a.recreate();
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/agent/src/main/java/com/hanhuy/android/protify/agent/internal/ProtifyResources.java:
--------------------------------------------------------------------------------
1 | package com.hanhuy.android.protify.agent.internal;
2 |
3 | import android.content.Context;
4 |
5 | import java.io.File;
6 | import java.io.FileInputStream;
7 | import java.io.FileOutputStream;
8 | import java.io.IOException;
9 | import java.nio.channels.FileChannel;
10 |
11 | /**
12 | * @author pfnguyen
13 | */
14 | public class ProtifyResources {
15 | public static boolean updateResourcesFile(Context ctx, String resourcePath) {
16 | if (resourcePath == null) return false;
17 | File resapk = new File(resourcePath);
18 | File cacheres = getResourcesFile(ctx);
19 | if (resapk.isFile() && resapk.length() > 0) {
20 | try {
21 | FileChannel ch = new FileInputStream(resapk).getChannel();
22 | FileChannel ch2 = new FileOutputStream(cacheres, false).getChannel();
23 | ch.transferTo(0, resapk.length(), ch2);
24 | ch.close();
25 | ch2.close();
26 | } catch (IOException e) {
27 | throw new RuntimeException("Cannot copy resource apk: " + e.getMessage(), e);
28 | }
29 | return true;
30 | }
31 | return false;
32 | }
33 |
34 | public static File getResourcesFile(Context ctx) {
35 | File f = new File(ctx.getFilesDir(), "protify-resources");
36 | f.mkdirs();
37 | return new File(f, "protify-resources.ap_");
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/agent/src/main/java/com/hanhuy/android/protify/agent/internal/Restarter.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2015 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.hanhuy.android.protify.agent.internal;
17 |
18 | import android.app.Activity;
19 | import android.app.AlarmManager;
20 | import android.app.PendingIntent;
21 | import android.content.Context;
22 | import android.content.ContextWrapper;
23 | import android.content.Intent;
24 | import android.os.Build;
25 | import android.support.annotation.NonNull;
26 | import android.support.annotation.Nullable;
27 |
28 | import android.os.Handler;
29 | import android.os.Looper;
30 | import android.util.ArrayMap;
31 | import android.util.Log;
32 | import android.widget.Toast;
33 |
34 | import java.lang.reflect.Field;
35 | import java.util.ArrayList;
36 | import java.util.Collection;
37 | import java.util.HashMap;
38 | import java.util.List;
39 | import java.util.Map;
40 | import java.lang.reflect.Method;
41 |
42 | /**
43 | * Handler capable of restarting parts of the application in order for changes to become
44 | * apparent to the user:
45 | *
46 | * - Apply a tiny change immediately (possible if we can detect that the change
47 | * is only used in a limited context (such as in a layout) and we can directly
48 | * poke the view hierarchy and schedule a paint
49 | *
- Apply a change to the current activity. We can restart just the activity
50 | * while the app continues running.
51 | *
- Restart the app with state persistence (simulates what happens when a user
52 | * puts an app in the background, then it gets killed by the memory monitor,
53 | * and then restored when the user brings it back
54 | *
- Restart the app completely.
55 | *
56 | */
57 | public class Restarter {
58 | private final static String LOG_TAG = "ProtifyRestarter";
59 |
60 | /**
61 | * Attempt to restart the app. Ideally this should also try to preserve as much state as
62 | * possible:
63 | *
64 | * - The current activity
65 | * - If possible, state in the current activity, and
66 | * - The activity stack
67 | *
68 | *
69 | * This may require some framework support. Apparently it may already be possible
70 | * (Dianne says to put the app in the background, kill it then restart it; need to
71 | * figure out how to do this.)
72 | */
73 | public static void restartApp(@Nullable Context appContext,
74 | @NonNull Collection knownActivities,
75 | boolean toast) {
76 | if (!knownActivities.isEmpty()) {
77 | // Can't live patch resources; instead, try to restart the current activity
78 | Activity foreground = getForegroundActivity(appContext);
79 |
80 | if (foreground != null) {
81 | // http://stackoverflow.com/questions/6609414/howto-programatically-restart-android-app
82 | //noinspection UnnecessaryLocalVariable
83 | if (toast) {
84 | showToast(foreground, "Protifying code...");
85 | }
86 | if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
87 | Log.v(LOG_TAG, "RESTARTING APP");
88 | }
89 | @SuppressWarnings("UnnecessaryLocalVariable") // fore code clarify
90 | Context context = foreground;
91 | Intent original = foreground.getIntent();
92 | Intent intent = original != null ? original : new Intent(context, foreground.getClass());
93 | PendingIntent pendingIntent = PendingIntent.getActivity(context, 0,
94 | intent, PendingIntent.FLAG_CANCEL_CURRENT);
95 | AlarmManager mgr = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
96 | mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, pendingIntent);
97 | if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
98 | Log.v(LOG_TAG, "Scheduling activity " + foreground
99 | + " to start after exiting process");
100 | }
101 | } else {
102 | showToast(knownActivities.iterator().next(), "Unable to restart app");
103 | if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
104 | Log.v(LOG_TAG, "Couldn't find any foreground activities to restart " +
105 | "for resource refresh");
106 | }
107 | }
108 | System.exit(0);
109 | }
110 | }
111 |
112 | static void showToast(@NonNull final Activity activity, @NonNull final String text) {
113 | if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
114 | Log.v(LOG_TAG, "About to show toast for activity " + activity + ": " + text);
115 | }
116 | activity.runOnUiThread(new Runnable() {
117 | @Override
118 | public void run() {
119 | try {
120 | Context context = activity.getApplicationContext();
121 | if (context instanceof ContextWrapper) {
122 | Context base = ((ContextWrapper) context).getBaseContext();
123 | if (base == null) {
124 | if (Log.isLoggable(LOG_TAG, Log.WARN)) {
125 | Log.w(LOG_TAG, "Couldn't show toast: no base context");
126 | }
127 | return;
128 | }
129 | }
130 |
131 | // For longer messages, leave the message up longer
132 | int duration = Toast.LENGTH_SHORT;
133 | if (text.length() >= 60 || text.indexOf('\n') != -1) {
134 | duration = Toast.LENGTH_LONG;
135 | }
136 |
137 | // Avoid crashing when not available, e.g.
138 | // java.lang.RuntimeException: Can't create handler inside thread that has
139 | // not called Looper.prepare()
140 | Toast.makeText(activity, text, duration).show();
141 | } catch (Throwable e) {
142 | if (Log.isLoggable(LOG_TAG, Log.WARN)) {
143 | Log.w(LOG_TAG, "Couldn't show toast", e);
144 | }
145 | }
146 | }
147 | });
148 | }
149 |
150 | @Nullable
151 | public static Activity getForegroundActivity(@Nullable Context context) {
152 | List list = getActivities(context, true);
153 | return list.isEmpty() ? null : list.get(0);
154 | }
155 |
156 | // http://stackoverflow.com/questions/11411395/how-to-get-current-foreground-activity-context-in-android
157 | @NonNull
158 | public static List getActivities(@Nullable Context context, boolean foregroundOnly) {
159 | List list = new ArrayList();
160 | try {
161 | Class activityThreadClass = Class.forName("android.app.ActivityThread");
162 | Object activityThread = getActivityThread(context, activityThreadClass);
163 | Field activitiesField = activityThreadClass.getDeclaredField("mActivities");
164 | activitiesField.setAccessible(true);
165 |
166 | // TODO: On older platforms, cast this to a HashMap
167 |
168 | Collection c;
169 | Object collection = activitiesField.get(activityThread);
170 |
171 | if (collection instanceof HashMap) {
172 | // Older platforms
173 | Map activities = (HashMap) collection;
174 | c = activities.values();
175 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT &&
176 | collection instanceof ArrayMap) {
177 | ArrayMap activities = (ArrayMap) collection;
178 | c = activities.values();
179 | } else {
180 | return list;
181 | }
182 | for (Object activityRecord : c) {
183 | Class activityRecordClass = activityRecord.getClass();
184 | if (foregroundOnly) {
185 | Field pausedField = activityRecordClass.getDeclaredField("paused");
186 | pausedField.setAccessible(true);
187 | if (pausedField.getBoolean(activityRecord)) {
188 | continue;
189 | }
190 | }
191 | Field activityField = activityRecordClass.getDeclaredField("activity");
192 | activityField.setAccessible(true);
193 | Activity activity = (Activity) activityField.get(activityRecord);
194 | if (activity != null) {
195 | list.add(activity);
196 | }
197 | }
198 | } catch (Throwable ignore) {
199 | }
200 | return list;
201 | }
202 |
203 | @Nullable
204 | public static Object getActivityThread(@Nullable Context context,
205 | @Nullable Class> activityThread) {
206 | try {
207 | if (activityThread == null) {
208 | activityThread = Class.forName("android.app.ActivityThread");
209 | }
210 | Method m = activityThread.getMethod("currentActivityThread");
211 | m.setAccessible(true);
212 | Object currentActivityThread = m.invoke(null);
213 | if (currentActivityThread == null && context != null) {
214 | // In older versions of Android (prior to frameworks/base 66a017b63461a22842)
215 | // the currentActivityThread was built on thread locals, so we'll need to try
216 | // even harder
217 | Field mLoadedApk = context.getClass().getField("mLoadedApk");
218 | mLoadedApk.setAccessible(true);
219 | Object apk = mLoadedApk.get(context);
220 | Field mActivityThreadField = apk.getClass().getDeclaredField("mActivityThread");
221 | mActivityThreadField.setAccessible(true);
222 | currentActivityThread = mActivityThreadField.get(apk);
223 | }
224 | return currentActivityThread;
225 | } catch (Throwable ignore) {
226 | return null;
227 | }
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/agent/src/main/java/com/hanhuy/android/protify/agent/internal/ZipUtil.java:
--------------------------------------------------------------------------------
1 | package com.hanhuy.android.protify.agent.internal;
2 |
3 | /*
4 | * Licensed to the Apache Software Foundation (ASF) under one or more
5 | * contributor license agreements. See the NOTICE file distributed with
6 | * this work for additional information regarding copyright ownership.
7 | * The ASF licenses this file to You under the Apache License, Version 2.0
8 | * (the "License"); you may not use this file except in compliance with
9 | * the License. You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | */
19 | /* Apache Harmony HEADER because the code in this class comes mostly from ZipFile, ZipEntry and
20 | * ZipConstants from android libcore.
21 | */
22 |
23 |
24 | import java.io.File;
25 | import java.io.IOException;
26 | import java.io.RandomAccessFile;
27 | import java.util.zip.CRC32;
28 | import java.util.zip.ZipException;
29 |
30 | /**
31 | * Tools to build a quick partial crc of zip files.
32 | */
33 | final class ZipUtil {
34 | static class CentralDirectory {
35 | long offset;
36 | long size;
37 | }
38 |
39 | /* redefine those constant here because of bug 13721174 preventing to compile using the
40 | * constants defined in ZipFile */
41 | private static final int ENDHDR = 22;
42 | private static final int ENDSIG = 0x6054b50;
43 |
44 | /**
45 | * Size of reading buffers.
46 | */
47 | private static final int BUFFER_SIZE = 0x4000;
48 |
49 | /**
50 | * Compute crc32 of the central directory of an apk. The central directory contains
51 | * the crc32 of each entries in the zip so the computed result is considered valid for the whole
52 | * zip file. Does not support zip64 nor multidisk but it should be OK for now since ZipFile does
53 | * not either.
54 | */
55 | static long getZipCrc(File apk) throws IOException {
56 | RandomAccessFile raf = new RandomAccessFile(apk, "r");
57 | try {
58 | CentralDirectory dir = findCentralDirectory(raf);
59 |
60 | return computeCrcOfCentralDir(raf, dir);
61 | } finally {
62 | raf.close();
63 | }
64 | }
65 |
66 | /* Package visible for testing */
67 | static CentralDirectory findCentralDirectory(RandomAccessFile raf) throws IOException,
68 | ZipException {
69 | long scanOffset = raf.length() - ENDHDR;
70 | if (scanOffset < 0) {
71 | throw new ZipException("File too short to be a zip file: " + raf.length());
72 | }
73 |
74 | long stopOffset = scanOffset - 0x10000 /* ".ZIP file comment"'s max length */;
75 | if (stopOffset < 0) {
76 | stopOffset = 0;
77 | }
78 |
79 | int endSig = Integer.reverseBytes(ENDSIG);
80 | while (true) {
81 | raf.seek(scanOffset);
82 | if (raf.readInt() == endSig) {
83 | break;
84 | }
85 |
86 | scanOffset--;
87 | if (scanOffset < stopOffset) {
88 | throw new ZipException("End Of Central Directory signature not found");
89 | }
90 | }
91 | // Read the End Of Central Directory. ENDHDR includes the signature
92 | // bytes,
93 | // which we've already read.
94 |
95 | // Pull out the information we need.
96 | raf.skipBytes(2); // diskNumber
97 | raf.skipBytes(2); // diskWithCentralDir
98 | raf.skipBytes(2); // numEntries
99 | raf.skipBytes(2); // totalNumEntries
100 | CentralDirectory dir = new CentralDirectory();
101 | dir.size = Integer.reverseBytes(raf.readInt()) & 0xFFFFFFFFL;
102 | dir.offset = Integer.reverseBytes(raf.readInt()) & 0xFFFFFFFFL;
103 | return dir;
104 | }
105 |
106 | /* Package visible for testing */
107 | static long computeCrcOfCentralDir(RandomAccessFile raf, CentralDirectory dir)
108 | throws IOException {
109 | CRC32 crc = new CRC32();
110 | long stillToRead = dir.size;
111 | raf.seek(dir.offset);
112 | int length = (int) Math.min(BUFFER_SIZE, stillToRead);
113 | byte[] buffer = new byte[BUFFER_SIZE];
114 | length = raf.read(buffer, 0, length);
115 | while (length != -1) {
116 | crc.update(buffer, 0, length);
117 | stillToRead -= length;
118 | if (stillToRead == 0) {
119 | break;
120 | }
121 | length = (int) Math.min(BUFFER_SIZE, stillToRead);
122 | length = raf.read(buffer, 0, length);
123 | }
124 | return crc.getValue();
125 | }
126 | }
127 |
128 |
--------------------------------------------------------------------------------
/agent/src/main/res/drawable-hdpi/protify_internal_ic_notification_loading.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scala-android/sbt-android-protify/00a3640df740a62eb942d08e9f96bd5a1faadda3/agent/src/main/res/drawable-hdpi/protify_internal_ic_notification_loading.png
--------------------------------------------------------------------------------
/agent/src/main/res/drawable-mdpi/protify_internal_ic_notification_loading.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scala-android/sbt-android-protify/00a3640df740a62eb942d08e9f96bd5a1faadda3/agent/src/main/res/drawable-mdpi/protify_internal_ic_notification_loading.png
--------------------------------------------------------------------------------
/agent/src/main/res/drawable-xhdpi/protify_internal_ic_notification_loading.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scala-android/sbt-android-protify/00a3640df740a62eb942d08e9f96bd5a1faadda3/agent/src/main/res/drawable-xhdpi/protify_internal_ic_notification_loading.png
--------------------------------------------------------------------------------
/agent/src/main/res/values-v14/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/agent/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 | AAPT BUG WORKAROUND (will not generate R.java without a non-style resource)
8 |
9 |
--------------------------------------------------------------------------------
/android/project.properties:
--------------------------------------------------------------------------------
1 | target=android-22
2 |
--------------------------------------------------------------------------------
/android/src/androidTest/assets/resources-release.ap_:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scala-android/sbt-android-protify/00a3640df740a62eb942d08e9f96bd5a1faadda3/android/src/androidTest/assets/resources-release.ap_
--------------------------------------------------------------------------------
/android/src/androidTest/scala/com/hanhuy/android/protify/ResourcesTest.scala:
--------------------------------------------------------------------------------
1 | package com.hanhuy.android.protify
2 | import android.support.test.InstrumentationRegistry
3 | import android.support.test.runner.AndroidJUnit4
4 | import android.support.test.rule.ActivityTestRule
5 | import org.junit.Rule
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 | import org.junit.Assert._
9 | import android.content.res.Resources
10 | import android.content.res.AssetManager
11 |
12 | import com.hanhuy.android.keepshare.{R => TestR}
13 |
14 | @RunWith(classOf[AndroidJUnit4])
15 | class ResourcesTest {
16 | @Test def shouldReadResources() = {
17 | val ctx = InstrumentationRegistry.getContext
18 | val in = ctx.getAssets.open("resources-release.ap_")
19 | val out = ctx.openFileOutput("resources-release.ap_", 0)
20 | val b = Array.ofDim[Byte](32768)
21 | Stream.continually(in.read(b, 0, 32768)).takeWhile(
22 | _ != -1).foreach (out.write(b, 0, _))
23 | in.close()
24 | out.close()
25 | val f = new java.io.File(ctx.getFilesDir, "resources-release.ap_")
26 | assertEquals(1380246, f.length)
27 | val inf = ctx.getPackageManager.getPackageArchiveInfo(f.getAbsolutePath, 0)
28 | assertNotNull(inf)
29 | val am2 = classOf[AssetManager].newInstance
30 | am2.asInstanceOf[PrivateAssetManager].addAssetPath(f.getAbsolutePath)
31 | val oldres = ctx.getResources
32 | val res = new Resources(
33 | am2, oldres.getDisplayMetrics, oldres.getConfiguration)
34 | val xpp = res.getLayout(TestR.layout.pin_setup)
35 | assertNotNull(xpp)
36 | xpp.close()
37 | assertEquals("KeepShare Lite", res.getString(TestR.string.appname))
38 | assertEquals("content://com.hanhuy.android.keepshare.lite/entry",
39 | res.getString(TestR.string.search_suggest_intent_data))
40 | assertEquals("com.hanhuy.android.keepshare.lite",
41 | res.getString(TestR.string.search_suggest_authority))
42 | assertEquals("KeepShare Lite Form Filler",
43 | res.getString(TestR.string.accessibility_service_label))
44 | }
45 | private type PrivateAssetManager = {
46 | def addAssetPath(s: java.lang.String): Unit
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
12 |
13 |
14 |
15 |
16 |
17 |
19 |
21 |
23 |
25 |
28 |
29 |
30 |
31 |
32 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/android/src/main/java/com/hanhuy/android/protify/RTxtLoaderBase.java:
--------------------------------------------------------------------------------
1 | package com.hanhuy.android.protify;
2 |
3 |
4 | import android.util.Log;
5 |
6 | import java.io.*;
7 |
8 | public abstract class RTxtLoaderBase {
9 |
10 | private final static String TAG = "RTxtLoader";
11 | private String lasthash;
12 |
13 | private static boolean eq(Object o1, Object o2) {
14 | return o1 == null ? o2 == null : o1.equals(o2);
15 | }
16 |
17 | public void load(String rtxt, String hash) {
18 | if (!eq(lasthash,hash)) {
19 | if (rtxt != null) {
20 | File f = new File(rtxt);
21 | if (f.isFile()) {
22 |
23 | BufferedReader r = null;
24 | try {
25 | r = new BufferedReader(new InputStreamReader(new FileInputStream(f), "utf-8"));
26 | String line;
27 | while ((line = r.readLine()) != null) {
28 | String[] parts = line.split(" ");
29 | switch (parts[0]) {
30 | case "int": {
31 | String clazz = parts[1];
32 | String name = parts[2];
33 | int value = asInt(parts[3]);
34 | setInt(clazz, name, value);
35 | break;
36 | }
37 | case "int[]": {
38 | String clazz = parts[1];
39 | String name = parts[2];
40 | int[] value = new int[parts.length - 5];
41 | for (int i = 0, j = 4; i < value.length; i++, j++) {
42 | value[i] = asInt(parts[j].replaceAll(",",""));
43 | }
44 | setIntArray(clazz, name, value);
45 | break;
46 | }
47 | }
48 | }
49 |
50 | } catch (IOException e) {
51 | // swallow
52 | } finally {
53 | try {
54 | if (r != null) r.close();
55 | } catch (IOException ex) {
56 | // swallow
57 | }
58 | }
59 | }
60 | }
61 |
62 | } else {
63 | Log.v(TAG, "Skipping R.txt loading");
64 | }
65 | lasthash = hash;
66 | }
67 |
68 | private static int asInt(String s) {
69 | return s.startsWith("0x") ? Integer.parseInt(s.substring(2), 16) : Integer.parseInt(s);
70 | }
71 |
72 | public abstract void setInt(String clazz, String name, int value);
73 | public abstract void setIntArray(String clazz, String name, int[] value);
74 | }
75 |
--------------------------------------------------------------------------------
/android/src/main/res/drawable-hdpi/protify_ic_launcher_internal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scala-android/sbt-android-protify/00a3640df740a62eb942d08e9f96bd5a1faadda3/android/src/main/res/drawable-hdpi/protify_ic_launcher_internal.png
--------------------------------------------------------------------------------
/android/src/main/res/drawable-ldpi/protify_ic_launcher_internal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scala-android/sbt-android-protify/00a3640df740a62eb942d08e9f96bd5a1faadda3/android/src/main/res/drawable-ldpi/protify_ic_launcher_internal.png
--------------------------------------------------------------------------------
/android/src/main/res/drawable-mdpi/protify_ic_launcher_internal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scala-android/sbt-android-protify/00a3640df740a62eb942d08e9f96bd5a1faadda3/android/src/main/res/drawable-mdpi/protify_ic_launcher_internal.png
--------------------------------------------------------------------------------
/android/src/main/res/drawable-xhdpi/protify_ic_launcher_internal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scala-android/sbt-android-protify/00a3640df740a62eb942d08e9f96bd5a1faadda3/android/src/main/res/drawable-xhdpi/protify_ic_launcher_internal.png
--------------------------------------------------------------------------------
/android/src/main/res/layout/main.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/android/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Protify
4 |
5 |
--------------------------------------------------------------------------------
/android/src/main/scala/com/hanhuy/android/protify/DexActivity.scala:
--------------------------------------------------------------------------------
1 | package com.hanhuy.android.protify
2 |
3 | import android.app.{ProgressDialog, Activity}
4 | import android.content.{Intent, Context}
5 | import android.os.{Build, Bundle}
6 | import android.support.v7.app.AppCompatActivity
7 | import android.util.AttributeSet
8 | import android.view.{View, MenuItem, Menu}
9 |
10 | import Intents._
11 | import android.widget.Toast
12 | import com.hanhuy.android.common.Logcat
13 | import dalvik.system.DexClassLoader
14 |
15 | /**
16 | * @author pfnguyen
17 | */
18 | object DexActivity {
19 | def start(ctx: Context) = {
20 | proxy foreach { case (p,a) => p.onProxyUnload(a) }
21 | proxy = None
22 | val intent = new Intent(ctx, if (DexArguments.appcompat)
23 | classOf[AppCompatDexActivity] else classOf[DexActivity])
24 | intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
25 | intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
26 | intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
27 | intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
28 | intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
29 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
30 | ctx.startActivity(intent)
31 | }
32 |
33 | var proxy = Option.empty[(ActivityProxy, Activity)]
34 | }
35 |
36 | object DexArguments {
37 | var resources = ""
38 | var dex = ""
39 | var proxy = ""
40 | var appcompat = false
41 | }
42 |
43 | trait ExternalDexLoader extends Activity with ExternalResourceLoader {
44 | val log2 = Logcat("ExternalDexLoader")
45 |
46 | override def resPath = Some(DexArguments.resources)
47 |
48 | private[this] var proxy = Option.empty[ActivityProxy]
49 |
50 | lazy val loader = {
51 | val cache = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
52 | getCodeCacheDir
53 | else
54 | getDir("dex", Context.MODE_PRIVATE)
55 | log2.v("Loading dex: " + System.currentTimeMillis)
56 | val cl = new DexClassLoader(DexArguments.dex, cache.getAbsolutePath, null, getClassLoader)
57 | log2.v("Loaded dex: " + System.currentTimeMillis)
58 | cl
59 | }
60 |
61 | override def onNewIntent(intent: Intent) = {
62 | super.onNewIntent(intent)
63 | log2.v("Re-launching")
64 | recreate()
65 | }
66 |
67 | override def onCreate(savedInstanceState: Bundle) = {
68 | val clazz = loader.loadClass(DexArguments.proxy)
69 | try {
70 | proxy = Some(clazz.newInstance.asInstanceOf[ActivityProxy])
71 | DexActivity.proxy = proxy map (_ -> this)
72 | } catch {
73 | case ex: Exception =>
74 | Toast.makeText(this,
75 | "Unable to load proxy: " + ex.getMessage, Toast.LENGTH_LONG).show()
76 | log2.w(ex.getMessage, ex)
77 | finish()
78 | }
79 |
80 | proxy foreach (_.onProxyLoad(this))
81 | super.onCreate(savedInstanceState)
82 | proxy foreach (_.onCreate(this, savedInstanceState))
83 | }
84 | override def onDestroy() = {
85 | proxy foreach (_.onDestroy(this))
86 | super.onDestroy()
87 | proxy foreach (_.onProxyUnload(this))
88 | DexActivity.proxy = None
89 | }
90 | override def onPause() = {
91 | proxy foreach (_.onPause(this))
92 | super.onPause()
93 | }
94 | override def onResume() = {
95 | proxy foreach (_.onResume(this))
96 | super.onResume()
97 | }
98 | override def onStart() = {
99 | proxy foreach (_.onStart(this))
100 | super.onStart()
101 | }
102 | override def onStop() = {
103 | proxy foreach (_.onStop(this))
104 | super.onStop()
105 | }
106 | override def onCreateOptionsMenu(menu: Menu) = {
107 | proxy foreach (_.onCreateOptionsMenu(this, menu))
108 | super.onCreateOptionsMenu(menu)
109 | }
110 |
111 | override def onOptionsItemSelected(item: MenuItem) = {
112 | proxy.fold(super.onOptionsItemSelected(item)) (
113 | _.onOptionsItemSelected(this, item)) || super.onOptionsItemSelected(item)
114 | }
115 |
116 | override def onCreateView(name: String, context: Context, attrs: AttributeSet) = try {
117 | super.onCreateView(name, context, attrs)
118 | } catch {
119 | case e: Exception => null
120 | }
121 |
122 |
123 | override def onCreateView(parent: View, name: String, context: Context, attrs: AttributeSet) = try {
124 | super.onCreateView(parent, name, context, attrs)
125 | } catch {
126 | case e: Exception => null
127 | }
128 | }
129 |
130 | class DexActivity extends Activity with ExternalDexLoader with ViewServerSupport
131 | class AppCompatDexActivity extends AppCompatActivity with ExternalDexLoader with ViewServerSupport
132 |
--------------------------------------------------------------------------------
/android/src/main/scala/com/hanhuy/android/protify/LayoutActivity.scala:
--------------------------------------------------------------------------------
1 | package com.hanhuy.android.protify
2 |
3 | import android.app.Activity
4 | import android.content.{Intent, Context}
5 | import android.content.res.{Configuration, AssetManager, Resources}
6 | import android.os.Bundle
7 | import android.support.v7.app.AppCompatActivity
8 | import android.util.{TypedValue, AttributeSet, DisplayMetrics}
9 | import android.view.LayoutInflater
10 |
11 | import android.widget.Toast
12 | import com.android.debug.hv.ViewServer
13 | import com.hanhuy.android.common.Logcat
14 |
15 | /**
16 | * @author pfnguyen
17 | */
18 | object LayoutArguments {
19 | var resources = Option.empty[String]
20 | var layout = Option.empty[Int]
21 | var theme = Option.empty[Int]
22 | var appcompat = false
23 | }
24 |
25 | object LayoutActivity {
26 | val log = Logcat("LayoutActivity")
27 | def start(ctx: Context) = {
28 | if (LayoutArguments.appcompat)
29 | log.v("Launching AppCompatLayoutActivity")
30 |
31 | val intent = new Intent(ctx, if (LayoutArguments.appcompat)
32 | classOf[AppCompatLayoutActivity] else classOf[LayoutActivity])
33 | intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
34 | intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
35 | intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
36 | intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
37 | intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
38 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
39 | ctx.startActivity(intent)
40 | }
41 | }
42 |
43 | trait LayoutActivityArguments extends Activity with ExternalResourceLoader {
44 | def resPath = LayoutArguments.resources
45 | def layoutRes = LayoutArguments.layout
46 |
47 | var lastTheme = Option.empty[Int]
48 |
49 | override def onNewIntent(intent: Intent) = {
50 | super.onNewIntent(intent)
51 | log.v("Re-launching")
52 | recreate()
53 | }
54 |
55 | override def onCreate(savedInstanceState: Bundle) = {
56 | LayoutArguments.theme foreach setTheme
57 | super.onCreate(savedInstanceState)
58 | log.v("Activity starting: " + System.currentTimeMillis)
59 | if (layoutRes.isEmpty) {
60 | Toast.makeText(this, "No resources to render", Toast.LENGTH_LONG).show()
61 | log.v("No resources found", new Exception("No resources"))
62 | finish()
63 | }
64 | layoutRes foreach { layout =>
65 | try {
66 | log.v("Before content set: " + System.currentTimeMillis)
67 | lastTheme = LayoutArguments.theme
68 | setContentView(layout)
69 | log.v("Content set: " + System.currentTimeMillis)
70 | } catch { case e: Exception =>
71 | val res = resources
72 | log.w(f"Unable to load requested layout 0x$layout%08x", e)
73 | Toast.makeText(this, f"Unable to load requested layout 0x$layout%08x: " + e.getMessage, Toast.LENGTH_LONG).show()
74 | finish()
75 | }
76 | }
77 | }
78 | }
79 | trait ExternalResourceLoader extends Activity {
80 | val log = Logcat("ExternalResourceLoader")
81 |
82 | private type PrivateAssetManager = {
83 | def addAssetPath(s: java.lang.String): Unit
84 | }
85 |
86 | def resPath: Option[String]
87 |
88 | private[this] var resourcesCache = Option.empty[Resources]
89 |
90 | def resources = resourcesCache getOrElse {
91 | val oldres = super.getResources
92 | resPath match {
93 | case Some(res) =>
94 | val f = new java.io.File(res)
95 | if (!f.exists) {
96 | log.w("Resources file does not exist: " + f)
97 | oldres
98 | } else {
99 | val am = classOf[AssetManager].newInstance
100 | am.asInstanceOf[PrivateAssetManager].addAssetPath(f.getAbsolutePath)
101 | log.v("Loaded resources from: " + res)
102 | val r = new ResourcesWrapper(am, oldres.getDisplayMetrics, oldres.getConfiguration, oldres)
103 | resourcesCache = Some(r)
104 | r
105 | }
106 | case None =>
107 | oldres
108 | }
109 | }
110 |
111 | override def getResources = if (resPath.isEmpty) {
112 | log.w("No resources available yet", new Exception("not ready"))
113 | super.getResources
114 | } else resources
115 | override def getLayoutInflater = LayoutInflater.from(this)
116 | }
117 | // currently does not work at all due to mismatch of R and injected resources
118 | // custom themes that do not depend on AppCompat /may/ work
119 | class AppCompatLayoutActivity extends AppCompatActivity with LayoutActivityArguments with ViewServerSupport
120 | class LayoutActivity extends Activity with LayoutActivityArguments with ViewServerSupport
121 |
122 | class ResourcesWrapper(am: AssetManager, dm: DisplayMetrics, c: Configuration, res: Resources) extends Resources(am, dm, c) {
123 | override def getIntArray(id: Int) = try {
124 | super.getIntArray(id)
125 | } catch {
126 | case e: Resources.NotFoundException => res.getIntArray(id)
127 | }
128 |
129 | override def getValue(id: Int, outValue: TypedValue, resolveRefs: Boolean) = try {
130 | super.getValue(id, outValue, resolveRefs)
131 | } catch {
132 | case e: Resources.NotFoundException => res.getValue(id, outValue, resolveRefs)
133 | }
134 |
135 | override def getValue(name: String, outValue: TypedValue, resolveRefs: Boolean) = try {
136 | super.getValue(name, outValue, resolveRefs)
137 | } catch {
138 | case e: Resources.NotFoundException => res.getValue(name, outValue, resolveRefs)
139 | }
140 |
141 | override def openRawResource(id: Int) = try {
142 | super.openRawResource(id)
143 | } catch {
144 | case e: Resources.NotFoundException => res.openRawResource(id)
145 | }
146 |
147 | override def openRawResource(id: Int, value: TypedValue) = try {
148 | super.openRawResource(id, value)
149 | } catch {
150 | case e: Resources.NotFoundException => res.openRawResource(id, value)
151 | }
152 |
153 | override def getDimensionPixelOffset(id: Int) = try {
154 | super.getDimensionPixelOffset(id)
155 | } catch {
156 | case e: Resources.NotFoundException => res.getDimensionPixelOffset(id)
157 | }
158 |
159 | override def getDimension(id: Int) = try {
160 | super.getDimension(id)
161 | } catch {
162 | case e: Resources.NotFoundException => res.getDimension(id)
163 | }
164 |
165 | override def getLayout(id: Int) = try {
166 | super.getLayout(id)
167 | } catch {
168 | case e: Resources.NotFoundException => res.getLayout(id)
169 | }
170 |
171 | override def openRawResourceFd(id: Int) = try {
172 | super.openRawResourceFd(id)
173 | } catch {
174 | case e: Resources.NotFoundException => res.openRawResourceFd(id)
175 | }
176 |
177 | override def getDimensionPixelSize(id: Int) = try {
178 | super.getDimensionPixelSize(id)
179 | } catch {
180 | case e: Resources.NotFoundException => res.getDimensionPixelSize(id)
181 | }
182 |
183 | override def getValueForDensity(id: Int, density: Int, outValue: TypedValue, resolveRefs: Boolean) = try {
184 | super.getValueForDensity(id, density, outValue, resolveRefs)
185 | } catch {
186 | case e: Resources.NotFoundException => res.getValueForDensity(id, density, outValue, resolveRefs)
187 | }
188 |
189 | override def getDrawable(id: Int) = try {
190 | super.getDrawable(id)
191 | } catch {
192 | case e: Resources.NotFoundException => res.getDrawable(id)
193 | }
194 |
195 | override def getDrawable(id: Int, theme: Resources#Theme) = try {
196 | super.getDrawable(id, theme)
197 | } catch {
198 | case e: Resources.NotFoundException => res.getDrawable(id, theme)
199 | }
200 |
201 | override def getResourceEntryName(resid: Int) = try {
202 | super.getResourceEntryName(resid)
203 | } catch {
204 | case e: Resources.NotFoundException => res.getResourceEntryName(resid)
205 | }
206 |
207 | override def parseBundleExtra(tagName: String, attrs: AttributeSet, outBundle: Bundle) = try {
208 | super.parseBundleExtra(tagName, attrs, outBundle)
209 | } catch {
210 | case e: Resources.NotFoundException => res.parseBundleExtra(tagName, attrs, outBundle)
211 | }
212 |
213 | override def getResourceTypeName(resid: Int) = try {
214 | super.getResourceTypeName(resid)
215 | } catch {
216 | case e: Resources.NotFoundException => res.getResourceTypeName(resid)
217 | }
218 |
219 | override def getMovie(id: Int) = try {
220 | super.getMovie(id)
221 | } catch {
222 | case e: Resources.NotFoundException => res.getMovie(id)
223 | }
224 |
225 | override def getColor(id: Int) = try {
226 | super.getColor(id)
227 | } catch {
228 | case e: Resources.NotFoundException => res.getColor(id)
229 | }
230 |
231 | override def getBoolean(id: Int) = try {
232 | super.getBoolean(id)
233 | } catch {
234 | case e: Resources.NotFoundException => res.getBoolean(id)
235 | }
236 |
237 | override def getFraction(id: Int, base: Int, pbase: Int) = try {
238 | super.getFraction(id, base, pbase)
239 | } catch {
240 | case e: Resources.NotFoundException => res.getFraction(id, base, pbase)
241 | }
242 |
243 | override def getStringArray(id: Int) = try {
244 | super.getStringArray(id)
245 | } catch {
246 | case e: Resources.NotFoundException => res.getStringArray(id)
247 | }
248 |
249 | override def getResourceName(resid: Int) = try {
250 | super.getResourceName(resid)
251 | } catch {
252 | case e: Resources.NotFoundException => res.getResourceName(resid)
253 | }
254 |
255 | override def getQuantityString(id: Int, quantity: Int, formatArgs: AnyRef*) = try {
256 | super.getQuantityString(id, quantity, formatArgs: _*)
257 | } catch {
258 | case e: Resources.NotFoundException => res.getQuantityString(id, quantity, formatArgs: _*)
259 | }
260 |
261 | override def getQuantityString(id: Int, quantity: Int) = try {
262 | super.getQuantityString(id, quantity)
263 | } catch {
264 | case e: Resources.NotFoundException => res.getQuantityString(id, quantity)
265 | }
266 |
267 | override def getResourcePackageName(resid: Int) = try {
268 | super.getResourcePackageName(resid)
269 | } catch {
270 | case e: Resources.NotFoundException => res.getResourcePackageName(resid)
271 | }
272 |
273 | override def getXml(id: Int) = try {
274 | super.getXml(id)
275 | } catch {
276 | case e: Resources.NotFoundException => res.getXml(id)
277 | }
278 |
279 | override def getInteger(id: Int) = try {
280 | super.getInteger(id)
281 | } catch {
282 | case e: Resources.NotFoundException => res.getInteger(id)
283 | }
284 |
285 | override def getColorStateList(id: Int) = try {
286 | super.getColorStateList(id)
287 | } catch {
288 | case e: Resources.NotFoundException => res.getColorStateList(id)
289 | }
290 |
291 | override def getAnimation(id: Int) = try {
292 | super.getAnimation(id)
293 | } catch {
294 | case e: Resources.NotFoundException => res.getAnimation(id)
295 | }
296 |
297 | override def obtainAttributes(set: AttributeSet, attrs: Array[Int]) = try {
298 | super.obtainAttributes(set, attrs)
299 | } catch {
300 | case e: Resources.NotFoundException => res.obtainAttributes(set, attrs)
301 | }
302 |
303 | override def obtainTypedArray(id: Int) = try {
304 | super.obtainTypedArray(id)
305 | } catch {
306 | case e: Resources.NotFoundException => res.obtainTypedArray(id)
307 | }
308 |
309 | override def getText(id: Int) = try {
310 | super.getText(id)
311 | } catch {
312 | case e: Resources.NotFoundException => res.getText(id)
313 | }
314 |
315 | override def getText(id: Int, `def`: CharSequence) = try {
316 | super.getText(id, `def`)
317 | } catch {
318 | case e: Resources.NotFoundException => res.getText(id, `def`)
319 | }
320 |
321 | override def getIdentifier(name: String, defType: String, defPackage: String) = try {
322 | super.getIdentifier(name, defType, defPackage)
323 | } catch {
324 | case e: Resources.NotFoundException => res.getIdentifier(name, defType, defPackage)
325 | }
326 |
327 | override def getTextArray(id: Int) = try {
328 | super.getTextArray(id)
329 | } catch {
330 | case e: Resources.NotFoundException => res.getTextArray(id)
331 | }
332 |
333 | override def getQuantityText(id: Int, quantity: Int) = try {
334 | super.getQuantityText(id, quantity)
335 | } catch {
336 | case e: Resources.NotFoundException => res.getQuantityText(id, quantity)
337 | }
338 |
339 | override def getString(id: Int) = try{
340 | super.getString(id)
341 | } catch {
342 | case e: Resources.NotFoundException => res.getString(id)
343 | }
344 |
345 | override def getString(id: Int, formatArgs: AnyRef*) = try {
346 | super.getString(id, formatArgs: _*)
347 | } catch {
348 | case e: Resources.NotFoundException => res.getString(id, formatArgs: _*)
349 | }
350 |
351 | override def getDrawableForDensity(id: Int, density: Int) = try {
352 | super.getDrawableForDensity(id, density)
353 | } catch {
354 | case e: Resources.NotFoundException => res.getDrawableForDensity(id, density)
355 | }
356 |
357 | override def getDrawableForDensity(id: Int, density: Int, theme: Resources#Theme) = try {
358 | super.getDrawableForDensity(id, density, theme)
359 | } catch {
360 | case e: Resources.NotFoundException => res.getDrawableForDensity(id, density, theme)
361 | }
362 | }
363 |
364 | trait ViewServerSupport extends Activity {
365 | override def onCreate(savedInstanceState: Bundle) = {
366 | super.onCreate(savedInstanceState)
367 | ViewServer.get(this).addWindow(this)
368 | }
369 |
370 | override def onDestroy() = {
371 | super.onDestroy()
372 | ViewServer.get(this).removeWindow(this)
373 | }
374 |
375 | override def onResume() = {
376 | super.onResume()
377 | ViewServer.get(this).setFocusedWindow(this)
378 | }
379 | }
380 |
--------------------------------------------------------------------------------
/android/src/main/scala/com/hanhuy/android/protify/MainActivity.scala:
--------------------------------------------------------------------------------
1 | package com.hanhuy.android.protify
2 |
3 | import android.app.Activity
4 | import android.os.Bundle
5 |
6 | class MainActivity extends Activity {
7 | override def onCreate(savedInstanceState: Bundle) = {
8 | super.onCreate(savedInstanceState)
9 | setContentView(R.layout.main)
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/android/src/main/scala/com/hanhuy/android/protify/receivers.scala:
--------------------------------------------------------------------------------
1 | package com.hanhuy.android.protify
2 |
3 | import java.io._
4 | import java.lang.reflect.Field
5 |
6 | import android.content.{Intent, Context, BroadcastReceiver}
7 | import com.hanhuy.android.common.Logcat
8 |
9 | import Intents._
10 | import com.hanhuy.android.common.ManagedResource.ResourceManager
11 |
12 | /**
13 | * @author pfnguyen
14 | */
15 | object Receivers {
16 | val log = Logcat("Receivers")
17 | lazy val rtxtloader = new RTxtLoader()
18 | }
19 | class LayoutReceiver extends BroadcastReceiver {
20 | import Receivers._
21 | override def onReceive(context: Context, intent: Intent) = {
22 | for {
23 | intent <- Option(intent)
24 | action <- Option(intent.getAction) if action == LAYOUT_INTENT
25 | extras <- Option(intent.getExtras)
26 | resources <- Option(extras.getString(EXTRA_RESOURCES))
27 | layout = extras.getInt(EXTRA_LAYOUT, -1) if layout != -1
28 | } {
29 | val theme = extras.getInt(EXTRA_THEME, 0)
30 |
31 | LayoutArguments.resources = Some(resources)
32 | LayoutArguments.layout = Some(layout)
33 | LayoutArguments.theme = if (theme == 0) None else Some(theme)
34 | LayoutArguments.appcompat = extras.getBoolean(EXTRA_APPCOMPAT, false)
35 |
36 | log.v("Loading R.txt " + System.currentTimeMillis)
37 | rtxtloader.load(extras.getString(EXTRA_RTXT), extras.getString(EXTRA_RTXT_HASH))
38 | log.v("Done loading R.txt " + System.currentTimeMillis)
39 |
40 | LayoutActivity.start(context)
41 | }
42 | }
43 | }
44 |
45 | class DexReceiver extends BroadcastReceiver {
46 | import Receivers._
47 | override def onReceive(context: Context, intent: Intent) = {
48 | log.v("Received intent: " + intent)
49 | for {
50 | intent <- Option(intent)
51 | action <- Option(intent.getAction) if action == DEX_INTENT
52 | extras <- Option(intent.getExtras)
53 | resources <- Option(extras.getString(EXTRA_RESOURCES))
54 | dex <- Option(extras.getString(EXTRA_DEX))
55 | cls <- Option(extras.getString(EXTRA_CLASS))
56 | } {
57 | log.v("Launching DexActivity " + System.currentTimeMillis)
58 | DexArguments.resources = resources
59 | DexArguments.appcompat = extras.getBoolean(EXTRA_APPCOMPAT, false)
60 | DexArguments.dex = dex
61 | DexArguments.proxy = cls
62 |
63 | log.v("Loading R.txt " + System.currentTimeMillis)
64 | rtxtloader.load(extras.getString(EXTRA_RTXT), extras.getString(EXTRA_RTXT_HASH))
65 | log.v("Done loading R.txt " + System.currentTimeMillis)
66 |
67 | DexActivity.start(context)
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/build.sbt:
--------------------------------------------------------------------------------
1 | import java.io.BufferedReader
2 |
3 | import bintray.Keys._
4 |
5 | import scala.annotation.tailrec
6 |
7 | // val desktop = project.in(file("desktop"))
8 |
9 | val rtxtGenerator = TaskKey[Seq[File]]("rtxt-generator")
10 | val buildInfoGenerator = TaskKey[Seq[File]]("build-info-generator")
11 |
12 | val common = project.in(file("common")).settings(
13 | crossPaths := false,
14 | autoScalaLibrary := false,
15 | organization := "com.hanhuy.android",
16 | name := "protify-common",
17 | javacOptions in Compile ++= "-target" :: "1.7" :: "-source" :: "1.7" :: Nil,
18 | exportJars := true,
19 | sourceGenerators in Compile <+= buildInfoGenerator,
20 | buildInfoGenerator := {
21 | val dest = (sourceManaged in Compile).value / "BuildInfo.java"
22 | val info =
23 | s"""
24 | |package com.hanhuy.android.protify;
25 | |public class BuildInfo {
26 | | public static String version = "${version.value}";
27 | | public static String name = "${name.value}";
28 | |}
29 | """.stripMargin
30 | IO.writeLines(dest, info :: Nil)
31 | dest :: Nil
32 | }
33 | )
34 |
35 | lazy val agent = project.in(file("agent")).settings(androidBuildAar).settings(
36 | platformTarget in Android := "android-19",
37 | mappings in (Compile, packageBin) ++= (mappings in (Compile, packageBin) in common).value,
38 | javacOptions in (Compile,doc) ~= {
39 | _.foldRight(List.empty[String]) { (x, a) =>
40 | if ("-bootclasspath" == x) {
41 | import java.io.File._
42 | x :: (System.getProperty("java.home") + separator + "lib" + separator + "rt.jar" + pathSeparator + a.head) :: a.tail
43 | } else if ("-target" == x) {
44 | a.tail
45 | }
46 | else x :: a
47 | }
48 | },
49 | autoScalaLibrary := false,
50 | organization := "com.hanhuy.android",
51 | packageForR := "com.hanhuy.android.protify.agent",
52 | libraryDependencies += "com.android.support" % "support-annotations" % "25.2.0" % "compile-internal",
53 | name := "protify-agent",
54 | javacOptions in Compile ++= "-target" :: "1.7" :: "-source" :: "1.7" :: Nil
55 | ) dependsOn(common % "compile-internal")
56 |
57 | val plugin = project.in(file("sbt-plugin")).settings(
58 | bintrayPublishSettings ++ scriptedSettings ++
59 | addSbtPlugin("org.scala-android" % "sbt-android" % "1.8.0-SNAPSHOT")
60 | ).settings(
61 | name := "sbt-android-protify",
62 | organization := "org.scala-android",
63 | scalacOptions ++= Seq("-deprecation","-Xlint","-feature", "-unchecked"),
64 | sbtPlugin := true,
65 | repository in bintray := "sbt-plugins",
66 | publishMavenStyle := false,
67 | licenses += ("MIT", url("http://opensource.org/licenses/MIT")),
68 | scriptedLaunchOpts ++= Seq("-Xmx1024m",
69 | "-Dplugin.version=" + version.value
70 | ),
71 | bintrayOrganization in bintray := None,
72 | libraryDependencies += "com.hanhuy.sbt" %% "bintray-update-checker" % "0.2",
73 | libraryDependencies += "com.google.code.findbugs" % "jsr305" % "3.0.1" % "compile-internal",
74 | mappings in (Compile, packageBin) ++= (mappings in (Compile, packageBin) in common).value,
75 | mappings in (Compile, packageBin) += (packageAar in agent).value -> "protify-agent.aar"
76 | ).dependsOn(common % "compile-internal")
77 |
78 | val lib = project.in(file("lib")).settings(androidBuildJar).settings(
79 | platformTarget in Android := "android-15",
80 | autoScalaLibrary := false,
81 | organization := "org.scala-android",
82 | name := "protify",
83 | publishMavenStyle := true,
84 | javacOptions in Compile ++= "-target" :: "1.7" :: "-source" :: "1.7" :: Nil,
85 | publishTo := {
86 | val nexus = "https://oss.sonatype.org/"
87 | if (isSnapshot.value)
88 | Some("snapshots" at nexus + "content/repositories/snapshots")
89 | else
90 | Some("releases" at nexus + "service/local/staging/deploy/maven2")
91 | },
92 | pomIncludeRepository := { _ => false },
93 | pomExtra :=
94 |
95 | git@github.com:pfn/protify.git
96 | scm:git:git@github.com:pfn/protify.git
97 |
98 |
99 |
100 | pfnguyen
101 | Perry Nguyen
102 | https://github.com/pfn
103 |
104 | ,
105 | licenses := Seq("BSD-style" -> url("http://www.opensource.org/licenses/bsd-license.php")),
106 | homepage := Some(url("https://github.com/pfn/protify"))
107 | )
108 |
109 | /*
110 | val mobile = project.in(file("android")).settings(androidBuild).settings(
111 | platformTarget in Android := "android-22",
112 | versionName in Android := Some(version.value),
113 | versionCode in Android := Some(1),
114 | minSdkVersion in Android := "16",
115 | targetSdkVersion in Android := "22",
116 | debugIncludesTests in Android := false,
117 | scalaVersion := "2.11.7",
118 | javacOptions in Compile ++= "-target" :: "1.7" :: "-source" :: "1.7" :: Nil,
119 | sourceGenerators in Compile <+= rtxtGenerator,
120 | rtxtGenerator <<= Def.task {
121 | val layout = (projectLayout in Android).value
122 | val rtxt = layout.gen / "R.txt"
123 | val aars = target.value / "aars"
124 | val librtxt = aars ** "R.txt" get
125 | val appcompatR = librtxt.filter(_.getParentFile.getName.startsWith("com.android.support-appcompat-v7")).head
126 | val designR = librtxt.filter(_.getParentFile.getName.startsWith("com.android.support-design")).head
127 | def collectConst(in: BufferedReader) = {
128 | IO.foldLines(in, Map.empty[String,Set[String]]) { (m, line) =>
129 | val parts = line.split(" ")
130 | val clazz = parts(1)
131 | val name = parts(2)
132 | m + ((clazz, m.getOrElse(clazz, Set.empty) + name))
133 | }
134 | }
135 | val appcompatConst = Using.fileReader(IO.utf8)(appcompatR)(collectConst)
136 | val designConst = Using.fileReader(IO.utf8)(designR)(collectConst)
137 |
138 | val rloader = layout.gen / "com" / "hanhuy" / "android" / "protify" / "RTxtLoader.java"
139 | val intTemplate =
140 | """
141 | | case "%s:%s":
142 | | %s
143 | | %s
144 | | break;
145 | """.stripMargin
146 | val arrayTemplate =
147 | """
148 | | case "%s:%s":
149 | | %s
150 | | %s
151 | | break;
152 | """.stripMargin
153 | val template =
154 | """
155 | |package com.hanhuy.android.protify;
156 | |public class RTxtLoader extends RTxtLoaderBase {
157 | | @Override public void setInt(String clazz, String name, int value) {
158 | | switch (clazz + ":" + name) {
159 | |%s
160 | | }
161 | | }
162 | | @Override public void setIntArray(String clazz, String name, int[] value) {
163 | | switch (clazz + ":" + name) {
164 | |%s
165 | | }
166 | | }
167 | |}
168 | """.stripMargin
169 | if (!rtxt.isFile) android.Plugin.fail("R.txt does not exist yet")
170 | val (vals, arys) = Using.fileReader(IO.utf8)(rtxt) { in =>
171 | IO.foldLines(in, (List.empty[String],List.empty[String])) { case ((values, arrays), line) =>
172 | val parts = line.split(" ")
173 | parts(0) match {
174 | case "int" =>
175 | val clazz = parts(1)
176 | val name = parts(2)
177 | val c = intTemplate format (clazz, name,
178 | if (designConst.getOrElse(clazz, Set.empty)(name)) s"android.support.design.R.$clazz.$name = value;" else "",
179 | if (appcompatConst.getOrElse(clazz, Set.empty)(name)) s"android.support.v7.appcompat.R.$clazz.$name = value;" else "")
180 | (c :: values, arrays)
181 | case "int[]" =>
182 | val clazz = parts(1)
183 | val name = parts(2)
184 | val c = arrayTemplate format (clazz, name,
185 | if (designConst.getOrElse(clazz, Set.empty)(name)) s"android.support.design.R.$clazz.$name = value;" else "",
186 | if (appcompatConst.getOrElse(clazz, Set.empty)(name)) s"android.support.v7.appcompat.R.$clazz.$name = value;" else "")
187 | (values, c :: arrays)
188 | }
189 | }
190 | }
191 | IO.writeLines(rloader,
192 | template.format(vals.mkString, arys.mkString) :: Nil)
193 | Seq(rloader)
194 | } dependsOn (rGenerator in Android),
195 | rGenerator in Android := {
196 | val r = (rGenerator in Android).value
197 |
198 | def exists(in: BufferedReader)(f: String => Boolean): Boolean = {
199 | @tailrec
200 | def readLine(accum: Boolean): Boolean = {
201 | val line = in.readLine()
202 | if (accum || (line eq null)) accum else readLine(f(line))
203 | }
204 | readLine(false)
205 | }
206 |
207 | val supportR = r filter {
208 | Using.fileReader(IO.utf8)(_) { in =>
209 | exists(in)(_ contains "package android.support")
210 | }
211 | }
212 | supportR foreach { s =>
213 | val lines = IO.readLines(s)
214 | IO.writeLines(s, lines map (_.replaceAll(" final ", " ")))
215 | }
216 | r
217 | },
218 | proguardOptions in Android ++=
219 | "-keep class scala.runtime.BoxesRunTime { *; }" :: // for debugging only
220 | "-keep class android.support.** { *; }" ::
221 | "-keep class com.hanhuy.android.protify.ActivityProxy {*;}" ::
222 | "-keep class com.hanhuy.android.protify.ActivityProxy$Simple {*;}" ::
223 | Nil,
224 | libraryDependencies ++=
225 | "com.hanhuy.android" %% "scala-common" % "1.0" ::
226 | "com.hanhuy.android" % "viewserver" % "1.0.3" ::
227 | "com.android.support" % "appcompat-v7" % "22.2.1" ::
228 | "com.android.support" % "design" % "22.2.1" ::
229 | Nil,
230 | manifestPlaceholders in Android := Map(
231 | "vmSafeMode" -> (apkbuildDebug in Android).value().toString
232 | )
233 | ).dependsOn(lib, common)
234 | */
235 |
236 | //Keys.`package` in Android <<= Keys.`package` in (mobile,Android)
237 |
238 | version in Global := "1.5.0-SNAPSHOT"
239 |
240 | pomExtra :=
241 |
242 | git@github.com:scala-android/sbt-android-protify.git
243 | scm:git:git@github.com:scala-android/sbt-android-protify.git
244 |
245 |
246 |
247 | pfnguyen
248 | Perry Nguyen
249 | https://github.com/pfn
250 |
251 |
252 |
--------------------------------------------------------------------------------
/common/src/main/java/com/hanhuy/android/protify/Intents.java:
--------------------------------------------------------------------------------
1 | package com.hanhuy.android.protify;
2 |
3 | public interface Intents {
4 | public final static String INSTALL_INTENT = "com.hanhuy.android.protify.action.INSTALL";
5 | public final static String CLEAN_INTENT = "com.hanhuy.android.protify.action.CLEAN";
6 | public final static String PROTIFY_INTENT = "com.hanhuy.android.protify.action.PROTIFY";
7 | public final static String DEX_INTENT = "com.hanhuy.android.protify.action.DEX";
8 | public final static String LAYOUT_INTENT = "com.hanhuy.android.protify.action.LAYOUT";
9 | public final static String EXTRA_LAYOUT = "com.hanhuy.android.protify.extra.LAYOUT";
10 | public final static String EXTRA_DEX = "com.hanhuy.android.protify.extra.DEX";
11 | public final static String EXTRA_CLASS = "com.hanhuy.android.protify.extra.CLASS";
12 | public final static String EXTRA_RESOURCES = "com.hanhuy.android.protify.extra.RESOURCES";
13 | public final static String EXTRA_THEME = "com.hanhuy.android.protify.extra.THEME";
14 | public final static String EXTRA_RTXT = "com.hanhuy.android.protify.extra.RTXT";
15 | public final static String EXTRA_RTXT_HASH = "com.hanhuy.android.protify.extra.RTXT_HASH";
16 | public final static String EXTRA_APPCOMPAT = "com.hanhuy.android.protify.extra.APPCOMPAT";
17 | public final static String EXTRA_DEX_INFO = "com.hanhuy.android.protify.extra.DEX_INFO";
18 | }
--------------------------------------------------------------------------------
/lib/src/main/java/com/hanhuy/android/protify/ActivityProxy.java:
--------------------------------------------------------------------------------
1 | package com.hanhuy.android.protify;
2 |
3 | import android.os.Bundle;
4 | import android.app.Activity;
5 | import android.view.Menu;
6 | import android.view.MenuItem;
7 |
8 | public interface ActivityProxy {
9 | public void onProxyLoad(Activity activity);
10 | public void onProxyUnload(Activity activity);
11 | public void onCreate(Activity activity, Bundle state);
12 | public void onDestroy(Activity activity);
13 | public void onPause(Activity activity);
14 | public void onResume(Activity activity);
15 | public void onStart(Activity activity);
16 | public void onStop(Activity activity);
17 | public void onCreateOptionsMenu(Activity activity, Menu menu);
18 | public boolean onOptionsItemSelected(Activity activity, MenuItem item);
19 |
20 | public static class Simple implements ActivityProxy {
21 | public void onProxyLoad(Activity activity) {}
22 | public void onProxyUnload(Activity activity) {}
23 | public void onCreate(Activity activity, Bundle state) {}
24 | public void onDestroy(Activity activity) {}
25 | public void onPause(Activity activity) {}
26 | public void onResume(Activity activity) {}
27 | public void onStart(Activity activity) {}
28 | public void onStop(Activity activity) {}
29 | public void onCreateOptionsMenu(Activity activity, Menu menu) {}
30 | public boolean onOptionsItemSelected(Activity activity, MenuItem item) {
31 | return false;
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=0.13.8
2 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("org.scala-android" % "sbt-android" % "1.7.7")
2 |
3 | resolvers += Resolver.url(
4 | "bintray-sbt-plugin-releases",
5 | url("https://dl.bintray.com/content/sbt/sbt-plugin-releases"))(
6 | Resolver.ivyStylePatterns)
7 |
8 | addSbtPlugin("me.lessis" % "bintray-sbt" % "0.1.2")
9 |
10 | libraryDependencies <+= sbtVersion ("org.scala-sbt" % "scripted-plugin" % _)
11 |
--------------------------------------------------------------------------------
/sbt-plugin/src/main/scala/plugin.scala:
--------------------------------------------------------------------------------
1 | package android.protify
2 |
3 | import java.io.File
4 | import java.net.URLEncoder
5 |
6 | import android.Keys.Internal._
7 | import android.{Aggregate, BuildOutput, Dex}
8 | import com.android.ddmlib.IDevice
9 | import com.hanhuy.sbt.bintray.UpdateChecker
10 | import sbt.Def.Initialize
11 | import sbt._
12 | import sbt.Keys._
13 | import android.Keys._
14 | import com.android.builder.packaging.PackagingUtils
15 | import sbt.classpath.ClasspathUtilities
16 | import xsbt.api.Discovery
17 |
18 | import scala.annotation.tailrec
19 | import sbt.Cache.seqFormat
20 | import sbt.Cache.StringFormat
21 | import sbt.Cache.IntFormat
22 | import sbt.Cache.tuple2Format
23 |
24 | import language.postfixOps
25 | import com.hanhuy.android.protify.BuildInfo
26 |
27 | import scala.util.Try
28 |
29 | object Keys {
30 | val protifyLayout = InputKey[Unit]("protify-layout", "prototype an android layout on device")
31 | val protify = TaskKey[Unit]("protify", "live-coding on-device")
32 | val Protify = config("protify") extend Compile
33 |
34 | @deprecated("use `enablePlugins(AndroidProtify)`", "1.4.0")
35 | def protifySettings: Seq[Setting[_]] = AndroidProtify.projectSettings
36 | }
37 | /**
38 | * @author pfnguyen
39 | */
40 | object AndroidProtify extends AutoPlugin {
41 | override def requires = android.AndroidApp
42 |
43 | val autoImport = Keys
44 |
45 | import android.protify.Keys._
46 | import Internal._
47 | override def projectSettings = List(
48 | clean <<= clean dependsOn (clean in Protify),
49 | streams in update <<= (streams in update) dependsOn (protifyLibraryDependencies in Protify, protifyExtractAgent in Protify),
50 | protify <<= protifyTaskDef dependsOn protifyHasDevice dependsOn protifyHasInstall,
51 | protifyLayout <<= protifyLayoutTaskDef(),
52 | protifyLayout <<= protifyLayout dependsOn (packageResources in Protify, compile in Compile)
53 | ) ++ inConfig(Protify)(List(
54 | clean <<= protifyCleanTaskDef,
55 | // because Keys.install and debug are implicitly 'in Android' (1.5.5+)
56 | install in Protify <<= protifyInstallTaskDef dependsOn protifyHasDevice dependsOn protifyHasInstall,
57 | debug in Protify <<= protifyRunTaskDef(true) dependsOn protifyHasDevice dependsOn protifyHasInstall,
58 | run <<= protifyRunTaskDef(false) dependsOn protifyHasDevice dependsOn protifyHasInstall,
59 | protifyHasDevice := {
60 | if (android.Commands.targetDevice((sdkPath in Android).value, streams.value.log).isEmpty)
61 | android.PluginFail("no device connected")
62 | },
63 | protifyHasInstall := {
64 | val all = allDevices.value
65 | val st = streams.value
66 | val sdk = sdkPath.value
67 | val layout = projectLayout.value
68 | implicit val output = outputLayout.value
69 |
70 | val execute = { (dev: IDevice) =>
71 | val installHash = layout.protifyInstalledHash(dev)
72 | if (!installHash.isFile)
73 | android.fail(s"Application has not been installed to ${dev.getSerialNumber}, android:install first")
74 | ()
75 | }
76 | if (all)
77 | android.Commands.deviceList(sdk, st.log).par foreach execute
78 | else
79 | android.Commands.targetDevice(sdk, st.log) foreach execute
80 | },
81 | protifyExtractAgent <<= protifyExtractAgentTaskDef,
82 | protifyDexAgent <<= protifyDexAgentTaskDef,
83 | protifyDexJar <<= protifyDexJarTaskDef,
84 | protifyLibraryDependencies <<= stableLibraryDependencies,
85 | protifyPublicResources <<= protifyPublicResourcesTaskDef,
86 | protifyLayouts <<= protifyLayoutsTaskDef storeAs protifyLayouts triggeredBy (compile in Compile),
87 | protifyThemes <<= discoverThemes storeAs protifyThemes triggeredBy (compile in Compile),
88 | protifyLayoutsAndThemes <<= (protifyLayouts,protifyThemes) map ((_,_)) storeAs protifyLayoutsAndThemes triggeredBy (compile in Compile),
89 | javacOptions := (javacOptions in Compile).value,
90 | scalacOptions := (scalacOptions in Compile).value,
91 | sourceGenerators <<= sourceGenerators in Compile,
92 | packageResources in Protify := {
93 | val resapk = (packageResources in Android).value
94 | val layout = (projectLayout in Android).value
95 | implicit val out = (outputLayout in Android).value
96 | val s = streams.value
97 | FileFunction.cached(s.cacheDirectory / "protify-resapk",
98 | FilesInfo.hash, FilesInfo.exists) { in =>
99 | IO.delete(layout.protifyResApkTemp)
100 | layout.protifyResApkTemp.mkdirs()
101 | IO.unzip(resapk, layout.protifyResApkTemp)
102 | IO.delete(layout.protifyResApk)
103 | val mappings =
104 | ((PathFinder(layout.protifyResApkTemp) ** android.FileOnlyFilter)
105 | pair relativeTo(layout.protifyResApkTemp)) ++
106 | ((PathFinder(layout.mergedAssets) ** android.FileOnlyFilter)
107 | pair rebase(layout.mergedAssets, "assets"))
108 | Zip.resources(mappings, layout.protifyResApk)
109 | Set(layout.protifyResApk)
110 | }(Set(resapk) ++ (layout.mergedAssets ** android.FileOnlyFilter).get)
111 | layout.protifyResApk
112 | },
113 | unmanagedSourceDirectories :=
114 | (unmanagedSourceDirectories in Compile).value ++ {
115 | val layout = (projectLayout in Android).value
116 | val gradleLike = Seq(
117 | layout.base / "src" / "protify" / "scala",
118 | layout.base / "src" / "protify" / "java"
119 | )
120 | val antLike = Seq(
121 | layout.base / "protify"
122 | )
123 | @tailrec
124 | def sourcesFor(p: ProjectLayout): Seq[File] = p match {
125 | case g: android.ProjectLayout.Gradle => gradleLike
126 | case a: android.ProjectLayout.Ant => antLike
127 | case w: android.ProjectLayout.Wrapped => sourcesFor(w.wrapped)
128 | }
129 | sourcesFor(layout)
130 | }
131 | ) ++ inConfig(Runtime)(Seq(
132 | dependencyClasspath :=
133 | dependencyClasspath.value.filterNot(a =>
134 | a.data.getParentFile.getName == "localAAR-protify-agent.aar" ||
135 | a.data.getName.startsWith("localAAR-protify-agent"))
136 | )) ++ inConfig(Android)(List(
137 | useProguardInDebug := false,
138 | localAars += {
139 | val layout = (projectLayout in Android).value
140 | implicit val out = (outputLayout in Android).value
141 | layout.protifyAgentAar
142 | },
143 | dexLegacyMode := {
144 | val legacy = dexLegacyMode.value
145 | val debug = apkbuildDebug.value()
146 | !debug && legacy
147 | },
148 | dexShards := true,
149 | dexAggregate := {
150 | val opts = dexAggregate.value
151 | val debug = apkbuildDebug.value()
152 | val layout = (projectLayout in Android).value
153 | implicit val out = (outputLayout in Android).value
154 | if (!debug) {
155 | // always clean out dex on release builds (clear out shards/multi)
156 | (layout.dex * "*.dex" get) foreach (_.delete())
157 | opts
158 | } else opts.copy(multi = true, mainClassesConfig = file("/"))
159 | },
160 | proguardOptions := {
161 | val debug = apkbuildDebug.value()
162 | val options = proguardOptions.value
163 | if (debug) {
164 | List(
165 | "-keep class * extends android.app.Application { (...); }"
166 | ) ++ options
167 | } else
168 | options
169 | },
170 | proguardConfig := proguardConfig.value filterNot (
171 | _ contains " com.hanhuy.android.protify.agent"),
172 | managedClasspath <+= Def.task {
173 | // this hack is required because packageApk doesn't take resources from
174 | // application's classes.jar (or it would get lost during proguard,
175 | // retrolambda, etc.) Instead, generate a resource-only jar and add it
176 | // to the classpath where it will be included.
177 | val resPath = (resourceManaged in Protify).value
178 | implicit val output = (outputLayout in Android).value
179 | val layout = (projectLayout in Android).value
180 | val appInfoFile = appInfoDescriptor(resPath)
181 | val jarfile = layout.protifyDescriptorJar
182 |
183 | IO.jar(Seq(appInfoFile) pair flat, jarfile, new java.util.jar.Manifest)
184 | Attributed.blank(jarfile)
185 | } dependsOn processManifest,
186 | apkbuildAggregate <<= Def.taskDyn {
187 | val debug = apkbuildDebug.value()
188 | if (debug) Def.task {
189 | Aggregate.Apkbuild(packagingOptions.value,
190 | debug, apkDebugSigningConfig.value, processManifest.value, (protifyDexAgent in Protify).value, Nil,
191 | collectJni.value, resourceShrinker.value, minSdkVersion.value.toInt)
192 |
193 | } else Def.task {
194 | Aggregate.Apkbuild(packagingOptions.value,
195 | debug, apkDebugSigningConfig.value, processManifest.value, (dex in Android).value, predex.value,
196 | collectJni.value, resourceShrinker.value, minSdkVersion.value.toInt)
197 |
198 | }
199 | },
200 | apkbuild := {
201 | implicit val output = outputLayout.value
202 | val layout = projectLayout.value
203 | val a = apkbuildAggregate.value
204 | val n = name.value
205 | val u = (unmanagedJars in Compile).value
206 | val m = managedClasspath.value
207 | val dcp = (dependencyClasspath in Compile).value
208 | val dcpAg = m ++ u ++ dcp
209 | val dexjar = (protifyDexJar in Protify).value
210 | val s = streams.value
211 | val logger = ilogger.value(s.log)
212 | android.Packaging.apkbuild(
213 | builder.value(s.log),
214 | android.Packaging.Jars(if (a.apkbuildDebug) Seq(Attributed.blank(dexjar)) else Nil, Nil, dcpAg),
215 | libraryProject.value,
216 | a,
217 | ndkAbiFilter.value.toSet,
218 | layout.collectJni,
219 | layout.resources,
220 | layout.collectResource,
221 | layout.mergedAssets,
222 | layout.unsignedApk(a.apkbuildDebug, n),
223 | logger,
224 | s)
225 | },
226 | install := {
227 | val all = allDevices.value
228 | implicit val output = outputLayout.value
229 | val layout = projectLayout.value
230 | val pkg = applicationId.value
231 | val s = streams.value
232 | install.value
233 | def installed(d: IDevice): Unit = {
234 | IO.copyFile(layout.protifyDexHash, layout.protifyInstalledHash(d))
235 | }
236 |
237 | if (all) android.Commands.deviceList(sdkPath.value, s.log) foreach installed
238 | else android.Commands.targetDevice(sdkPath.value, s.log) foreach installed
239 | ()
240 | },
241 | processManifest := {
242 | if (libraryProject.value)
243 | android.fail("protifySettings cannot be applied to libraryProject")
244 | val processed = processManifest.value
245 | if (apkbuildDebug.value()) {
246 | val pkg = packageForR.value
247 | val appInfoFile = appInfoDescriptor((resourceManaged in Protify).value)
248 | import scala.xml._
249 | import scala.xml.transform._
250 | object ApplicationTransform extends RewriteRule {
251 |
252 | import android.Resources.ANDROID_NS
253 |
254 | override def transform(n: Node): Seq[Node] = n match {
255 | case Elem(prefix, "application", attribs, scope, children @ _*) =>
256 | val androidPrefix = scope.getPrefix(ANDROID_NS)
257 | val realApplication = attribs.get(ANDROID_NS, n, "name").fold(
258 | "android.app.Application") { nm =>
259 | val appName = nm.head.text
260 | if (appName.startsWith("."))
261 | pkg + appName
262 | else if (appName.contains("."))
263 | appName
264 | else
265 | pkg + "." + appName
266 | }
267 | IO.write(appInfoFile, realApplication)
268 | val attrs = attribs.filterNot(_.prefixedKey == s"$androidPrefix:name")
269 | val withNameAttr = new PrefixedAttribute(androidPrefix,
270 | "name", "com.hanhuy.android.protify.agent.ProtifyApplication",
271 | attrs.foldLeft(Null: MetaData)((a,b) => a.append(b)))
272 | // ugh, need to create elements instead of xml literals because
273 | // we want to allow non-'android' namespace prefixes
274 | val activityName = new PrefixedAttribute(androidPrefix,
275 | "name", "com.hanhuy.android.protify.agent.internal.ProtifyActivity", Null)
276 | val activityExported = new PrefixedAttribute(androidPrefix,
277 | "exported", "false", activityName)
278 | val activityTheme = new PrefixedAttribute(androidPrefix,
279 | "theme", "@style/InternalProtifyDialogTheme", activityExported)
280 | val activityE = new Elem(null, "activity", activityTheme, TopScope,
281 | minimizeEmpty = true)
282 |
283 | //
286 |
287 | import com.hanhuy.android.protify.Intents
288 | val actionName0 = new PrefixedAttribute(androidPrefix,
289 | "name", Intents.PROTIFY_INTENT, Null)
290 | val actionName1 = new PrefixedAttribute(androidPrefix,
291 | "name", Intents.CLEAN_INTENT, Null)
292 | val actionName2 = new PrefixedAttribute(androidPrefix,
293 | "name", Intents.INSTALL_INTENT, Null)
294 | val intentFilterAction0 = new Elem(null, "action", actionName0, TopScope, minimizeEmpty = true)
295 | val intentFilterAction1 = new Elem(null, "action", actionName1, TopScope, minimizeEmpty = true)
296 | val intentFilterAction2 = new Elem(null, "action", actionName2, TopScope, minimizeEmpty = true)
297 | val intentFilter = new Elem(null, "intent-filter", Null, TopScope, minimizeEmpty = true,
298 | intentFilterAction0, intentFilterAction1, intentFilterAction2)
299 | val receiverName = new PrefixedAttribute(androidPrefix,
300 | "name", "com.hanhuy.android.protify.agent.internal.ProtifyReceiver", Null)
301 | val receiverPermission = new PrefixedAttribute(androidPrefix,
302 | "permission", "android.permission.INSTALL_PACKAGES", receiverName)
303 | val receiverExported = new PrefixedAttribute(androidPrefix,
304 | "exported", "true", receiverPermission)
305 | val receiverE = new Elem(null, "receiver", receiverExported, TopScope,
306 | minimizeEmpty = true, intentFilter)
307 | //
310 | //
311 | //
312 | //
313 | //
314 | Elem(prefix, "application", withNameAttr, scope, true, children :+ activityE :+ receiverE:_*)
315 | case x => x
316 | }
317 | }
318 | val root = XML.loadFile(processed)
319 | XML.save(processed.getAbsolutePath,
320 | new RuleTransformer(ApplicationTransform).apply(root), "utf-8")
321 | }
322 | processed
323 | },
324 | collectResources <<= collectResources dependsOn (protifyPublicResources in Protify),
325 | cleanForR := {
326 | val ignores: Set[(Option[String],AttributeKey[_])] = Set(
327 | (None, protifyLayout.key),
328 | (None, protify.key),
329 | (Some(Protify.name), run.key),
330 | (Some(Protify.name), install.key)
331 | )
332 | implicit val o = outputLayout.value
333 | val l = projectLayout.value
334 | val d = (classDirectory in Compile).value
335 | val s = streams.value
336 |
337 | val roots = executionRoots.value map (r =>
338 | (r.scope.config.toOption.map(_.name),r.key))
339 | if (!roots.exists(ignores)) {
340 | FileFunction.cached(s.cacheDirectory / "clean-for-r",
341 | FilesInfo.hash, FilesInfo.exists) { in =>
342 | if (in.nonEmpty) {
343 | s.log.info("Rebuilding all classes because R.java has changed")
344 | IO.delete(d)
345 | }
346 | in
347 | }(Set(l.gen ** "R.java" get: _*))
348 | }
349 | Seq.empty[File]
350 | },
351 | cleanForR <<= cleanForR dependsOn rGenerator
352 | )))
353 |
354 | override def globalSettings = (onLoad := onLoad.value andThen { s =>
355 | Project.runTask(updateCheck in Keys.Protify, s).fold(s)(_._1)
356 | }) ::
357 | (updateCheck in Keys.Protify := {
358 | val log = streams.value.log
359 |
360 | UpdateChecker("pfn", "sbt-plugins", "sbt-android-protify") {
361 | case Left(t) =>
362 | log.debug("Failed to load version info: " + t)
363 | case Right((versions, current)) =>
364 | log.debug("available versions: " + versions)
365 | log.debug("current version: " + BuildInfo.version)
366 | log.debug("latest version: " + current)
367 | if (versions(BuildInfo.version)) {
368 | if (BuildInfo.version != current) {
369 | log.warn(
370 | s"UPDATE: A newer sbt-android-protify is available:" +
371 | s" $current, currently running: ${BuildInfo.version}")
372 | }
373 | }
374 | }
375 | }) ::
376 | Nil
377 |
378 | type ResourceId = (String,Int)
379 | private[android] object Internal {
380 | val protifyExtractAgent = TaskKey[Unit]("internal-protify-extract-agent", "internal key: extract embedded protify agent aar")
381 | val protifyLibraryDependencies = TaskKey[Unit]("internal-protify-check-dependencies", "internal key: make sure libraryDependencies are stable")
382 | val protifyDexAgent = TaskKey[File]("internal-protify-dex-agent", "internal key: dex protify-agent.jar")
383 | val protifyDexJar = TaskKey[File]("internal-protify-dex-jar", "internal key: create a jar containing all dexes")
384 | val protifyPublicResources = TaskKey[Unit]("internal-protify-public-resources", "internal key: generate public.xml from R.txt")
385 | val protifyLayouts = TaskKey[Seq[ResourceId]]("internal-protify-layouts", "internal key: autodetected layout files")
386 | val protifyThemes = TaskKey[(Seq[ResourceId],Seq[ResourceId])]("internal-protify-themes", "internal key: platform themes, app themes")
387 | val protifyLayoutsAndThemes = TaskKey[(Seq[ResourceId],(Seq[ResourceId],Seq[ResourceId]))]("internal-protify-layouts-and-themes", "internal key: themes and layouts")
388 | val protifyHasDevice = TaskKey[Unit]("internal-protify-has-device", "internal key: fail-fast when a device is not connected") in Protify
389 | val protifyHasInstall = TaskKey[Unit]("internal-protify-has-install", "internal key: fail-fast when selected devices do not have an instrumented apk installed") in Protify
390 | }
391 |
392 | private[this] def appInfoDescriptor(target: File) =
393 | target / "protify_application_info.txt"
394 |
395 | val protifyExtractAgentTaskDef = Def.task {
396 | val layout = (projectLayout in Android).value
397 | implicit val out = (outputLayout in Android).value
398 | val buflen = 32768
399 | val buf = Array.ofDim[Byte](buflen)
400 | val url = android.Resources.resourceUrl("protify-agent.aar")
401 | val uc = url.openConnection()
402 | val lastMod = uc.getLastModified
403 | uc.getInputStream.close()
404 | if (layout.protifyAgentAar.lastModified < lastMod) {
405 | Using.urlInputStream(url) { in =>
406 | Using.fileOutputStream(false)(layout.protifyAgentAar) { out =>
407 | Iterator.continually(in.read(buf, 0, buflen)).takeWhile(_ != -1) foreach (out.write(buf, 0, _))
408 | }
409 | }
410 | }
411 | }
412 |
413 | private val discoverThemes = Def.task {
414 | val androidJar = (platformJars in Android).value._1
415 | implicit val out = (outputLayout in Android).value
416 | val resPath = (projectLayout in Android).value.mergedRes
417 | val log = streams.value.log
418 | val cl = ClasspathUtilities.toLoader(file(androidJar))
419 | val style = cl.loadClass("android.R$style")
420 | type Theme = (String,String)
421 |
422 | val values = (resPath ** "values*" ** "*.xml").get
423 | import scala.xml._
424 | val allstyles = values flatMap { f =>
425 | val xml = XML.loadFile(f)
426 | (xml \ "style") map { n =>
427 | val nm = n.attribute("name").head.text.replace('.','_')
428 | val parent = n.attribute("parent").fold(nm.substring(0, nm.indexOf("_")))(_.text).replace('.','_')
429 | (nm,parent)
430 | }
431 | }
432 | val tree = allstyles.toMap
433 | @tailrec
434 | def isTheme(style: String): Boolean = {
435 | if (style startsWith "Theme_") true
436 | else if (tree.contains(style))
437 | isTheme(tree(style))
438 | else false
439 | }
440 | @tailrec
441 | def isAppCompatTheme(style: String): Boolean = {
442 | if (style startsWith "Theme_AppCompat") true
443 | else if (tree.contains(style))
444 | isAppCompatTheme(tree(style))
445 | else false
446 | }
447 |
448 | val pkg = (packageForR in Android).value
449 | val loader = ClasspathUtilities.toLoader((classDirectory in Compile).value)
450 | // TODO fix me, do not assume exists!
451 | val themes = Try(loader.loadClass(pkg + ".R$style")).toOption.fold(Seq.empty[ResourceId]) { clazz =>
452 | allstyles.map(_._1) filter isTheme flatMap { t =>
453 | try {
454 | val f = clazz.getDeclaredField(t)
455 | Seq((t, f.getInt(null)))
456 | } catch {
457 | case e: Exception =>
458 | log.warn(s"Unable to lookup field: $t, because ${e.getMessage}")
459 | Seq.empty
460 | }
461 | }
462 | }
463 | val appcompat = themes filter (t => isAppCompatTheme(t._1))
464 |
465 | // return (platform + all app themes, appcompat-only themes)
466 | ((style.getDeclaredFields filter (_.getName startsWith "Theme_") map { f =>
467 | (f.getName, f.getInt(null))
468 | } toSeq) ++ themes,appcompat)
469 | }
470 | private val protifyLayoutsTaskDef = Def.task {
471 | val pkg = (packageForR in Android).value
472 | val loader = ClasspathUtilities.toLoader((classDirectory in Compile).value)
473 | val clazz = loader.loadClass(pkg + ".R$layout")
474 | val fields = clazz.getDeclaredFields
475 | fields.map(f => f.getName -> f.getInt(null)).toSeq
476 | }
477 |
478 | def protifyLayoutTaskDef(): Initialize[InputTask[Unit]] = {
479 | val parser = loadForParser(protifyLayoutsAndThemes in Protify) { (s, stored) =>
480 | import sbt.complete.Parser
481 | import sbt.complete.DefaultParsers._
482 | val res = stored.getOrElse((Seq.empty[ResourceId],(Seq(("",0)),Seq.empty[ResourceId])))
483 | val layouts = res._1.map(_._1)
484 | val themes = res._2._1 map (t => token(t._1))
485 | EOF.map(_ => None) | (Space ~> Parser.opt(token(NotSpace examples layouts.toSet) ~ Parser.opt((Space ~> Parser.oneOf(themes)) <~ SpaceClass.*)))
486 | }
487 | Def.inputTask {
488 | val res = (packageResources in Protify).value
489 | val l = parser.parsed
490 | val log = streams.value.log
491 | val all = (allDevices in Android).value
492 | val sdk = (sdkPath in Android).value
493 | val layout = (projectLayout in Android).value
494 | val rTxt = layout.gen / "R.txt"
495 | val rTxtHash = if (rTxt.isFile) Hash.toHex(Hash(rTxt)) else "no-r.txt"
496 | val layouts = loadFromContext(protifyLayouts in Protify, sbt.Keys.resolvedScoped.value, state.value).getOrElse(Nil)
497 | val themes = loadFromContext(protifyThemes in Protify, sbt.Keys.resolvedScoped.value, state.value).getOrElse((Nil,Nil))
498 | if (layouts.isEmpty || themes._1.isEmpty) {
499 | android.fail("No layouts or themes cached, try again?")
500 | }
501 | if (l.isEmpty) {
502 | log.info("Previewing R.layout." + layouts.head._1)
503 | }
504 | val resid = l.fold(layouts.head._2)(r => layouts.toMap.apply(r._1))
505 | val appcompat = themes._2.toMap
506 | val theme = l.flatMap(_._2)
507 | val themeid = theme.fold(0)(themes._1.toMap.apply)
508 | log.debug("available layouts: " + layouts)
509 | import android.Commands
510 | import com.hanhuy.android.protify.Intents._
511 | val isAppcompat = theme.fold(false)(appcompat.contains)
512 | def execute(dev: IDevice): Unit = {
513 | val f = java.io.File.createTempFile("resources", ".ap_")
514 | val f2 = java.io.File.createTempFile("RES", ".txt")
515 | f.delete()
516 | f2.delete()
517 | val cmdS =
518 | "am" :: "broadcast" ::
519 | "-a" :: LAYOUT_INTENT ::
520 | "-e" :: EXTRA_RESOURCES :: s"/data/local/tmp/protify/${f.getName}" ::
521 | "-e" :: EXTRA_RTXT :: s"/data/local/tmp/protify/${f2.getName}" ::
522 | "-e" :: EXTRA_RTXT_HASH :: rTxtHash ::
523 | "--ez" :: EXTRA_APPCOMPAT :: isAppcompat ::
524 | "--ei" :: EXTRA_THEME :: themeid ::
525 | "--ei" :: EXTRA_LAYOUT :: resid ::
526 | "-n" ::
527 | "com.hanhuy.android.protify/.LayoutReceiver" ::
528 | Nil
529 |
530 | log.debug("Executing: " + cmdS.mkString(" "))
531 | dev.executeShellCommand("rm -r /data/local/tmp/protify/*", new Commands.ShellResult)
532 | android.Tasks.logRate(log, s"resources deployed to ${dev.getSerialNumber}:", res.length + rTxt.length) {
533 | dev.pushFile(res.getAbsolutePath, s"/data/local/tmp/protify/${f.getName}")
534 | if (rTxt.isFile)
535 | dev.pushFile(rTxt.getAbsolutePath, s"/data/local/tmp/protify/${f2.getName}")
536 | }
537 | dev.executeShellCommand(cmdS.mkString(" "), new Commands.ShellResult)
538 | }
539 | if (all)
540 | Commands.deviceList(sdk, log).par foreach execute
541 | else
542 | Commands.targetDevice(sdk, log) foreach execute
543 | }
544 | }
545 |
546 | private[this] def doInstall(intent: String,
547 | layout: ProjectLayout,
548 | pkg: String,
549 | res: File,
550 | dexfile: Seq[File],
551 | predexes: Seq[File],
552 | st: sbt.Keys.TaskStreams)(implicit m: ProjectLayout => BuildOutput): IDevice => Unit = {
553 | val dexfiles = dexfile.map(f => (f,f.getName)) ++ predexes.map { f =>
554 | val name = f.getParentFile.getName.dropRight(4) // ".jar"
555 | (f, name + ".dex")
556 | }
557 | val dexfileHashes = dexfiles map (f => (f._1, Hash.toHex(Hash(f._1)), f._2))
558 | val cacheDirectory = st.cacheDirectory / "protify"
559 | val log = st.log
560 | import com.hanhuy.android.protify.Intents._
561 |
562 | dev => {
563 | import java.io.File.createTempFile
564 |
565 | val installHash = layout.protifyInstalledHash(dev)
566 | if (!installHash.isFile)
567 | android.fail(s"Application has not been installed to ${dev.getSerialNumber}, android:install first")
568 | val installed = IO.readLines(installHash)
569 | val hashes = installed.map(_.split(":")(1)).toSet
570 | val topush = dexfileHashes.filterNot(d => hashes(d._2))
571 |
572 | val dexlist = topush map { case (p, _, n) =>
573 | val t = createTempFile("classes", ".dex")
574 | t.delete()
575 | (p, s"/data/local/tmp/protify/$pkg/${t.getName}", n)
576 | }
577 | val restmp = createTempFile("resources", ".ap_")
578 | restmp.delete()
579 | val dexinfo = createTempFile("dex-info", ".txt")
580 | dexinfo.deleteOnExit()
581 | val cmdS =
582 | "am" :: "broadcast" ::
583 | "-a" :: intent ::
584 | "-e" :: EXTRA_RESOURCES :: s"/data/local/tmp/protify/$pkg/${restmp.getName}" ::
585 | "-e" :: EXTRA_DEX_INFO :: s"/data/local/tmp/protify/$pkg/${dexinfo.getName}" ::
586 | "-n" ::
587 | s"$pkg/com.hanhuy.android.protify.agent.internal.ProtifyReceiver" ::
588 | Nil
589 |
590 |
591 | IO.write(dexinfo, dexlist.map(d => d._2 + ":" + d._3).mkString("\n"))
592 |
593 | log.debug("Executing: " + cmdS.mkString(" "))
594 |
595 | dev.executeShellCommand(s"rm -r /data/local/tmp/protify/$pkg/*", new android.Commands.ShellResult)
596 | var pushres = false
597 | val pushdex = dexlist.nonEmpty
598 | FileFunction.cached(cacheDirectory / dev.safeSerial / "res", FilesInfo.lastModified) { in =>
599 | pushres = true
600 | in
601 | }(Set(res))
602 | val pushlen = (if (pushres) res.length else 0) + (if (pushdex) topush.map(_._1.length).sum else 0)
603 | if (pushres || pushdex) {
604 | android.Tasks.logRate(log, s"code deployed to ${dev.getSerialNumber}:", pushlen) {
605 | if (pushres) {
606 | val resdest = s"/data/local/tmp/protify/$pkg/${restmp.getName}"
607 | log.debug(s"Pushing ${res.getAbsolutePath} to $resdest")
608 | log.info(s"Sending ${res.getName} (${android.Packaging.sizeString(res.length)})")
609 | dev.pushFile(res.getAbsolutePath, resdest)
610 | }
611 | if (pushdex) {
612 | dev.pushFile(dexinfo.getAbsolutePath, s"/data/local/tmp/protify/$pkg/${dexinfo.getName}")
613 | dexlist.foreach { case (d, p, n) =>
614 | log.debug(s"Pushing ${d.getAbsolutePath} to $p")
615 | log.info(s"Sending ${d.getName} (${android.Packaging.sizeString(d.length)})")
616 | dev.pushFile(d.getAbsolutePath, p)
617 | }
618 | }
619 | }
620 | dev.executeShellCommand(cmdS.mkString(" "), new android.Commands.ShellResult)
621 | }
622 | dexinfo.delete()
623 |
624 | val oldhashes = installed.map { i =>
625 | val split = i.split(":")
626 | (split(0),split(1))
627 | }.toMap
628 |
629 | val newhashes = topush.map { n =>
630 | (n._3,n._2)
631 | }.toMap
632 |
633 | IO.writeLines(layout.protifyInstalledHash(dev),
634 | oldhashes ++ newhashes map { case (k,v) => s"$k:$v" } toList)
635 | }
636 | }
637 |
638 | val protifyTaskDef = Def.task {
639 | val res = (packageResources in Protify).value
640 | val layout = (projectLayout in Android).value
641 | implicit val output = (outputLayout in Android).value
642 | val all = (allDevices in Android).value
643 | val sdk = (sdkPath in Android).value
644 | val pkg = (applicationId in Android).value
645 | val st = streams.value
646 | val dexfile = (dex in Android).value * "*.dex" get
647 | val predexes = (predex in Android).value flatMap (_._2 * "*.dex" get)
648 |
649 | import com.hanhuy.android.protify.Intents
650 | val execute = doInstall(Intents.PROTIFY_INTENT, layout, pkg, res, dexfile, predexes, st)
651 | import android.Commands
652 |
653 | if (all)
654 | Commands.deviceList(sdk, st.log).par foreach execute
655 | else
656 | Commands.targetDevice(sdk, st.log) foreach execute
657 | }
658 | val protifyInstallTaskDef = Def.task {
659 | val res = (packageResources in Protify).value
660 | val layout = (projectLayout in Android).value
661 | implicit val output = (outputLayout in Android).value
662 | val all = (allDevices in Android).value
663 | val sdk = (sdkPath in Android).value
664 | val pkg = (applicationId in Android).value
665 | val st = streams.value
666 | val dexfile = (dex in Android).value * "*.dex" get
667 | val predexes = (predex in Android).value flatMap (_._2 * "*.dex" get)
668 |
669 | import com.hanhuy.android.protify.Intents
670 | val execute = doInstall(Intents.INSTALL_INTENT, layout, pkg, res, dexfile, predexes, st)
671 | import android.Commands
672 |
673 | if (all)
674 | Commands.deviceList(sdk, st.log).par foreach execute
675 | else
676 | Commands.targetDevice(sdk, st.log) foreach execute
677 | }
678 | def protifyRunTaskDef(debug: Boolean): Def.Initialize[InputTask[Unit]] = Def.inputTask {
679 | val k = (sdkPath in Android).value
680 | val l = (projectLayout in Android).value
681 | val p = (applicationId in Android).value
682 | val s = streams.value
683 | val all = (allDevices in Android).value
684 | val isLib = (libraryProject in Android).value
685 | implicit val output = (outputLayout in Android).value
686 | if (isLib)
687 | android.fail("This project is not runnable, it has set 'libraryProject in Android := true")
688 |
689 | val manifestXml = l.processedManifest
690 | import scala.xml.XML
691 | import android.Resources.ANDROID_NS
692 | import android.Commands
693 | val m = XML.loadFile(manifestXml)
694 | // if an arg is specified, try to launch that
695 | android.parsers.activityParser.parsed orElse ((m \\ "activity") find {
696 | // runs the first-found activity
697 | a => (a \ "intent-filter") exists { filter =>
698 | val attrpath = "@{%s}name" format ANDROID_NS
699 | (filter \\ attrpath) exists (_.text == "android.intent.action.MAIN")
700 | }
701 | } map { activity =>
702 | val name = activity.attribute(ANDROID_NS, "name") get 0 text
703 |
704 | "%s/%s" format (p, if (name.indexOf(".") == -1) "." + name else name)
705 | }) match {
706 | case Some(intent) =>
707 | val receiver = new Commands.ShellLogging(l => s.log.info(l))
708 | val command = "am start %s -n %s" format (if (debug) "-D" else "", intent)
709 | def execute(d: IDevice): Unit = {
710 | s.log.info(s"Running on ${d.getProperty(IDevice.PROP_DEVICE_MODEL)} (${d.getSerialNumber})...")
711 | s.log.debug("Executing [%s]" format command)
712 | d.executeShellCommand(command, receiver)
713 | s.log.debug("run command executed")
714 | }
715 | if (all)
716 | Commands.deviceList(k, s.log).par foreach execute
717 | else
718 | Commands.targetDevice(k, s.log) foreach execute
719 | case None =>
720 | android.fail(
721 | "No activity found with action 'android.intent.action.MAIN'")
722 | }
723 |
724 | ()
725 | } dependsOn (install in Protify)
726 | val protifyCleanTaskDef = Def.task {
727 | if (apkbuildDebug.value()) {
728 | val st = streams.value
729 | val cacheDirectory = (streams in protify).value.cacheDirectory / "protify"
730 | val log = st.log
731 | val all = (allDevices in Android).value
732 | val sdk = (sdkPath in Android).value
733 | val pkg = (applicationId in Android).value
734 | implicit val out = (outputLayout in Android).value
735 | val layout = (projectLayout in Android).value
736 | layout.protifyPublicXml.delete()
737 | layout.rTxt.delete()
738 | import android.Commands
739 | import com.hanhuy.android.protify.Intents._
740 | def execute(dev: IDevice): Unit = {
741 | val cmdS =
742 | "am" :: "broadcast" ::
743 | "-a" :: CLEAN_INTENT ::
744 | "-n" ::
745 | s"$pkg/com.hanhuy.android.protify.agent.internal.ProtifyReceiver" ::
746 | Nil
747 |
748 | log.debug("Executing: " + cmdS.mkString(" "))
749 | dev.executeShellCommand(s"rm -r /data/local/tmp/protify/$pkg", new Commands.ShellResult)
750 | FileFunction.cached(cacheDirectory / dev.safeSerial / "res", FilesInfo.lastModified) { in =>
751 | Set.empty
752 | }(Set.empty)
753 | FileFunction.cached(cacheDirectory / dev.safeSerial / "dex", FilesInfo.lastModified) { in =>
754 | Set.empty
755 | }(Set.empty)
756 | dev.executeShellCommand(cmdS.mkString(" "), new Commands.ShellResult)
757 | IO.copyFile(layout.protifyDexHash, layout.protifyInstalledHash(dev))
758 | }
759 | Try {
760 | if (all)
761 | Commands.deviceList(sdk, log).par foreach execute
762 | else
763 | Commands.targetDevice(sdk, log) foreach execute
764 | }
765 | }
766 | ()
767 | }
768 | def discoverActivityProxies(analysis: inc.Analysis): Seq[String] =
769 | Discovery(Set("com.hanhuy.android.protify.ActivityProxy"), Set.empty)(Tests.allDefs(analysis)).collect({
770 | case (definition, discovered) if !definition.modifiers.isAbstract &&
771 | discovered.baseClasses("com.hanhuy.android.protify.ActivityProxy") =>
772 | definition.name }).sorted
773 |
774 | private val protifyDexAgentTaskDef = Def.task {
775 | implicit val out = (outputLayout in Android).value
776 | val layout = (projectLayout in Android).value
777 | val u = (unmanagedJars in Compile).value
778 | val agentJar = u.find(a =>
779 | a.data.getParentFile.getName == "localAAR-protify-agent.aar" ||
780 | a.data.getName.startsWith("localAAR-protify-agent")).get.data
781 | val bldr = (builder in Android).value
782 | val lib = (libraryProject in Android).value
783 | val bin = layout.protifyDexAgent
784 | val debug = (apkbuildDebug in Android).value()
785 | val s = streams.value
786 | bin.mkdirs()
787 |
788 | val f = File.createTempFile("fake-maindex", ".lst")
789 | f.deleteOnExit()
790 | val dexOpts = Aggregate.Dex(
791 | (false,agentJar :: Nil),
792 | (dexMaxHeap in Android).value,
793 | (dexMaxProcessCount in Android).value,
794 | true, // enable multidex so that dexInProcess works correctly
795 | f, // pass a bogus file for main dex list, unused
796 | (dexMinimizeMain in Android).value,
797 | (dexInProcess in Android).value,
798 | (buildToolInfo in Android).value,
799 | (dexAdditionalParams in Android).value)
800 | val d = Dex.dex(bldr(s.log), dexOpts, Nil, None, true, lib, bin, false, debug, s)
801 | f.delete()
802 | d
803 | }
804 | private val protifyDexJarTaskDef = Def.task {
805 | implicit val out = (outputLayout in Android).value
806 | val layout = (projectLayout in Android).value
807 |
808 | val dx = ((dex in Android).value * "*.dex" get) map { f =>
809 | (f, s"protify-dex/${f.getName}")
810 | }
811 | val enumRe = """classes(\d+).dex""".r
812 | val pd = (predex in Android).value.flatMap(_._2 * "*.dex" get) map { f =>
813 | val name = f.getParentFile.getName.dropRight(4) // ".jar"
814 | val ext = f.getName match {
815 | case enumRe(num) ⇒ s"_$num"
816 | case _ ⇒ ""
817 | }
818 | (f, s"protify-dex/$name$ext.dex")
819 | }
820 |
821 | // must check FilesInfo.hash because shardedDex copies into final location
822 | FileFunction.cached(streams.value.cacheDirectory / "protify-dex.jar", FilesInfo.hash) { in =>
823 | val prefixlen = "protify-dex/".length
824 | val hashes = (dx ++ pd) map { case (f, path) =>
825 | path.substring(prefixlen) + ":" + Hash.toHex(Hash(f))
826 | }
827 | IO.writeLines(layout.protifyDexHash, hashes)
828 | IO.jar(dx ++ pd, layout.protifyDexJar, new java.util.jar.Manifest)
829 | Set(layout.protifyDexJar, layout.protifyDexHash)
830 | }((dx ++ pd).map(_._1).toSet)
831 |
832 | layout.protifyDexJar
833 | }
834 |
835 | val stableLibraryDependencies = Def.taskDyn {
836 | val libcheckdir = streams.value.cacheDirectory / "protify-libcheck"
837 | val libcheck = (libcheckdir * "*").get.headOption.map(_.getName)
838 | val moduleHash = Hash.toHex(Hash((libraryDependencies in Scope(This,This,This,This)).value.mkString(";")))
839 | if (libcheck exists (_ != moduleHash)) Def.task {
840 | streams.value.log.warn("libraryDependencies have changed, forcing clean build")
841 | val _ = (clean in Compile).value
842 | } else Def.task {
843 | libcheckdir.mkdirs()
844 | IO.touch(libcheckdir / moduleHash)
845 | }
846 | }
847 | val protifyPublicResourcesTaskDef = Def.task {
848 | val tools = android.Keys.Internal.buildToolInfo.value
849 | if (tools.getRevision.getMajor < 24) {
850 | implicit val out = (outputLayout in Android).value
851 | val layout = (projectLayout in Android).value
852 | val rtxt = layout.gen / "R.txt"
853 | val public = layout.protifyPublicXml
854 | public.getParentFile.mkdirs()
855 | val idsfile = layout.protifyIdsXml
856 | idsfile.getParentFile.mkdirs()
857 | if (rtxt.isFile) {
858 | FileFunction.cached(streams.value.cacheDirectory / "public-xml", FilesInfo.hash) { in =>
859 | val values = (layout.mergedRes ** "values*" ** "*.xml").get
860 | import scala.xml._
861 | val allnames = values.flatMap { f =>
862 | if (f.getName == "public.xml") Nil
863 | else {
864 | val xml = XML.loadFile(f)
865 | xml.descendant flatMap { n =>
866 | n.attribute("name").map { a =>
867 | val nm = a.text
868 | (nm.replace('.', '_'), nm)
869 | }
870 | }
871 | }
872 | }.toMap
873 |
874 | streams.value.log.info("Maintaining resource ID consistency")
875 | val (publics, ids) = Using.fileReader(IO.utf8)(rtxt) { in =>
876 | IO.foldLines(in, (List("
"), List(""))) { case ((xs, ys), line) =>
877 | val parts = line.split(" ")
878 | val cls = parts(1)
879 | val nm = allnames.getOrElse(parts(2), parts(2)).trim
880 | val value = parts(3)
881 | if ("styleable" != cls)
882 | (if ("id" != cls || !nm.startsWith("Id.")) s""" """ :: xs else xs,
883 | if ("id" == cls && !nm.startsWith("Id.")) s""" """ :: ys else ys)
884 | else
885 | (xs, ys)
886 | }
887 | }
888 | if (publics.length > 1)
889 | IO.writeLines(public, """""" :: "" :: publics)
890 | else
891 | IO.delete(public)
892 | if (ids.length > 1)
893 | IO.writeLines(idsfile, """""" :: "" :: ids)
894 | else
895 | IO.delete(idsfile)
896 | Set(public, idsfile)
897 | }(Set(rtxt))
898 | }
899 | }
900 | ()
901 | }
902 |
903 | implicit class ProtifyLayout (val layout: ProjectLayout)(implicit m: ProjectLayout => BuildOutput) {
904 | def protify = layout.intermediates / "protify"
905 | def protifyDex = protify / "dex"
906 | def protifyDexAgent = protify / "agent"
907 | def protifyAgentAar = protify / "protify-agent.aar"
908 | def protifyDexJar = protify / "protify-dex.jar"
909 | def protifyDexHash = protify / "protify-dex-hash.txt"
910 | def protifyIdsXml = layout.generatedRes / "values" / "protify-ids.xml"
911 | def protifyPublicXml = layout.mergedRes / "values" / "protify-public.xml"
912 | def protifyInstalledHash(dev: IDevice) = {
913 | val path = protify / "installed" / dev.safeSerial
914 | path.getParentFile.mkdirs()
915 | path
916 | }
917 | def protifyResApkTemp = protify / "resapk-unpacked"
918 | def protifyResApk = protify / "protify-resources.ap_"
919 | def protifyAppInfoDescriptor = protify / "protify_application_info.txt"
920 | def protifyDescriptorJar = protify / "protify-descriptor.jar"
921 | }
922 | implicit class SafeIDevice (val device: IDevice) extends AnyVal {
923 | def safeSerial = URLEncoder.encode(device.getSerialNumber, "utf-8")
924 | }
925 |
926 | object Zip {
927 | // copied from IO.zip and its implementations
928 | import java.util.zip._
929 |
930 | def resources(sources: Traversable[(File, String)], outputZip: File): Unit =
931 | archive(sources.toSeq, outputZip)
932 |
933 | private def archive(sources: Seq[(File, String)], outputFile: File) {
934 | val noCompress = PackagingUtils.getDefaultNoCompressPredicate
935 | if (outputFile.isDirectory)
936 | sys.error("Specified output file " + outputFile + " is a directory.")
937 | else {
938 | val outputDir = outputFile.getParentFile
939 | IO.createDirectory(outputDir)
940 | withZipOutput(outputFile) { output =>
941 | val createEntry: (String => ZipEntry) = name => {
942 | val ze = new ZipEntry(name)
943 | ze.setMethod(if (noCompress.test(name)) ZipEntry.STORED else ZipEntry.DEFLATED)
944 | ze
945 | }
946 | writeZip(sources, output)(createEntry)
947 | }
948 | }
949 | }
950 | private def writeZip(sources: Seq[(File, String)], output: ZipOutputStream)(createEntry: String => ZipEntry) {
951 | val files = sources.flatMap { case (file, name) => if (file.isFile) (file, normalizeName(name)) :: Nil else Nil }
952 |
953 | val now = System.currentTimeMillis
954 | // The CRC32 for an empty value, needed to store directories in zip files
955 | val emptyCRC = new CRC32().getValue
956 |
957 | def makeFileEntry(file: File, name: String, crc: Long) = {
958 | // log.debug("\tAdding " + file + " as " + name + " ...")
959 | val e = createEntry(name)
960 | e setTime file.lastModified
961 | if (e.getMethod == ZipEntry.STORED) {
962 | e.setSize(file.length)
963 | e.setCrc(crc)
964 | }
965 | e
966 | }
967 | def addFileEntry(file: File, name: String) {
968 | val baos = new java.io.ByteArrayOutputStream(file.length.toInt)
969 | IO.transfer(file, baos)
970 | val bs = baos.toByteArray
971 | val crc = new CRC32()
972 | crc.update(bs)
973 |
974 | output putNextEntry makeFileEntry(file, name, crc.getValue)
975 | val bais = new java.io.ByteArrayInputStream(bs)
976 | IO.transfer(bais, output)
977 | output.closeEntry()
978 | }
979 |
980 | //Add all files to the generated Zip
981 | files foreach { case (file, name) => addFileEntry(file, name) }
982 | }
983 | private def withZipOutput(file: File)(f: ZipOutputStream => Unit) {
984 | Using.fileOutputStream(false)(file) { fileOut =>
985 | val (zipOut, ext) = (new ZipOutputStream(fileOut), "zip")
986 | try {
987 | f(zipOut)
988 | }
989 | finally {
990 | zipOut.close()
991 | }
992 | }
993 | }
994 | private def normalizeName(name: String) = {
995 | val sep = File.separatorChar
996 | if (sep == '/') name else name.replace(sep, '/')
997 | }
998 | }
999 | }
1000 |
--------------------------------------------------------------------------------
/sbt-plugin/src/sbt-test/protify/layout/build.sbt:
--------------------------------------------------------------------------------
1 | protifySettings
--------------------------------------------------------------------------------
/sbt-plugin/src/sbt-test/protify/layout/project.properties:
--------------------------------------------------------------------------------
1 | target=android-22
2 |
--------------------------------------------------------------------------------
/sbt-plugin/src/sbt-test/protify/layout/project/build.scala:
--------------------------------------------------------------------------------
1 | object Build extends android.AutoBuild
2 |
--------------------------------------------------------------------------------
/sbt-plugin/src/sbt-test/protify/layout/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | {
2 | val ver = System.getProperty("plugin.version")
3 | if (ver == null)
4 | throw new RuntimeException("""
5 | |The system property 'plugin.version' is not defined.
6 | |Specify this property using scriptedLaunchOpts -Dplugin.version."""
7 | .stripMargin)
8 | else addSbtPlugin("org.scala-android" % "sbt-android-protify" % ver)
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/sbt-plugin/src/sbt-test/protify/layout/src/androidTest/java/com/example/protify/MainActivityTest.java:
--------------------------------------------------------------------------------
1 | package com.example.protify;
2 |
3 | import android.test.ActivityInstrumentationTestCase2;
4 |
5 | /**
6 | * This is a simple framework for a test of an Application. See
7 | * {@link android.test.ApplicationTestCase ApplicationTestCase} for more information on
8 | * how to write and extend Application tests.
9 | *
10 | * To run this test, you can type:
11 | * adb shell am instrument -w \
12 | * -e class com.example.protify.MainActivityTest \
13 | * com.example.protify.tests/android.test.InstrumentationTestRunner
14 | */
15 | public class MainActivityTest extends ActivityInstrumentationTestCase2 {
16 |
17 | public MainActivityTest() {
18 | super("com.example.protify", MainActivity.class);
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/sbt-plugin/src/sbt-test/protify/layout/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/sbt-plugin/src/sbt-test/protify/layout/src/main/java/com/example/protify/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.example.protify;
2 |
3 | import android.app.Activity;
4 | import android.os.Bundle;
5 |
6 | public class MainActivity extends Activity
7 | {
8 | /** Called when the activity is first created. */
9 | @Override
10 | public void onCreate(Bundle savedInstanceState)
11 | {
12 | super.onCreate(savedInstanceState);
13 | setContentView(R.layout.main);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/sbt-plugin/src/sbt-test/protify/layout/src/main/res/drawable-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scala-android/sbt-android-protify/00a3640df740a62eb942d08e9f96bd5a1faadda3/sbt-plugin/src/sbt-test/protify/layout/src/main/res/drawable-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sbt-plugin/src/sbt-test/protify/layout/src/main/res/drawable-ldpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scala-android/sbt-android-protify/00a3640df740a62eb942d08e9f96bd5a1faadda3/sbt-plugin/src/sbt-test/protify/layout/src/main/res/drawable-ldpi/ic_launcher.png
--------------------------------------------------------------------------------
/sbt-plugin/src/sbt-test/protify/layout/src/main/res/drawable-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scala-android/sbt-android-protify/00a3640df740a62eb942d08e9f96bd5a1faadda3/sbt-plugin/src/sbt-test/protify/layout/src/main/res/drawable-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sbt-plugin/src/sbt-test/protify/layout/src/main/res/drawable-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scala-android/sbt-android-protify/00a3640df740a62eb942d08e9f96bd5a1faadda3/sbt-plugin/src/sbt-test/protify/layout/src/main/res/drawable-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sbt-plugin/src/sbt-test/protify/layout/src/main/res/layout/main.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/sbt-plugin/src/sbt-test/protify/layout/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Layout Test
4 |
5 |
--------------------------------------------------------------------------------
/sbt-plugin/src/sbt-test/protify/layout/test:
--------------------------------------------------------------------------------
1 | > android:package
2 |
--------------------------------------------------------------------------------