├── .gitignore ├── .travis.yml ├── CHANGES ├── LICENSE ├── README.md ├── build.sbt ├── notes ├── 0.4.1.markdown ├── 0.4.2.markdown ├── 0.5.0.markdown ├── 0.5.1.markdown ├── 0.6.0.markdown ├── 0.6.1.markdown ├── 0.6.2.markdown └── about.markdown ├── project ├── Status.scala └── build.properties └── src └── main └── scala ├── AndroidBase.scala ├── AndroidDdm.scala ├── AndroidDefaults.scala ├── AndroidEmulator.scala ├── AndroidHelpers.scala ├── AndroidInstall.scala ├── AndroidJavaLayout.scala ├── AndroidLaunch.scala ├── AndroidManifestGenerator.scala ├── AndroidNdk.scala ├── AndroidPath.scala ├── AndroidPlugin.scala ├── AndroidPreload.scala ├── AndroidProjects.scala ├── AndroidRelease.scala ├── AndroidTarget.scala ├── AndroidTest.scala ├── ApkBuilder.scala ├── PasswordManager.scala ├── TypedLayouts.scala ├── TypedResources.scala └── legacy └── Github.scala /.gitignore: -------------------------------------------------------------------------------- 1 | .DS* 2 | lib_managed/ 3 | target/ 4 | project/boot/ 5 | project/build/target/ 6 | project/plugins/src_managed/ 7 | project/plugins/project/ 8 | temp 9 | .idea 10 | .idea_modules 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | 3 | script: wget https://raw.github.com/paulp/sbt-extras/master/sbt && chmod u+x ./sbt && ./sbt test 4 | 5 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | == 0.4.x 2 | 3 | * CHANGES no longer maintained, cf notes/x.y.z.markdown 4 | 5 | == 0.4 06/02/10 6 | 7 | * xsbt compatibility (n8han) 8 | * jarsigning support (n8han) 9 | * Windows compatibility fixes (timgilbert) 10 | 11 | == 0.3 07/12/09 12 | 13 | * removed dependencies to ant+antelese 14 | 15 | == 0.2 05/12/09 16 | 17 | * use ANDROID_SDK_HOME for locating SDK 18 | * exclude Android provided artifacts (commons-*) 19 | * added project generator script 20 | * added AndroidTestProject for unit testing 21 | * extract project metadata from AndroidManifest.xml (package name, api level) 22 | * autodiscovery of google add-on apis (Google Maps) 23 | 24 | == 0.1 Initial release, 07/07/09 25 | 26 | * Mark Harrah, based off http://github.com/weihsiu/saisiyat/ 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Walter Chang, Mark Harrah, Jan Berkel 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 3. The name of the author may not be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 16 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Build Scala Android apps using Scala 2 | 3 | *NOTE:* this plugin is currently not actively maintained. Have a look instead at [android-sdk-plugin](https://github.com/pfn/android-sdk-plugin). 4 | 5 | sbt-android-plugin is an extension for the Scala build tool [sbt][] which 6 | aims to make it as simple as possible to get started with Scala on Android. 7 | 8 | Together with [giter8][] you can create and build a simple Android Scala project in a 9 | matter of minutes. 10 | 11 | ## Getting started 12 | 13 | See the [Getting started][] guide on the wiki for more documentation. In case 14 | you're not not familiar with sbt make sure to check out its excellent 15 | [Getting Started Guide][sbt-getting-started] first. 16 | 17 | ## Hacking on the plugin 18 | 19 | If you need make modifications to the plugin itself, you can compile 20 | and install it locally (you need at least sbt 0.11.x to build it): 21 | 22 | $ git clone git://github.com/jberkel/android-plugin.git 23 | $ cd android-plugin 24 | $ sbt publish-local 25 | 26 | ## Migrating from 0.7.x 27 | 28 | For those who are familiar with the 0.7.x plugin, there is a [migration guide][] 29 | for a quick reference. The 0.7.x version is no longer maintained - but it is 30 | still available in the [0.7.x][] branch. 31 | 32 | ## Mailing list 33 | 34 | There's no official mailing list for the project but most contributors hang 35 | out in [scala-on-android][] or [simple-build-tool][]. 36 | 37 | You can also check out a list of 38 | [projects using sbt-android-plugin][] to see some real-world examples. 39 | 40 | ## Credits 41 | 42 | This code is based on work by Walter Chang 43 | ([saisiyat](http://github.com/weihsiu/saisiyat/)), turned into a plugin by 44 | [Mark Harrah](http://github.com/harrah), and maintained by 45 | [Jan Berkel](https://github.com/jberkel). 46 | 47 | A lot of people have contributed to the plugin; see [contributors][] for a full 48 | list. 49 | 50 | [![Build Status](https://secure.travis-ci.org/jberkel/android-plugin.png?branch=master)](http://travis-ci.org/jberkel/android-plugin) 51 | 52 | [sbt]: https://github.com/harrah/xsbt/wiki 53 | [scala-on-android]: http://groups.google.com/group/scala-on-android 54 | [simple-build-tool]: http://groups.google.com/group/simple-build-tool 55 | [0.7.x]: https://github.com/jberkel/android-plugin/tree/0.7.x 56 | [migration guide]: https://github.com/jberkel/android-plugin/wiki/migration_guide 57 | [contributors]: https://github.com/jberkel/android-plugin/wiki/Contributors 58 | [homebrew]: https://github.com/mxcl/homebrew 59 | [projects using sbt-android-plugin]: https://github.com/jberkel/android-plugin/wiki/Projects-using-sbt-android-plugin 60 | [Getting started]: https://github.com/jberkel/android-plugin/wiki/getting-started 61 | [giter8]: https://github.com/n8han/giter8 62 | [sbt-getting-started]: http://scala-sbt.org/release/docs/Getting-Started/Welcome 63 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := "sbt-android" 2 | 3 | organization := "org.scala-sbt" 4 | 5 | version := "0.7.1-SNAPSHOT" 6 | 7 | scalacOptions ++= Seq("-unchecked", "-deprecation", "-Xcheckinit", "-Xfatal-warnings") 8 | 9 | publishMavenStyle := false 10 | 11 | publishTo <<= (version) { version: String => 12 | val scalasbt = "http://scalasbt.artifactoryonline.com/scalasbt/" 13 | val (name, url) = if (version.contains("-")) 14 | ("sbt-plugin-snapshots", scalasbt+"sbt-plugin-snapshots") 15 | else 16 | ("sbt-plugin-releases", scalasbt+"sbt-plugin-releases") 17 | Some(Resolver.url(name, new URL(url))(Resolver.ivyStylePatterns)) 18 | } 19 | 20 | credentials += Credentials(Path.userHome / ".ivy2" / ".credentials") 21 | 22 | libraryDependencies ++= Seq( 23 | "com.google.android.tools" % "ddmlib" % "r10", 24 | "net.sf.proguard" % "proguard-base" % "4.8" 25 | ) 26 | 27 | sbtPlugin := true 28 | 29 | commands += Status.stampVersion 30 | -------------------------------------------------------------------------------- /notes/0.4.1.markdown: -------------------------------------------------------------------------------- 1 | * Build and install test packages before running test-emulator/device 2 | * Android 2.1 fix (pk11) 3 | * README updates (stevej) 4 | * Require sbt 0.7.0 / Scala 2.7.7 (to build plugin itself) 5 | * Posterous support :) 6 | -------------------------------------------------------------------------------- /notes/0.4.2.markdown: -------------------------------------------------------------------------------- 1 | * Plugin has been renamed to sbt-android-plugin 2 | * sbt start-emulator/device actions (GH-4) (steve918) 3 | * Various proguard config enhancements (GH-9, GH-10, GH-11) 4 | -------------------------------------------------------------------------------- /notes/0.5.0.markdown: -------------------------------------------------------------------------------- 1 | * `TypedResources` trait processes layout definitions to generate typed resource references in a Scala source file 2 | -------------------------------------------------------------------------------- /notes/0.5.1.markdown: -------------------------------------------------------------------------------- 1 | * Better compatibility with Scala 2.8.x 2 | * New Android API levels 3 | -------------------------------------------------------------------------------- /notes/0.6.0.markdown: -------------------------------------------------------------------------------- 1 | * Plugin ported to sbt 0.10.x/0.11.x ([Philip Cali][philcali]) 2 | 3 | * Update path to Google's Android add-ons ([nuriaion][nuriaion]) 4 | 5 | * Use the ApkBuilder class in sdklib.jar instead of the deprecated apkbuilder command-line executable ([Paul Butcher][paulbutcher]) 6 | 7 | * Support for building standard Java Android projects ([Jan Berkel][jberkel]) 8 | 9 | * AndroidManifestGenerator trait to control and increment 10 | versionCode and versionName properties 11 | ([Kevin Hester][geeksville]) 12 | 13 | * Integrated ddmlib to grab screenshots and memory/thread dumps from emulator / device ([Jan Berkel][jberkel]) 14 | 15 | * Android library project support ([Paul Butcher][paulbutcher]) 16 | 17 | * Include resources in apk ([Kevin Hester][geeksville]) 18 | 19 | * Github API integration: upload apks to github/S3 ([Jan Berkel][jberkel]) 20 | 21 | * Upgrade to proguard 4.6 w/ optional optimization ([Martin Kneissl][mkneissl]) 22 | 23 | [nuriaion]: https://github.com/Nuriaion 24 | [paulbutcher]: https://github.com/paulbutcher/ 25 | [jberkel]: https://github.com/jberkel 26 | [geeksville]: https://github.com/geeksville 27 | [philcali]: https://github.com/philcali 28 | [mkneissl]: https://github.com/mkneissl/ 29 | -------------------------------------------------------------------------------- /notes/0.6.1.markdown: -------------------------------------------------------------------------------- 1 | * Added PasswordManager ([Jan Berkel][jberkel]) 2 | 3 | * Android Library support ([PR-112][], [Logan Johnson][loganj] / [Matthew Willis][appamatto]) 4 | 5 | * Generate JNI C header files ([PR-115][], [Martin Kneissl][mkneissl]) 6 | 7 | [jberkel]: https://github.com/jberkel 8 | [loganj]: https://github.com/loganj 9 | [appamatto]: https://github.com/appamatto 10 | [mkneissl]: https://github.com/mkneissl 11 | [PR-112]: https://github.com/jberkel/android-plugin/pull/112 12 | [PR-115]: https://github.com/jberkel/android-plugin/pull/115 13 | -------------------------------------------------------------------------------- /notes/0.6.2.markdown: -------------------------------------------------------------------------------- 1 | * fixed aapt-generate reports success even when aapt fails ([GH-125][], [Jan Berkel][jberkel]) 2 | 3 | * Support for .so artifacts in libraryDependencies ([PR-123][], [Matthew Willis][appamatto]) 4 | 5 | * fix native libraryDepencies renaming ([PR-124][], [Matthew Willis][appamatto]) 6 | 7 | * Added android:test-only-(device|emulator) ([Jan Berkel][jberkel]) 8 | 9 | * Parse output from Android instrumentation tests ([Jan Berkel][jberkel]) 10 | 11 | * Add ability to predex external libraries ([PR-107][], [ezh][ezh]) 12 | 13 | * Added support for sbt 0.12.x 14 | 15 | [jberkel]: https://github.com/jberkel 16 | [appamatto]: https://github.com/appamatto 17 | [ezh]: https://github.com/ezh 18 | [PR-123]: https://github.com/jberkel/android-plugin/pull/123 19 | [PR-124]: https://github.com/jberkel/android-plugin/pull/124 20 | [GH-125]: https://github.com/jberkel/android-plugin/issues/125 21 | [PR-107]: https://github.com/jberkel/android-plugin/pull/107 22 | -------------------------------------------------------------------------------- /notes/about.markdown: -------------------------------------------------------------------------------- 1 | [sbt-android-plugin][1] is a plug-in for simple-build-tool that provides support 2 | for developing Android applications in Scala. 3 | 4 | [1]: https://github.com/jberkel/android-plugin 5 | -------------------------------------------------------------------------------- /project/Status.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import Keys._ 3 | 4 | object Status 5 | { 6 | def stampVersion = Command.command("stamp-version") { state => 7 | Project.extract(state).append((version ~= stamp) :: Nil, state) 8 | } 9 | def stamp(v: String): String = 10 | if(v endsWith Snapshot) 11 | (v stripSuffix Snapshot) + "-" + timestampString(System.currentTimeMillis) 12 | else 13 | v 14 | def timestampString(time: Long): String = 15 | { 16 | val format = new java.text.SimpleDateFormat("yyyyMMdd-HHmmss") 17 | format.format(new java.util.Date(time)) 18 | } 19 | final val Snapshot = "-SNAPSHOT" 20 | } 21 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.12.0 2 | -------------------------------------------------------------------------------- /src/main/scala/AndroidBase.scala: -------------------------------------------------------------------------------- 1 | package sbtandroid 2 | 3 | import sbt._ 4 | 5 | import scala.xml._ 6 | 7 | import Keys._ 8 | import AndroidPlugin._ 9 | import AndroidHelpers._ 10 | 11 | import sbinary.DefaultProtocol.StringFormat 12 | 13 | object AndroidBase { 14 | def getNativeTarget(parent: File, name: String, abi: String) = { 15 | val extension = "-" + abi + ".so" 16 | if (name endsWith extension) { 17 | val stripped = name.substring(0, name indexOf '-') + ".so" 18 | val target = new File(abi) / stripped 19 | Some(parent / target.toString) 20 | } else None 21 | } 22 | 23 | def copyNativeLibrariesTask = 24 | (streams, managedNativePath, dependencyClasspath) map { 25 | (s, natives, deps) => { 26 | val sos = (deps.map(_.data)).filter(_.name endsWith ".so") 27 | var copied = Seq.empty[File] 28 | for (so <- sos) 29 | getNativeTarget(natives, so.name, "armeabi") orElse getNativeTarget(natives, so.name, "armeabi-v7a") map { 30 | target => 31 | target.getParentFile.mkdirs 32 | IO.copyFile(so, target) 33 | copied +:= target 34 | s.log.info("Copied native library: " + target.toString) 35 | } 36 | 37 | // Clean up stale native libraries 38 | for (path <- IO.listFiles(natives / "armeabi") ++ IO.listFiles(natives / "armeabi-v7a")) { 39 | s.log.debug("Checking native library: " + path.toString) 40 | if (path.name.endsWith(".so") && !copied.contains(path)) { 41 | IO.delete(path) 42 | s.log.debug("Deleted native library: " + path.toString) 43 | } 44 | } 45 | } 46 | } 47 | 48 | private def apklibSourcesTask = 49 | (apklibDependencies, streams) map { 50 | (projectLibs, s) => { 51 | if (!projectLibs.isEmpty) { 52 | s.log.debug("Generating source files from ApkLibs") 53 | val xs = for ( 54 | l <- projectLibs; 55 | f <- l.sources 56 | ) yield f 57 | 58 | s.log.info("Generated " + xs.size + " source files from " + projectLibs.size + " ApkLibs") 59 | xs 60 | } else Seq.empty 61 | } 62 | } 63 | 64 | private def apklibPackageTask = 65 | (manifestPath, mainResPath, mainAssetsPath, javaSource, scalaSource, packageApkLibPath, streams) map { 66 | (manPath, rPath, aPath, jPath, sPath, apklib, s) => 67 | s.log.info("packaging apklib") 68 | val mapping = 69 | (PathFinder(manPath) x flat) ++ 70 | (PathFinder(jPath) ** "*.java" x rebase(jPath, "src")) ++ 71 | (PathFinder(sPath) ** "*.scala" x rebase(sPath, "src")) ++ 72 | ((PathFinder(rPath) ***) x rebase(rPath, "res")) ++ 73 | ((PathFinder(aPath) ***) x rebase(aPath, "assets")) 74 | IO.jar(mapping, apklib, new java.util.jar.Manifest) 75 | apklib 76 | } 77 | 78 | private def aarlibDependenciesTask = 79 | (update, aarlibBaseDirectory, aarlibLibManaged, aarlibResourceManaged, resourceManaged, streams, 80 | unmanagedBase) map { 81 | (updateReport, aarlibBaseDirectory, aarlibLibManaged, aarlibResourceManaged, resManaged, s, 82 | unmanagedBase) => { 83 | 84 | // We want to extract every aarlib in the classpath that is not already 85 | // set to provided (which should mean that another project already 86 | // provides the aarLib). 87 | val allaarlibs = updateReport.matching(artifactFilter(`type` = "aar")) 88 | val unmanagedaarlibs = Option(unmanagedBase.listFiles) 89 | .map(f => f.filter(_.name.endsWith(".aar")).toList) 90 | .getOrElse(Seq.empty) 91 | val providedaarlibs = updateReport.matching(configurationFilter(name = "provided")) 92 | val aarlibs = (allaarlibs --- providedaarlibs get) ++ unmanagedaarlibs 93 | 94 | // Make the destination directories 95 | aarlibBaseDirectory.mkdirs 96 | aarlibLibManaged.mkdirs 97 | aarlibResourceManaged.mkdirs 98 | 99 | // Extract the aarLibs 100 | aarlibs map { aarlib => 101 | 102 | // Check if the AAR lib is up to date 103 | val dest = aarlibResourceManaged / aarlib.base 104 | val destjar = aarlibLibManaged / (aarlib.base + ".jar") 105 | val timestamp = dest / ".timestamp" 106 | 107 | // Check if the AAR lib is up to date 108 | if (timestamp.lastModified < aarlib.lastModified) { 109 | 110 | // Unzip the aarlib to a temporary directory 111 | s.log.info("Extracting library " + aarlib.name) 112 | val unzipped = IO.unzip(aarlib, dest) 113 | 114 | // Move the classres in place 115 | IO.move(dest / "classes.jar", destjar) 116 | 117 | // Add a marker 118 | IO.delete(timestamp) 119 | new java.io.PrintWriter(timestamp, "UTF-8").close 120 | } 121 | 122 | // Read the package name from the manifest 123 | val manifest = dest / "AndroidManifest.xml" 124 | val pkgName = XML.loadFile(manifest).attribute("package").get.head.text 125 | 126 | // Return a LibraryProject instance with some info about this aarLib 127 | LibraryProject( 128 | pkgName, 129 | manifest, 130 | Set(destjar), 131 | Some(dest / "res") filter { _.exists }, 132 | Some(dest / "assets") filter { _.exists } 133 | ) 134 | } 135 | } 136 | } 137 | 138 | private def apklibDependenciesTask = 139 | (update, apklibBaseDirectory, apklibSourceManaged, apklibResourceManaged, resourceManaged, streams, 140 | unmanagedBase) map { 141 | (updateReport, apklibBaseDirectory, apklibSourceManaged, apklibResourceManaged, resManaged, s, 142 | unmanagedBase) => { 143 | 144 | // Make the destination directories 145 | apklibBaseDirectory.mkdirs 146 | apklibSourceManaged.mkdirs 147 | apklibResourceManaged.mkdirs 148 | 149 | // We want to extract every apklib in the classpath that is not already 150 | // set to provided (which should mean that another project already 151 | // provides the ApkLib). 152 | // We also want to include apklibs in unmanagedBase. 153 | val allApklibs = updateReport.matching(artifactFilter(`type` = "apklib")) 154 | val unmanagedApklibs = Option(unmanagedBase.listFiles) 155 | .map(f => f.filter(_.name.endsWith(".apklib")).toList) 156 | .getOrElse(Seq.empty) 157 | val providedApklibs = updateReport.matching(configurationFilter(name = "provided")) 158 | val apklibs = (allApklibs --- providedApklibs get) ++ unmanagedApklibs 159 | 160 | // Extract the ApkLibs 161 | apklibs map { apklib => 162 | 163 | // Unzip the ApkLib to a temporary directory 164 | s.log.info("Extracting library " + apklib.name) 165 | val dest = apklibResourceManaged / apklib.base 166 | val unzipped = IO.unzip(apklib, dest) 167 | 168 | // Move sources to the managed dir 169 | def moveContents(fromDir: File, toDir: File) = { 170 | toDir.mkdirs() 171 | val pairs = for ( 172 | file <- unzipped; 173 | rel <- IO.relativize(fromDir, file) 174 | ) yield (file, toDir / rel) 175 | IO.move(pairs) 176 | pairs map { case (_,t) => t } 177 | } 178 | val sources = moveContents(dest / "src", apklibSourceManaged) 179 | 180 | // Read the package name from the manifest 181 | val manifest = dest / "AndroidManifest.xml" 182 | val pkgName = XML.loadFile(manifest).attribute("package").get.head.text 183 | 184 | // Return a LibraryProject instance with some info about this ApkLib 185 | LibraryProject( 186 | pkgName, 187 | manifest, 188 | sources, 189 | Some(dest / "res") filter { _.exists }, 190 | Some(dest / "assets") filter { _.exists } 191 | ) 192 | } 193 | } 194 | } 195 | 196 | private def aaptGenerateTask = 197 | (manifestPackage, aaptPath, generatedProguardConfigPath, 198 | manifestPath, resPath, libraryJarPath, managedJavaPath, 199 | aarlibDependencies, apklibDependencies, apklibSourceManaged, 200 | streams, useDebug) map { 201 | 202 | (mPackage, aPath, proGen, mPath, rPath, jarPath, javaPath, 203 | aarlibs, apklibs, apklibJavaPath, s, useDebug) => 204 | 205 | // Create the managed Java path if necessary 206 | javaPath.mkdirs 207 | 208 | // Arguments for resource directories 209 | val libraryResPathArgs = rPath.flatMap(p => Seq("-S", p.absolutePath)) 210 | 211 | // Arguments for library assets 212 | val extlibs = apklibs ++ aarlibs 213 | val libraryAssetPathArgs = for ( 214 | lib <- extlibs; 215 | d <- lib.assetsDir.toSeq; 216 | arg <- Seq("-A", d.absolutePath) 217 | ) yield arg 218 | 219 | def runAapt(`package`: String, outJavaPath: File, args: String*) { 220 | s.log.info("Running AAPT for package " + `package`) 221 | 222 | val aapt = Seq(aPath.absolutePath, "package", "--auto-add-overlay", "-m", 223 | "--custom-package", `package`, 224 | "-M", mPath.head.absolutePath, 225 | "-I", jarPath.absolutePath, 226 | "-J", outJavaPath.absolutePath, 227 | "-G", proGen.absolutePath) ++ 228 | args ++ 229 | libraryResPathArgs ++ 230 | libraryAssetPathArgs 231 | 232 | if (aapt.run(false).exitValue != 0) sys.error("error generating resources") 233 | } 234 | 235 | def createBuildConfig(`package`: String, outJavaPath: File) = { 236 | // Split the package name in a filesystem path 237 | var path = outJavaPath 238 | `package`.split('.').foreach { path /= _ } 239 | 240 | // Create that path, if needed 241 | path.mkdirs 242 | 243 | // Write BuildConfig.java 244 | val buildConfig = path / "BuildConfig.java" 245 | IO.write(buildConfig, """ 246 | package %s; 247 | public final class BuildConfig { 248 | public static final boolean DEBUG = %s; 249 | }""".format(`package`, useDebug)) 250 | 251 | // Return the name of the generated file 252 | buildConfig 253 | } 254 | 255 | // Run aapt to generate resources for the main package, and each apklib and 256 | // AAR dependency. 257 | runAapt(mPackage, javaPath) 258 | apklibs.foreach(lib => runAapt(lib.pkgName, javaPath, "--non-constant-id")) 259 | aarlibs.foreach(lib => runAapt(lib.pkgName, javaPath, "--non-constant-id")) 260 | 261 | // Generate BuildConfig.java for the main package, and each apklib and AAR 262 | // dependency. 263 | val generatedBuildConfig = ( 264 | Seq(createBuildConfig(mPackage, javaPath)) ++ 265 | apklibs.map(lib => createBuildConfig(lib.pkgName, javaPath)) ++ 266 | aarlibs.map(lib => createBuildConfig(lib.pkgName, javaPath)) 267 | ) 268 | 269 | // Return the list of generated files 270 | (javaPath ** "R.java" get) ++ generatedBuildConfig 271 | } 272 | 273 | private def aidlGenerateTask = 274 | (sourceDirectories, idlPath, platformPath, managedJavaPath, javaSource, streams) map { 275 | (sDirs, idPath, platformPath, javaPath, jSource, s) => 276 | val aidlPaths = sDirs.map(_ ** "*.aidl").reduceLeft(_ +++ _).get 277 | if (aidlPaths.isEmpty) { 278 | s.log.debug("No AIDL files found, skipping") 279 | Nil 280 | } else { 281 | val processor = aidlPaths.map { ap => 282 | idPath.absolutePath :: 283 | "-p" + (platformPath / "framework.aidl").absolutePath :: 284 | "-o" + javaPath.absolutePath :: 285 | "-I" + jSource.absolutePath :: 286 | ap.absolutePath :: Nil 287 | }.foldLeft(None.asInstanceOf[Option[ProcessBuilder]]) { (f, s) => 288 | f match { 289 | case None => Some(s) 290 | case Some(first) => Some(first #&& s) 291 | } 292 | }.get 293 | s.log.debug("generating aidl "+processor) 294 | processor ! 295 | 296 | val rPath = javaPath ** "R.java" 297 | javaPath ** "*.java" --- (rPath) get 298 | } 299 | } 300 | 301 | def findPath() = (manifestPath) map { p => 302 | manifest(p.head).attribute("package").getOrElse(sys.error("package not defined")).text 303 | } 304 | 305 | def isPreinstalled(f: Attributed[java.io.File], preinstalled: Seq[ModuleID]): Boolean = { 306 | f.get(moduleID.key) match { 307 | case Some(m) => preinstalled exists (pm => 308 | pm.organization == m.organization && 309 | pm.name == m.name) 310 | case None => false 311 | } 312 | } 313 | 314 | /** 315 | * Returns the internal dependencies for the "provided" scope only 316 | */ 317 | def providedInternalDependenciesTask(proj: ProjectRef, struct: Load.BuildStructure) = { 318 | // "Provided" dependencies of a ResolvedProject 319 | def providedDeps(op: ResolvedProject): Seq[ProjectRef] = { 320 | op.dependencies 321 | .filter(p => (p.configuration getOrElse "") == "provided") 322 | .map(_.project) 323 | } 324 | 325 | // Collect every "provided" dependency in the dependency graph 326 | def collectDeps(projRef: ProjectRef): Seq[ProjectRef] = { 327 | val deps = Project.getProject(projRef, struct).toSeq.flatMap(providedDeps) 328 | deps.flatMap(ref => ref +: collectDeps(ref)).distinct 329 | } 330 | 331 | // Return the list of "provided" internal dependencies for the ProjectRef 332 | // in argument. 333 | collectDeps(proj) 334 | .flatMap(exportedProducts in (_, Compile) get struct.data) 335 | .join.map(_.flatten.files) 336 | } 337 | 338 | val providedInternalDependencies = TaskKey[Seq[File]]("provided-internal-dependencies") 339 | 340 | lazy val globalSettings: Seq[Setting[_]] = Seq( 341 | 342 | // At the moment, we NEED to use Java 6 class files 343 | javacOptions ++= Seq( 344 | "-encoding", "utf8", 345 | "-target", "1.6", 346 | "-source", "1.6" 347 | ), 348 | 349 | // Same thing for Scalac 350 | scalacOptions ++= Seq( 351 | "-encoding", "utf8", 352 | "-target:jvm-1.6" 353 | ), 354 | 355 | // By default, use the first device we find as the ADB target 356 | adbTarget in Global := AndroidDefaultTargets.Auto, 357 | 358 | // By default, don't cache passwords 359 | cachePasswords in Global := false, 360 | 361 | // By default, no additional Proguard options and optimizations 362 | proguardOptions := Seq.empty, 363 | proguardOptimizations := Seq.empty, 364 | 365 | // Default excludes two problematic files, override for custom exclusions 366 | proguardInJarsFilter := { jar: File => 367 | Seq("!META-INF/MANIFEST.MF", "!library.properties") 368 | }, 369 | 370 | // Dex options 371 | dxMemory := "-JXmx512m", 372 | 373 | // Platform path for the current project 374 | platformPath <<= (sdkPath, platformName) (_ / "platforms" / _), 375 | 376 | // Path to the platform android.jar for the current project 377 | libraryJarPath <<= (platformPath, libraryJarName) (_ / _), 378 | 379 | // By default, if preloading is enabled, preload the Scala library 380 | preloadFilters := Seq(filterName("scala-library")), 381 | 382 | // Default IntelliJ configuration (for sbtidea integration) 383 | ideaConfiguration := Compile, 384 | 385 | // Default key alias 386 | keyalias := "alias_name", 387 | 388 | // Apk defaults to the Compile scope 389 | apk <<= apk in Compile, 390 | 391 | // Use typed resources by default 392 | useTypedResources := true, 393 | 394 | // Use typed layouts by default 395 | useTypedLayouts := true, 396 | 397 | // Gradle uses libs/ as the unmanaged JAR directory! 398 | unmanagedBase <<= (baseDirectory) (_ / "libs"), 399 | 400 | // Path to the unmanaged native libraries 401 | unmanagedNativePath <<= (baseDirectory) (_ / "lib"), 402 | 403 | // Path to the managed native libraries 404 | managedNativePath <<= (crossTarget) (_ / "native_managed"), 405 | 406 | // Default native directories 407 | nativeDirectories := Seq.empty, 408 | nativeDirectories <+= unmanagedNativePath map (x => x), 409 | nativeDirectories <+= managedNativePath map (x => x) 410 | ) 411 | 412 | lazy val settings: Seq[Setting[_]] = (Seq ( 413 | 414 | // Path to the Proguard-ed class JAR 415 | classesMinJarName <<= (artifact, configuration, version) ( 416 | (a, c, v) => "classes-%s-%s-%s.min.jar".format(a.name, c.name, v) ), 417 | 418 | // Path to the dexed class file 419 | classesDexName <<= (artifact, configuration, version) ( 420 | (a, c, v) => "classes-%s-%s-%s.dex".format(a.name, c.name, v) ), 421 | 422 | // Name and path to the resource APK 423 | resourcesApkName <<= (artifact, configuration, version) ( 424 | (a, c, v) => "resources-%s-%s-%s.apk".format(a.name, c.name, v) ), 425 | resourcesApkPath <<= (target, resourcesApkName) (_ / _), 426 | 427 | // Name and path to the final APK 428 | packageApkName <<= (artifact, configuration, versionName) map ( 429 | (a, c, v) => "%s-%s-%s.apk".format(a.name, c.name, v) ), 430 | packageApkPath <<= (target, packageApkName) map (_ / _), 431 | 432 | // Name and path to the final ApkLib 433 | packageApkLibName <<= (artifact, configuration, versionName) map ( 434 | (a, c, v) => "%s-%s-%s.apklib".format(a.name, c.name, v) ), 435 | packageApkLibPath <<= (target, packageApkLibName) map (_ / _), 436 | 437 | // Path to the manifest file 438 | manifestPath <<= (sourceDirectory, manifestName) map((s,m) => Seq(s / m)), 439 | 440 | // Package information, extracted from the manifest 441 | manifestPackage <<= findPath, 442 | manifestPackageName <<= findPath storeAs manifestPackageName triggeredBy manifestPath, 443 | minSdkVersion <<= (manifestPath, manifestSchema) map ( (p,s) => usesSdk(p.head, s, "minSdkVersion")), 444 | maxSdkVersion <<= (manifestPath, manifestSchema) map ( (p,s) => usesSdk(p.head, s, "maxSdkVersion")), 445 | versionName <<= (manifestPath, manifestSchema, version) map ((p, schema, version) => 446 | manifest(p.head).attribute(schema, "versionName").map(_.text).getOrElse(version) 447 | ), 448 | 449 | // Main asset and resource paths 450 | mainAssetsPath <<= (sourceDirectory, assetsDirectoryName) (_ / _), 451 | mainResPath <<= (sourceDirectory, resDirectoryName) (_ / _) map (x=> x), 452 | 453 | // Managed sources and resources 454 | managedSourceDirectories <+= apklibSourceManaged, 455 | managedJavaPath <<= (sourceManaged) (_ / "java"), 456 | managedScalaPath <<= (sourceManaged) ( _ / "scala"), 457 | 458 | // Resource paths 459 | // 460 | // By default, include the main resource path, as well as the resources 461 | // from additional ApkLib dependencies. 462 | resPath := Seq(), 463 | resPath <+= mainResPath, 464 | resPath <++= apklibDependencies map (apklibs => apklibs.flatMap(_.resDir)), 465 | resPath <++= aarlibDependencies map (aarlibs => aarlibs.flatMap(_.resDir)), 466 | 467 | // Path to the resources APK file 468 | resourcesApkPath <<= (target, resourcesApkName) (_ / _), 469 | 470 | // Assets go into the resource directories 471 | resourceDirectories <<= resourceDirectories in Compile, 472 | resourceDirectories <+= (mainAssetsPath), 473 | 474 | // ApkLib paths 475 | apklibBaseDirectory <<= crossTarget (_ / "apklib_managed"), 476 | apklibSourceManaged <<= apklibBaseDirectory (_ / "src"), 477 | apklibResourceManaged <<= apklibBaseDirectory (_ / "res"), 478 | apklibDependencies <<= apklibDependenciesTask, 479 | apklibPackage <<= apklibPackageTask, 480 | apklibSources <<= apklibSourcesTask, 481 | 482 | // AAR lib paths 483 | aarlibBaseDirectory <<= crossTarget (_ / "aarlib_managed"), 484 | aarlibLibManaged <<= aarlibBaseDirectory (_ / "lib"), 485 | aarlibResourceManaged <<= aarlibBaseDirectory (_ / "res"), 486 | aarlibDependencies <<= aarlibDependenciesTask, 487 | 488 | // Output path of the DX command 489 | dxOutputPath <<= (target, classesDexName) (_ / _), 490 | 491 | // Inputs for the DX command 492 | dxInputs <<= 493 | (proguard, includedClasspath, classDirectory) map ( 494 | (proguard, includedClasspath, classDirectory) => proguard match { 495 | case Some(f) => Seq(f) 496 | case None => includedClasspath :+ classDirectory 497 | } 498 | ), 499 | 500 | // Paths to be predexed by DX to improve build times. 501 | // 502 | // Usually, libraries that won't change much over time, and, by default, 503 | // the inputs that are part of the managed classpath. 504 | dxPredex <<= (managedClasspath, dxInputs) map { 505 | (cp, inputs) => { cp filter (inputs contains _.data) files } 506 | }, 507 | 508 | // Provided internal dependencies (usually, class directories from a 509 | // dependency project set as "provided") 510 | providedInternalDependencies <<= (thisProjectRef, buildStructure) flatMap providedInternalDependenciesTask, 511 | 512 | // The full input classpath 513 | inputClasspath <<= (dependencyClasspath) map { dcp => 514 | dcp filterNot (cpe => cpe.get(artifact.key) match { 515 | case Some(k) => k.`type` == "so" 516 | case None => false 517 | }) map (_.data) 518 | }, 519 | 520 | // The included classpath entries 521 | includedClasspath <<= 522 | (update, libraryJarPath, usePreloaded, dependencyClasspath, preinstalledModules, preloadFilters, providedInternalDependencies) map { 523 | (update, libraryJarPath, usePreloaded, dependencyClasspath, preinstalledModules, preloadFilters, providedInternalDependencies) => 524 | 525 | // Filters out the entries that are _not_ to be included in the final APK 526 | val notIncludedFilters = ( 527 | (if (usePreloaded) preloadFilters else Seq.empty) ++ 528 | (preinstalledModules map (filterModule _)) 529 | ) 530 | 531 | // Provided dependencies that are not to be included in the APK 532 | val provided = ( 533 | providedInternalDependencies ++ 534 | update.select(Set("provided")) ++ 535 | Seq(libraryJarPath) 536 | ) 537 | 538 | // Filter the full classpath 539 | dependencyClasspath.filterNot { cpe => 540 | (notIncludedFilters exists (f => f(cpe))) || 541 | (provided contains cpe.data) 542 | }.files 543 | }, 544 | 545 | // The provided classpath entries are those that are in `fullClasspath` but 546 | // not in `includedClasspath`. 547 | providedClasspath <<= (inputClasspath, includedClasspath) map ((in, incl) => 548 | in filterNot (incl contains _)), 549 | 550 | // Path to Proguard's output JAR 551 | proguardOutputPath <<= (target, classesMinJarName) (_ / _), 552 | 553 | // Path to the generated (with aapt -G) Proguard configuration 554 | generatedProguardConfigPath <<= (target, generatedProguardConfigName) (_ / _), 555 | 556 | // Copy native library dependencies 557 | copyNativeLibraries <<= copyNativeLibrariesTask, 558 | 559 | // AAPT and AIDL source generation 560 | aaptGenerate <<= aaptGenerateTask, 561 | aidlGenerate <<= aidlGenerateTask, 562 | 563 | // Manifest generator rules 564 | manifestRewriteRules := Seq.empty, 565 | 566 | // Migrate settings from the defaults in Compile 567 | sourceDirectory <<= sourceDirectory in Compile, 568 | sourceDirectories <<= sourceDirectories in Compile, 569 | resourceDirectory <<= resourceDirectory in Compile, 570 | javaSource <<= javaSource in Compile, 571 | scalaSource <<= scalaSource in Compile, 572 | dependencyClasspath <<= dependencyClasspath in Compile, 573 | managedClasspath <<= managedClasspath in Compile, 574 | 575 | // Add the dependencies to the classpath 576 | unmanagedClasspath <+= (libraryJarPath) map (Attributed.blank(_)), 577 | unmanagedJars <++= (aarlibDependencies) map { 578 | libs => libs.flatMap(_.sources).map(Attributed.blank(_)) }, 579 | 580 | // Set the default classpath types 581 | classpathTypes := Set("jar", "bundle", "so"), 582 | 583 | // Configure the source generators 584 | sourceGenerators <+= (apklibSources, aaptGenerate, aidlGenerate) map (_ ++ _ ++ _) 585 | )) 586 | } 587 | -------------------------------------------------------------------------------- /src/main/scala/AndroidDdm.scala: -------------------------------------------------------------------------------- 1 | package sbtandroid 2 | 3 | import sbt._ 4 | import complete.Parser 5 | import Keys._ 6 | import complete.DefaultParsers._ 7 | 8 | import AndroidPlugin._ 9 | 10 | import com.android.ddmlib.AndroidDebugBridge 11 | import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener 12 | import com.android.ddmlib.IDevice 13 | import com.android.ddmlib.ClientData 14 | import com.android.ddmlib.Client 15 | import com.android.ddmlib.ThreadInfo 16 | import com.android.ddmlib.ClientData.IHprofDumpHandler 17 | 18 | import java.io.{File, OutputStream, FileOutputStream} 19 | import java.awt.image.{BufferedImage, RenderedImage} 20 | import javax.imageio.ImageIO 21 | 22 | import scala.collection.mutable 23 | import sbinary.DefaultProtocol.StringFormat 24 | 25 | object AndroidDdm { 26 | var bridge: Option[AndroidDebugBridge] = None 27 | val infos = scala.collection.mutable.Map.empty[String, ThreadInfo] 28 | 29 | val THREAD_STATUS = Array[String]( 30 | "unknown", "zombie", "running", "timed-wait", "monitor", 31 | "wait", "init", "start", "native", "vmwait", 32 | "suspended") 33 | 34 | val clientListener = new IClientChangeListener() { 35 | override def clientChanged(client: Client, mask: Int) { 36 | mask match { 37 | case Client.CHANGE_NAME => 38 | // client connected 39 | case Client.CHANGE_THREAD_DATA => 40 | // thread status changed (thread died etc) 41 | if (client.getClientData.getThreads != null) { 42 | val tnames = for (tinfo <- client.getClientData.getThreads) 43 | yield { tinfo.getThreadName } 44 | infos.retain((name,_) => tnames.contains(name)) 45 | } 46 | case Client.CHANGE_THREAD_STACKTRACE => 47 | if (client.getClientData.getThreads != null) { 48 | for (tinfo <- client.getClientData.getThreads; if 49 | tinfo.getStackTrace != null) { 50 | infos.put(tinfo.getThreadName, tinfo) 51 | } 52 | } 53 | case _ => //System.out.println("client: "+client.getClientData.getClientDescription+" mask: "+mask) 54 | } 55 | } 56 | } 57 | 58 | def createBridge(path: String, clientSupport: Boolean) = { 59 | bridge.getOrElse { 60 | AndroidDebugBridge.addClientChangeListener(clientListener) 61 | AndroidDebugBridge.init(clientSupport) 62 | java.lang.Runtime.getRuntime.addShutdownHook(new Thread() { 63 | override def run() { terminateBridge() } 64 | }) 65 | bridge = Some(AndroidDebugBridge.createBridge(path, false)) 66 | bridge.get 67 | } 68 | } 69 | 70 | def terminateBridge() { 71 | AndroidDebugBridge.terminate() 72 | bridge = None 73 | } 74 | 75 | def withDevice[F](emulator: Boolean, path: String) 76 | (action: IDevice => F):Option[F] = { 77 | var count = 0 78 | val bridged = createBridge(path, true) 79 | 80 | while (!bridged.hasInitialDeviceList && count < 50) { 81 | Thread.sleep(100) 82 | count += 1 83 | } 84 | if (!bridged.hasInitialDeviceList) { 85 | System.err.println("Timeout getting device list") 86 | None 87 | } else { 88 | val (emus, devices) = bridged.getDevices.partition(_.isEmulator) 89 | (if (emulator) emus else devices).headOption.map(action) 90 | } 91 | } 92 | 93 | def withClient[F](emulator: Boolean, path: String, clientPkg: String) 94 | (action: Client => F):Option[F] = { 95 | withDevice(emulator, path) { device => 96 | var client = device.getClient(clientPkg) 97 | var count = 0 98 | while (client == null && count < 10) { 99 | client = device.getClient(clientPkg) 100 | Thread.sleep(100) 101 | count += 1 102 | } 103 | if (client != null) { 104 | action(client) 105 | } else { 106 | None.asInstanceOf[F] 107 | } 108 | } 109 | } 110 | 111 | // ported from http://dustingram.com/wiki/Device_Screenshots_with_the_Android_Debug_Bridge 112 | def screenshot(emulator: Boolean, landscape: Boolean, path: String):Option[Screenshot] = { 113 | withDevice(emulator, path) { device => 114 | val raw = device.getScreenshot 115 | val (width2, height2) = if (landscape) (raw.height, raw.width) else (raw.width, raw.height) 116 | val image = new BufferedImage(width2, height2, BufferedImage.TYPE_INT_RGB) 117 | var index = 0 118 | val indexInc = raw.bpp >> 3 119 | for (y <- 0 until raw.height; x <- 0 until raw.width) { 120 | val value = raw.getARGB(index) 121 | if (landscape) 122 | image.setRGB(y, raw.width - x - 1, value) 123 | else 124 | image.setRGB(x, y, value) 125 | index += indexInc 126 | } 127 | Screenshot(image) 128 | } 129 | } 130 | 131 | def dumpHprof(app: String, path: String, emulator: Boolean, streams: TaskStreams) 132 | (success: (Client, Array[Byte]) => Unit) { 133 | withClient(emulator, path, app) { client => 134 | ClientData.setHprofDumpHandler(new IHprofDumpHandler() { 135 | 136 | override def onSuccess(path: String, client: Client) { sys.error("not supported") } 137 | override def onSuccess(data: Array[Byte], client: Client) { success(client, data) } 138 | override def onEndFailure(client: Client, message:String) { sys.error(message) } 139 | }) 140 | client.dumpHprof() 141 | streams.log.info("requested hprof dump") 142 | }.orElse(sys.error("can not get client "+app+", is it running")) 143 | } 144 | 145 | def fetchThreads(app: String, path: String, emulator: Boolean):Option[Array[ThreadInfo]] = { 146 | withClient(emulator, path, app) { client => 147 | client.setThreadUpdateEnabled(true) 148 | client.requestThreadUpdate() 149 | val threads = client.getClientData.getThreads 150 | if (threads != null) 151 | threads.foreach(t => client.requestThreadStackTrace(t.getThreadId)) 152 | threads 153 | }.orElse { // client died 154 | //infos.clear() 155 | None 156 | } 157 | } 158 | 159 | private def writeHprof(toolsPath: File)(client: Client, data: Array[Byte]) = { 160 | val pkg = client.getClientData.getClientDescription 161 | val pid = client.getClientData.getPid 162 | val tmp = new File(pkg+".tmp") 163 | val fos = new FileOutputStream(tmp) 164 | fos.write(data) 165 | fos.close() 166 | val hprof = "%s-%d-%d.hprof".format(pkg, pid, System.currentTimeMillis()) 167 | String.format("%s/hprof-conv %s %s", toolsPath, tmp.getName, hprof).! 168 | tmp.delete() 169 | System.err.println("heap dump written to "+hprof) 170 | file(hprof) 171 | } 172 | 173 | case class Screenshot(r: RenderedImage) { 174 | def toFile(format: String, f: File, str:TaskStreams):File = { ImageIO.write(r, format, f); f } 175 | def toFile(format: String, s: String, str:TaskStreams):File = { 176 | val tstamp = System.currentTimeMillis().toString 177 | val file = new File(String.format("%s-%s.%s", s, tstamp, format)) 178 | str.log.info("screenshot written to "+file.getName) 179 | toFile(format, file, str) 180 | } 181 | def toOutputStream(format: String, o: OutputStream) = ImageIO.write(r, format, o) 182 | } 183 | 184 | def printStackTask = (parsed: TaskKey[String]) => 185 | (parsed, streams) map { (p, s) => 186 | def doPrint(tinfo: ThreadInfo, includeStack: Boolean) { 187 | def status (ti: ThreadInfo) = { 188 | val colorize = (s: String) => s match { 189 | case "running" => scala.Console.GREEN+s+scala.Console.RESET 190 | case _ => s 191 | } 192 | val state = THREAD_STATUS(ti.getStatus+1) 193 | "state: %s, utime: %d, stime: %d, sampled %.1f secs ago".format( 194 | colorize(state), ti.getUtime, ti.getStime, 195 | (System.currentTimeMillis() - ti.getStackCallTime) / 1000f) 196 | } 197 | 198 | s.log.info(tinfo.getThreadName+" ("+status(tinfo)+")") 199 | if (includeStack) 200 | s.log.info(tinfo.getStackTrace.map(_.toString).mkString("\n")) 201 | } 202 | 203 | p match { 204 | case "all" => infos.toList 205 | .sortWith({case((_,i1),(_,i2)) => i1.getUtime > i2.getUtime }) 206 | .foreach({case (_,info) => doPrint(info, false)}) 207 | case _ => doPrint(infos.get(p).getOrElse(sys.error("thread not found")), true) 208 | } 209 | () 210 | } 211 | 212 | def threadListParser = (s: State, app: String, path: String, emu: Boolean) => { 213 | fetchThreads(app, path, emu) 214 | Space ~> infos.map({ case (k, v) => token(k) }) 215 | .reduceLeftOption(_ | _) 216 | .getOrElse(token("")) 217 | } 218 | 219 | lazy val settings: Seq[Setting[_]] = (Seq ( 220 | screenshotDevice <<= (dbPath, streams) map { (p,s) => 221 | screenshot(false, false, p.absolutePath).getOrElse(sys.error("could not get screenshot")).toFile("png", "device", s) 222 | }, 223 | screenshotEmulator <<= (dbPath, streams) map { (p,s) => 224 | screenshot(true, false, p.absolutePath).getOrElse(sys.error("could not get screenshot")).toFile("png", "emulator", s) 225 | }, 226 | hprofEmulator <<= (manifestPackage, dbPath, streams, toolsPath) map { (m,p,s, toolsPath) => 227 | dumpHprof(m, p.absolutePath, true, s)(writeHprof(toolsPath)) 228 | }, 229 | hprofDevice <<= (manifestPackage, dbPath, streams, toolsPath) map { (m,p,s, toolsPath) => 230 | dumpHprof(m, p.absolutePath, false, s)(writeHprof(toolsPath)) 231 | }, 232 | threadsEmulator <<= InputTask( 233 | (resolvedScoped, dbPath) ( (ctx, path) => (s: State) => 234 | threadListParser(s, loadFromContext(manifestPackageName, ctx, s) getOrElse "", path.absolutePath, true))) 235 | (printStackTask), 236 | 237 | threadsDevice <<= InputTask( 238 | (resolvedScoped, dbPath) ( (ctx, path) => (s: State) => 239 | threadListParser(s, loadFromContext(manifestPackageName, ctx, s) getOrElse "", path.absolutePath, false))) 240 | (printStackTask), 241 | 242 | stopBridge <<= (streams) map { (s) => 243 | terminateBridge() 244 | s.log.info("terminated debug bridge. older versions of the SDK might not be "+ 245 | "able to call init() again.") 246 | } 247 | )) 248 | } 249 | -------------------------------------------------------------------------------- /src/main/scala/AndroidDefaults.scala: -------------------------------------------------------------------------------- 1 | package sbtandroid 2 | 3 | import sbt._ 4 | 5 | import Keys._ 6 | import AndroidPlugin._ 7 | import AndroidHelpers._ 8 | 9 | object AndroidDefaults { 10 | val DefaultAaaptName = "aapt" 11 | val DefaultAadbName = "adb" 12 | val DefaultAaidlName = "aidl" 13 | val DefaultDxName = "dx" 14 | val DefaultAndroidManifestName = "AndroidManifest.xml" 15 | val DefaultAndroidJarName = "android.jar" 16 | val DefaultAssetsDirectoryName = "assets" 17 | val DefaultResDirectoryName = "res" 18 | val DefaultGeneratedProguardConfigName = "proguard-generated.txt" 19 | val DefaultManifestSchema = "http://schemas.android.com/apk/res/android" 20 | val DefaultEnvs = List("ANDROID_SDK_HOME", "ANDROID_SDK_ROOT", "ANDROID_HOME") 21 | 22 | lazy val settings: Seq[Setting[_]] = {Seq( 23 | // Command executable names 24 | aaptName := DefaultAaaptName, 25 | adbName := DefaultAadbName, 26 | aidlName := DefaultAaidlName, 27 | dxName := DefaultDxName, 28 | manifestName := DefaultAndroidManifestName, 29 | libraryJarName := DefaultAndroidJarName, 30 | assetsDirectoryName := DefaultAssetsDirectoryName, 31 | generatedProguardConfigName := DefaultGeneratedProguardConfigName, 32 | resDirectoryName := DefaultResDirectoryName, 33 | manifestSchema := DefaultManifestSchema, 34 | envs := DefaultEnvs, 35 | 36 | // A list of modules which are already included in Android 37 | preinstalledModules := Seq[ModuleID]( 38 | ModuleID("org.apache.httpcomponents", "httpcore", null), 39 | ModuleID("org.apache.httpcomponents", "httpclient", null), 40 | ModuleID("org.json", "json" , null), 41 | ModuleID("commons-logging", "commons-logging", null), 42 | ModuleID("commons-codec", "commons-codec", null) 43 | ) 44 | )} 45 | } 46 | -------------------------------------------------------------------------------- /src/main/scala/AndroidEmulator.scala: -------------------------------------------------------------------------------- 1 | package sbtandroid 2 | 3 | import sbt._ 4 | import Keys._ 5 | 6 | import AndroidPlugin._ 7 | import AndroidHelpers.isWindows 8 | 9 | import complete.DefaultParsers._ 10 | 11 | object AndroidEmulator { 12 | private def emulatorStartTask = (parsedTask: TaskKey[String]) => 13 | (parsedTask, toolsPath) map { (avd, toolsPath) => 14 | "%s/emulator -avd %s".format(toolsPath, avd).run 15 | () 16 | } 17 | 18 | private def listDevicesTask: Project.Initialize[Task[Unit]] = (dbPath) map { 19 | _ +" devices" ! 20 | } 21 | 22 | private def killAdbTask: Project.Initialize[Task[Unit]] = (dbPath) map { 23 | _ +" kill-server" ! 24 | } 25 | 26 | private def emulatorStopTask = (dbPath, streams) map { (dbPath, s) => 27 | s.log.info("Stopping emulators") 28 | val serial = "%s -e get-serialno".format(dbPath).!! 29 | "%s -s %s emu kill".format(dbPath, serial).! 30 | () 31 | } 32 | 33 | def installedAvds(sdkHome: File) = (s: State) => { 34 | val avds = ((Path.userHome / ".android" / "avd" * "*.ini") +++ 35 | (if (isWindows) (sdkHome / ".android" / "avd" * "*.ini") 36 | else PathFinder.empty)).get 37 | Space ~> avds.map(f => token(f.base)) 38 | .reduceLeftOption(_ | _).getOrElse(token("none")) 39 | } 40 | 41 | lazy val baseSettings: Seq[Setting[_]] = (Seq( 42 | listDevices <<= listDevicesTask, 43 | killAdb <<= killAdbTask, 44 | emulatorStart <<= InputTask((sdkPath)(installedAvds(_)))(emulatorStartTask), 45 | emulatorStop <<= emulatorStopTask 46 | )) 47 | 48 | lazy val aggregateSettings: Seq[Setting[_]] = Seq( 49 | listDevices, 50 | emulatorStart, 51 | emulatorStop 52 | ) map { aggregate in _ := false } 53 | 54 | lazy val settings: Seq[Setting[_]] = baseSettings ++ aggregateSettings 55 | } 56 | -------------------------------------------------------------------------------- /src/main/scala/AndroidHelpers.scala: -------------------------------------------------------------------------------- 1 | package sbtandroid 2 | 3 | import sbt._ 4 | import Keys._ 5 | 6 | import AndroidPlugin._ 7 | 8 | object AndroidHelpers { 9 | 10 | def directory(path: SettingKey[File]) = path map (IO.createDirectory(_)) 11 | 12 | /** 13 | * Finds out where the Android SDK is located on your system, based on : 14 | * * Environment variables 15 | * * The local.properties files 16 | */ 17 | def determineAndroidSdkPath(envs: Seq[String], dir: File): File = { 18 | // Try to find the SDK path in the default environment variables 19 | val paths = for ( e <- envs; p = System.getenv(e); if p != null) yield p 20 | if (!paths.isEmpty) Path(paths.head).asFile 21 | 22 | // If not found, try to read the `local.properties` file 23 | else { 24 | val local = new File(dir, "local.properties") 25 | if (local.exists()) { 26 | (for (sdkDir <- (for (l <- IO.readLines(local); 27 | if (l.startsWith("sdk.dir"))) 28 | yield l.substring(l.indexOf('=')+1))) 29 | yield new File(sdkDir)).headOption.getOrElse( 30 | sys.error("local.properties did not contain sdk.dir") 31 | ) 32 | 33 | // If nothing is found either, display an error 34 | } else { 35 | sys.error( 36 | "Android SDK not found. You might need to set %s".format(envs.mkString(" or ")) 37 | ) 38 | } 39 | } 40 | } 41 | 42 | /** 43 | * Finds out which versions of the build-tools, if any, is the most recent 44 | */ 45 | def determineBuildToolsVersion(sdkPath: File): Option[String] = { 46 | // Find out which versions of the build tools are installed 47 | val buildToolsPath = (sdkPath / "build-tools") 48 | 49 | // If this path doesn't exist, just set the version to "" 50 | if (!buildToolsPath.exists) None 51 | 52 | // Else, sort the installed versions and take the most recent 53 | else Some(buildToolsPath.listFiles.map(_.name).reduceLeft((s1, s2) => { 54 | 55 | // Convert the version numbers to arrays of integers 56 | val v1 = s1 split '.' map (_.toInt) 57 | val v2 = s2 split '.' map (_.toInt) 58 | 59 | // Compare them 60 | // (Will crash if the version numbers don't contain 3 digits) 61 | if((v1(0) > v2(0)) || 62 | (v1(0) == v2(0) && v1(1) > v2(1)) || 63 | (v1(0) == v2(0) && v1(1) == v2(1) && v1(2) > v2(1))) 64 | s1 else s2 65 | })) 66 | } 67 | 68 | def isWindows = System.getProperty("os.name").startsWith("Windows") 69 | def osBatchSuffix = if (isWindows) ".bat" else "" 70 | 71 | def dxMemoryParameter(javaOpts: String) = { 72 | // per http://code.google.com/p/android/issues/detail?id=4217, dx.bat 73 | // doesn't currently support -JXmx arguments. For now, omit them in windows. 74 | if (isWindows) "" else javaOpts 75 | } 76 | 77 | /** 78 | * Retrieves an attribute from the AndroidManifest.xml file 79 | */ 80 | def usesSdk(mpath: File, schema: String, key: String) = 81 | (manifest(mpath) \ "uses-sdk").head.attribute(schema, key).map(_.text.toInt) 82 | 83 | /** 84 | * Returns the name of the app's main activity. 85 | */ 86 | def launcherActivity(schema: String, amPath: File, mPackage: String) = { 87 | val launcher = for ( 88 | activity <- (manifest(amPath) \\ "activity"); 89 | action <- (activity \\ "action"); 90 | val name = action.attribute(schema, "name").getOrElse(sys.error{ 91 | "action name not defined" 92 | }).text; 93 | if name == "android.intent.action.MAIN" 94 | ) yield { 95 | val act = activity.attribute(schema, "name").getOrElse(sys.error("activity name not defined")).text 96 | if (act.contains(".")) act else mPackage+"."+act 97 | } 98 | launcher.headOption.getOrElse("") 99 | } 100 | 101 | /** 102 | * Loads the AndroidManifest.xml file at path `mpath` 103 | */ 104 | def manifest(mpath: File) = xml.XML.loadFile(mpath) 105 | 106 | /** 107 | * Comparison function for version numbers. 108 | * Returns true if `a1 LT a2` 109 | */ 110 | def compareVersions(s1: String, s2: String) = { 111 | // Split the strings into version numbers 112 | val a1 = s1 split "\\." 113 | val a2 = s2 split "\\." 114 | 115 | // Recursive comparison function 116 | def compare_rec(i: Int): Boolean = { 117 | // Try reading the current index 118 | val v1 = try { Some(a1(i)) } catch { case e: ArrayIndexOutOfBoundsException => None } 119 | val v2 = try { Some(a2(i)) } catch { case e: ArrayIndexOutOfBoundsException => None } 120 | 121 | // Match them 122 | (v1, v2) match { 123 | case (Some(s1), Some(s2)) => { 124 | val i1 = try { Some(s1.toInt) } catch { case e: NumberFormatException => None } 125 | val i2 = try { Some(s2.toInt) } catch { case e: NumberFormatException => None } 126 | (i1, i2) match { 127 | case (Some(u1), Some(u2)) => u1 > u2 || (u1 == u2 && compare_rec(i+1)) 128 | case (None, Some(_)) => false 129 | case (Some(_), None) => true 130 | case (None, None) => s1 > s2 || (s1 == s2 && compare_rec(i+1)) 131 | } 132 | } 133 | 134 | case (None, Some(_)) => false 135 | case (Some(_), None) => true 136 | case (None, None) => true 137 | } 138 | } 139 | compare_rec(0) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/main/scala/AndroidInstall.scala: -------------------------------------------------------------------------------- 1 | package sbtandroid 2 | 3 | import java.util.Properties 4 | import proguard.{Configuration=>ProGuardConfiguration, ProGuard, ConfigurationParser, ConfigurationWriter} 5 | 6 | import sbt._ 7 | import Keys._ 8 | import AndroidPlugin._ 9 | import AndroidHelpers._ 10 | 11 | import java.io.{File => JFile} 12 | 13 | object AndroidInstall { 14 | 15 | /** 16 | * Task that installs a package on the target 17 | */ 18 | private val installTask = 19 | (adbTarget, dbPath, packageApkPath, streams) map { (t, dp, p, s) => 20 | s.log.info("Installing %s".format(p.name)) 21 | t.installPackage(dp, s, p) 22 | () 23 | } 24 | 25 | /** 26 | * Task that uninstalls a package from the target 27 | */ 28 | private val uninstallTask = 29 | (adbTarget, dbPath, manifestPackage, streams) map { (t, dp, p, s) => 30 | s.log.info("Uninstalling %s".format(p)) 31 | t.uninstallPackage(dp, s, p) 32 | () 33 | } 34 | 35 | private def aaptPackageTask: Project.Initialize[Task[File]] = 36 | (aaptPath, manifestPath, resPath, mainAssetsPath, libraryJarPath, resourcesApkPath, streams) map { 37 | (aaptPath, manifestPath, resPath, mainAssetsPath, libraryJarPath, resourcesApkPath, streams) => 38 | 39 | // Make assets directory 40 | mainAssetsPath.mkdirs 41 | 42 | // Resource arguments 43 | val libraryResPathArgs = resPath.flatMap(p => Seq("-S", p.absolutePath)) 44 | 45 | // AAPT command line 46 | val aapt = Seq(aaptPath.absolutePath, "package", 47 | "--auto-add-overlay", "-f", 48 | "-M", manifestPath.head.absolutePath, 49 | "-A", mainAssetsPath.absolutePath, 50 | "-I", libraryJarPath.absolutePath, 51 | "-F", resourcesApkPath.absolutePath) ++ 52 | libraryResPathArgs 53 | 54 | // Package resources 55 | streams.log.info("Packaging resources in " + resourcesApkPath.absolutePath) 56 | streams.log.debug("Running: " + aapt.mkString(" ")) 57 | if (aapt.run(false).exitValue != 0) sys.error("Error packaging resources") 58 | 59 | // Return the path to the resources APK 60 | resourcesApkPath 61 | } 62 | 63 | private def dxTask: Project.Initialize[Task[File]] = 64 | (dxPath, dxMemory, target, proguard, dxInputs, dxPredex, 65 | proguardOptimizations, classDirectory, dxOutputPath, scalaInstance, streams) map { 66 | (dxPath, dxMemory, target, proguard, dxInputs, dxPredex, 67 | proguardOptimizations, classDirectory, dxOutputPath, scalaInstance, streams) => 68 | 69 | // Main dex command 70 | def dexing(inputs: Seq[JFile], output: JFile) { 71 | val uptodate = output.exists && inputs.forall(input => 72 | input.isDirectory match { 73 | case true => 74 | (input ** "*").get.forall(_.lastModified <= output.lastModified) 75 | case false => 76 | input.lastModified <= output.lastModified 77 | } 78 | ) 79 | 80 | if (!uptodate) { 81 | val noLocals = if (proguardOptimizations.isEmpty) "" else "--no-locals" 82 | val dxCmd = (Seq(dxPath.absolutePath, 83 | dxMemoryParameter(dxMemory), 84 | "--dex", noLocals, 85 | "--num-threads="+java.lang.Runtime.getRuntime.availableProcessors, 86 | "--output="+output.getAbsolutePath) ++ 87 | inputs.map(_.absolutePath)).filter(_.length > 0) 88 | streams.log.debug(dxCmd.mkString(" ")) 89 | streams.log.info("Dexing "+output.getAbsolutePath) 90 | streams.log.debug(dxCmd !!) 91 | } else streams.log.debug("DEX file " + output.getAbsolutePath + "is up to date, skipping") 92 | } 93 | 94 | // First, predex the inputs in dxPredex 95 | val dxPredexInputs = dxInputs filter (dxPredex contains _) map { jarPath => 96 | 97 | // Generate the output path 98 | val outputPath = target / (jarPath.getName + ".apk") 99 | 100 | // Predex the library 101 | dexing(Seq(jarPath), outputPath) 102 | 103 | // Return the output path 104 | outputPath 105 | } 106 | 107 | // Non-predexed inputs 108 | val dxClassInputs = dxInputs filterNot (dxPredex contains _) 109 | 110 | // Generate the final DEX 111 | dexing(dxClassInputs +++ dxPredexInputs get, dxOutputPath) 112 | 113 | // Return the path to the generated final DEX file 114 | dxOutputPath 115 | } 116 | 117 | private def proguardTask: Project.Initialize[Task[Option[File]]] = 118 | (proguardConfiguration, proguardOutputPath, streams) map { 119 | (proguardConfiguration, proguardOutputPath, streams) => 120 | 121 | proguardConfiguration map { configFile => 122 | // Execute Proguard 123 | streams.log.info("Executing Proguard with configuration file " + configFile.getAbsolutePath) 124 | 125 | // Parse the configuration 126 | val config = new ProGuardConfiguration 127 | val parser = new ConfigurationParser(configFile, new Properties) 128 | parser.parse(config) 129 | 130 | // Execute ProGuard 131 | val proguard = new ProGuard(config) 132 | proguard.execute 133 | 134 | // Return the proguard-ed output JAR 135 | proguardOutputPath 136 | } 137 | } 138 | 139 | private def proguardConfigurationTask: Project.Initialize[Task[Option[File]]] = 140 | (useProguard, proguardOptimizations, classDirectory, 141 | generatedProguardConfigPath, includedClasspath, providedClasspath, 142 | proguardOutputPath, manifestPackage, proguardOptions, sourceManaged, 143 | proguardInJarsFilter) map { 144 | 145 | (useProguard, proguardOptimizations, classDirectory, 146 | genConfig, includedClasspath, providedClasspath, 147 | proguardOutputPath, manifestPackage, proguardOptions, sourceManaged, 148 | proguardInJarsFilter) => 149 | 150 | if (useProguard) { 151 | 152 | val generatedOptions = 153 | if(genConfig.exists()) 154 | scala.io.Source.fromFile(genConfig).getLines.filterNot(x => x.isEmpty || x.head == '#').toSeq 155 | else Seq() 156 | 157 | val optimizationOptions = if (proguardOptimizations.isEmpty) Seq("-dontoptimize") else proguardOptimizations 158 | val sep = JFile.pathSeparator 159 | 160 | // Input class files 161 | val inClass = "\"" + classDirectory.absolutePath + "\"" 162 | 163 | // Input library JARs to be included in the APK 164 | val inJars = includedClasspath 165 | .map{ jar => "\"%s\"(%s)".format(jar, proguardInJarsFilter(jar).mkString(",")) } 166 | .mkString(sep) 167 | 168 | // Input library JARs to be provided at runtime 169 | val inLibrary = providedClasspath 170 | .map("\"" + _.absolutePath + "\"") 171 | .mkString(sep) 172 | 173 | // Output JAR 174 | val outJar = "\""+proguardOutputPath.absolutePath+"\"" 175 | 176 | // Proguard arguments 177 | val args = ( 178 | "-injars" :: inClass :: 179 | "-injars" :: inJars :: 180 | "-outjars" :: outJar :: 181 | "-libraryjars" :: inLibrary :: 182 | Nil) ++ 183 | generatedOptions ++ 184 | optimizationOptions ++ ( 185 | "-dontwarn" :: "-dontobfuscate" :: 186 | "-dontnote scala.Enumeration" :: 187 | "-dontnote org.xml.sax.EntityResolver" :: 188 | "-keep public class * extends android.app.backup.BackupAgent" :: 189 | "-keep public class * extends android.appwidget.AppWidgetProvider" :: 190 | "-keep class scala.collection.SeqLike { public java.lang.String toString(); }" :: 191 | "-keep class scala.reflect.ScalaSignature" :: 192 | "-keep public class * implements junit.framework.Test { public void test*(); }" :: 193 | """ 194 | -keepclassmembers class * implements java.io.Serializable { 195 | private static final java.io.ObjectStreamField[] serialPersistentFields; 196 | private void writeObject(java.io.ObjectOutputStream); 197 | private void readObject(java.io.ObjectInputStream); 198 | java.lang.Object writeReplace(); 199 | java.lang.Object readResolve(); 200 | } 201 | """ :: Nil) ++ proguardOptions 202 | 203 | // Instantiate the Proguard configuration 204 | val config = new ProGuardConfiguration 205 | new ConfigurationParser(args.toArray[String], new Properties).parse(config) 206 | 207 | // Write that to a file 208 | val configFile = sourceManaged / "proguard.txt" 209 | val writer = new ConfigurationWriter(configFile) 210 | writer.write(config) 211 | writer.close 212 | 213 | // Return the configuration file 214 | Some(configFile) 215 | 216 | } else None 217 | } 218 | 219 | private val apkTask = 220 | (useDebug, packageConfig, streams) map { (debug, c, s) => 221 | val builder = new ApkBuilder(c, debug) 222 | builder.build.fold(sys.error(_), s.log.info(_)) 223 | s.log.debug(builder.outputStream.toString) 224 | c.packageApkPath 225 | } 226 | 227 | lazy val settings: Seq[Setting[_]] = Seq( 228 | 229 | // Resource package generation 230 | aaptPackage <<= aaptPackageTask, 231 | 232 | // Dexing (DX) 233 | dx <<= dxTask, 234 | 235 | // Clean generated APK 236 | cleanApk <<= (packageApkPath) map (IO.delete(_)), 237 | 238 | // Proguard 239 | proguard <<= proguardTask, 240 | proguard <<= proguard dependsOn (compile), 241 | 242 | // Proguard configuration 243 | proguardConfiguration <<= proguardConfigurationTask, 244 | 245 | // Final APK generation 246 | packageConfig <<= 247 | (toolsPath, packageApkPath, resourcesApkPath, dxOutputPath, 248 | nativeDirectories, dxInputs, resourceDirectory) map 249 | (ApkConfig(_, _, _, _, _, _, _)), 250 | 251 | apk <<= apkTask dependsOn (cleanApk, aaptPackage, dx, copyNativeLibraries), 252 | 253 | // Package installation 254 | install <<= installTask dependsOn apk, 255 | 256 | // Package uninstallation 257 | uninstall <<= uninstallTask 258 | ) 259 | } 260 | -------------------------------------------------------------------------------- /src/main/scala/AndroidJavaLayout.scala: -------------------------------------------------------------------------------- 1 | package sbtandroid 2 | 3 | import sbt._ 4 | import Keys._ 5 | import AndroidPlugin._ 6 | 7 | /** Some sensible defaults for building java projects with the plugin */ 8 | object AndroidJavaLayout { 9 | lazy val settings: Seq[Setting[_]] = (Seq( 10 | autoScalaLibrary in GlobalScope := false, 11 | useProguard in Compile := false, 12 | useTypedResources in Compile := false, 13 | useTypedLayouts in Compile := false, 14 | manifestPath in Compile <<= (baseDirectory, manifestName) map((s,m) => Seq(s / m)) map (x=>x), 15 | mainResPath in Compile <<= (baseDirectory, resDirectoryName) (_ / _) map (x=>x), 16 | mainAssetsPath in Compile <<= (baseDirectory, assetsDirectoryName) (_ / _), 17 | javaSource in Compile <<= (baseDirectory) (_ / "src"), 18 | unmanagedBase <<= baseDirectory (_ / "libs") 19 | ) 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/main/scala/AndroidLaunch.scala: -------------------------------------------------------------------------------- 1 | package sbtandroid 2 | 3 | import sbt._ 4 | 5 | import Keys._ 6 | import AndroidPlugin._ 7 | import AndroidHelpers._ 8 | 9 | object AndroidLaunch { 10 | 11 | val startTask = ( 12 | (adbTarget, dbPath, streams, manifestSchema, manifestPackage, manifestPath) map { 13 | (adbTarget, dbPath, streams, manifestSchema, manifestPackage, manifestPath) => 14 | adbTarget.startApp(dbPath, streams, manifestSchema, manifestPackage, manifestPath) 15 | () 16 | } 17 | ) 18 | 19 | lazy val settings: Seq[Setting[_]] = 20 | AndroidInstall.settings ++ 21 | (Seq ( 22 | start <<= startTask, 23 | start <<= start dependsOn install 24 | )) 25 | } 26 | -------------------------------------------------------------------------------- /src/main/scala/AndroidManifestGenerator.scala: -------------------------------------------------------------------------------- 1 | package sbtandroid 2 | 3 | import sbt._ 4 | 5 | import Keys._ 6 | import AndroidPlugin._ 7 | 8 | import scala.xml._ 9 | import scala.xml.transform._ 10 | 11 | object AndroidManifestGenerator { 12 | 13 | /** 14 | * Rewrite rule to make an Android manifest consistent with the project's 15 | * version settings. 16 | */ 17 | case class VersionRule(version: String, versionCode: Int) extends RewriteRule { 18 | val namespacePrefix = "http://schemas.android.com/apk/res/android" 19 | 20 | override def transform(n: scala.xml.Node): Seq[scala.xml.Node] = n match { 21 | 22 | case manifest: Elem if(manifest.label == "manifest") => { 23 | 24 | // Create or replace versionName and versionCode attributes to conform to 25 | // the project's settings. 26 | val verName = 27 | new PrefixedAttribute("android", "versionName", version, Null) 28 | val verCode = 29 | new PrefixedAttribute("android", "versionCode", versionCode.toString, Null) 30 | 31 | // Update the element 32 | manifest % verName % verCode 33 | } 34 | 35 | case other => other 36 | } 37 | } 38 | 39 | /** 40 | * This task transparently takes the AndroidManifest.xml file and applies a 41 | * series of transformations based on the project's settings. 42 | */ 43 | private def generateManifestTask = 44 | (sourceManaged, configuration, manifestTemplatePath, manifestRewriteRules, streams) map { 45 | (sourceManaged, configuration, manifestTemplatePath, rules, streams) => 46 | 47 | // Load the AndroidManifest.xml file as a template 48 | val manifest = XML.loadFile(manifestTemplatePath) 49 | 50 | // Apply transformation rules 51 | val newManifest = new RuleTransformer(rules: _*)(manifest) 52 | 53 | // Create the output file and directories 54 | sourceManaged.mkdirs() 55 | 56 | // This is the path to the manifest 57 | val manifestPath = sourceManaged / "AndroidManifest.xml" 58 | 59 | // Save the final AndroidManifest.xml file 60 | XML.save(manifestPath.absolutePath, newManifest) 61 | streams.log.info("Generated " + manifestPath) 62 | 63 | // Return the path of the generated manifest 64 | Seq(manifestPath) 65 | } 66 | 67 | /** 68 | * Default settings that will override the default static AndroidManifest.xml 69 | * behavior. 70 | */ 71 | lazy val settings: Seq[Setting[_]] = (Seq( 72 | manifestRewriteRules <+= (version, versionCode) map (VersionRule(_, _)), 73 | 74 | manifestTemplateName := "AndroidManifest.xml", 75 | manifestTemplatePath <<= (sourceDirectory, manifestTemplateName)(_/_), 76 | 77 | manifestPath <<= generateManifestTask, 78 | generateManifest <<= generateManifestTask 79 | )) 80 | } 81 | -------------------------------------------------------------------------------- /src/main/scala/AndroidNdk.scala: -------------------------------------------------------------------------------- 1 | package sbtandroid 2 | 3 | import sbt._ 4 | 5 | import Keys._ 6 | import AndroidPlugin._ 7 | 8 | import java.io.File 9 | 10 | /** 11 | * Support for the Android NDK. 12 | * 13 | * Adding support for compilation of C/C++ sources using the NDK. 14 | * 15 | * Adapted from work by Daniel Solano Gómez 16 | * 17 | * @author Daniel Solano Gómez, Martin Kneissl. 18 | */ 19 | object AndroidNdk { 20 | /** The default name for the 'ndk-build' tool. */ 21 | val DefaultNdkBuildName = "ndk-build" 22 | /** The default directory name for native sources. */ 23 | val DefaultJniDirectoryName = "jni" 24 | /** The default directory name for compiled native objects. */ 25 | val DefaultObjDirectoryName = "obj" 26 | /** The default directory name for compiled libraries. */ 27 | val DefaultLibDirectoryName = "lib" 28 | /** The list of environment variables to check for the NDK. */ 29 | val DefaultEnvs = List("ANDROID_NDK_HOME", "ANDROID_NDK_ROOT") 30 | /** The make environment variable name for the javah generated header directory. */ 31 | val DefaultJavahOutputEnv = "SBT_MANAGED_JNI_INCLUDE" 32 | /** The make environment variable name for the native libraries in lib/ */ 33 | val DefaultNdkUnmanagedEnv = "SBT_UNMANAGED_NATIVE" 34 | 35 | /** 36 | * Default NDK settings 37 | */ 38 | lazy val globalSettings: Seq[Setting[_]] = (Seq ( 39 | 40 | // Options for ndk-build 41 | ndkBuildName := DefaultNdkBuildName, 42 | ndkJniDirectoryName := DefaultJniDirectoryName, 43 | ndkObjDirectoryName := DefaultObjDirectoryName, 44 | ndkLibDirectoryName := DefaultLibDirectoryName, 45 | ndkUnmanagedEnv := DefaultNdkUnmanagedEnv, 46 | ndkEnvs := DefaultEnvs, 47 | 48 | // Options for javah 49 | javahName := "javah", 50 | javahOutputEnv := DefaultJavahOutputEnv, 51 | javahOutputFile := None, 52 | 53 | // Path to the ndk-build executable 54 | ndkBuildPath <<= (ndkEnvs, ndkBuildName) { (envs, ndkBuildName) => 55 | val paths = for { 56 | e <- envs 57 | p = System.getenv(e) 58 | if p != null 59 | b = new File(p, ndkBuildName) 60 | if b.canExecute 61 | } yield b 62 | paths.headOption 63 | }, 64 | 65 | // Path to the javah executable 66 | javahPath <<= (javaHome, javahName) apply { (home, name) => 67 | home map ( h => (h / "bin" / name).absolutePath ) getOrElse name 68 | }, 69 | 70 | // List of classes against which we run javah 71 | jniClasses := Seq.empty 72 | )) 73 | 74 | /** 75 | * NDK-related paths 76 | */ 77 | lazy val pathSettings: Seq[Setting[_]] = (Seq ( 78 | 79 | // Path to the JNI sources 80 | ndkJniSourcePath <<= (sourceDirectory, ndkJniDirectoryName) (_ / _), 81 | 82 | // Path to the output .so libraries 83 | ndkNativeOutputPath <<= (crossTarget, ndkLibDirectoryName, configuration) (_ / _ / _.name), 84 | 85 | // Path to the compiled object files 86 | ndkNativeObjectPath <<= (crossTarget, ndkObjDirectoryName, configuration) (_ / _ / _.name), 87 | 88 | // Path to the javah include directory 89 | javahOutputDirectory <<= (sourceManaged, ndkJniDirectoryName) (_ / _) 90 | 91 | )) 92 | 93 | private def split(file: File) = { 94 | val parentsBottomToTop = Iterator.iterate(file)(_.getParentFile).takeWhile(_ != null).map(_.getName).toSeq 95 | parentsBottomToTop.reverse 96 | } 97 | 98 | private def compose(parent: File, child: File): File = { 99 | if (child.isAbsolute) { 100 | child 101 | } else { 102 | split(child).foldLeft(parent)(new File(_,_)) 103 | } 104 | } 105 | 106 | private def javahTask( 107 | javahPath: String, 108 | classpath: Seq[File], 109 | classes: Seq[String], 110 | outputDirectory: File, 111 | outputFile: Option[File], 112 | streams: TaskStreams) { 113 | 114 | val log = streams.log 115 | if (classes.isEmpty) { 116 | log.debug("No JNI classes, skipping javah") 117 | 118 | } else { 119 | outputDirectory.mkdirs() 120 | 121 | val classpathArgument = classpath.map(_.getAbsolutePath()).mkString(File.pathSeparator) 122 | val outputArguments = outputFile match { 123 | case Some(file) => 124 | val outputFile = compose(outputDirectory, file) 125 | // Neither javah nor RichFile.relativeTo will work unless the directories exist. 126 | Option(outputFile.getParentFile) foreach (_.mkdirs()) 127 | if (! (outputFile relativeTo outputDirectory).isDefined) { 128 | log.warn("javah output file [" + outputFile + "] is not within javah output directory [" + 129 | outputDirectory + "], continuing anyway") 130 | } 131 | 132 | Seq("-o", outputFile.absolutePath) 133 | case None => Seq("-d", outputDirectory.absolutePath) 134 | } 135 | 136 | val javahCommandLine = Seq( 137 | javahPath, 138 | "-classpath", classpathArgument) ++ 139 | outputArguments ++ classes 140 | 141 | log.debug("Running javah: " + (javahCommandLine mkString " ")) 142 | val exitCode = Process(javahCommandLine) ! log 143 | 144 | if (exitCode != 0) { 145 | sys.error("javah exited with " + exitCode) 146 | } 147 | } 148 | } 149 | 150 | private def runNdkBuild( 151 | ndkBuildPath: Option[File], 152 | ndkUnmanagedEnv: String, 153 | javahOutputEnv: String, 154 | javahOutputDirectory: File, 155 | ndkNativeObjectPath: File, 156 | ndkNativeOutputPath: File, 157 | ndkJniSourcePath: File, 158 | envs: Seq[String], 159 | streams: TaskStreams, 160 | targets: String*): Seq[File] = { 161 | 162 | if (ndkJniSourcePath.exists) { 163 | 164 | // Arch-dependant output directory 165 | val unmanagedArch = ndkNativeOutputPath / "${TARGET_ARCH_ABI}" 166 | 167 | // Source root for ndk-build 168 | val sourceBase = ndkJniSourcePath.getParentFile 169 | 170 | // NDK-Build path 171 | val ndkBuildTool = ndkBuildPath getOrElse (sys.error("Android NDK not found. " + 172 | "You might need to set " + envs.mkString(" or "))) 173 | 174 | // Create the ndk-build command 175 | val ndkBuild = ( 176 | ndkBuildTool.absolutePath :: 177 | "-C" :: sourceBase.absolutePath :: 178 | (javahOutputEnv + "=" + javahOutputDirectory.absolutePath) :: 179 | (ndkUnmanagedEnv + "=" + unmanagedArch.absolutePath) :: 180 | ("NDK_APP_OUT=" + ndkNativeObjectPath.absolutePath) :: 181 | ("NDK_APP_DST_DIR=" + unmanagedArch.absolutePath) :: 182 | targets.toList 183 | ) 184 | 185 | // Run that command 186 | streams.log.debug("Running ndk-build: " + ndkBuild.mkString(" ")) 187 | val exitValue = ndkBuild.run(false).exitValue 188 | if (exitValue != 0) sys.error("ndk-build failed with nonzero exit code (" + exitValue + ")") 189 | 190 | // Return the output path 191 | Seq(ndkNativeOutputPath) 192 | 193 | // Return nothing if there is no source 194 | } else { 195 | streams.log.debug("No JNI sources found, skipping ndk-build") 196 | Seq.empty 197 | } 198 | } 199 | 200 | private val ndkBuildTask = 201 | (ndkBuildPath, ndkUnmanagedEnv, javahOutputEnv, javahOutputDirectory, 202 | ndkNativeObjectPath, ndkNativeOutputPath, ndkJniSourcePath, ndkEnvs, streams) map ( 203 | runNdkBuild(_, _, _, _, _, _, _, _, _) 204 | ) 205 | 206 | private val ndkCleanTask = 207 | (ndkBuildPath, ndkUnmanagedEnv, javahOutputEnv, javahOutputDirectory, 208 | ndkNativeObjectPath, ndkNativeOutputPath, ndkJniSourcePath, ndkEnvs, streams) map { 209 | (bp, ue, joe, jod, nob, nou, nj, env, s) => 210 | runNdkBuild(bp, ue, joe, jod, nob, nou, nj, env, s, "clean"); () 211 | } 212 | 213 | lazy val settings: Seq[Setting[_]] = pathSettings ++ Seq ( 214 | 215 | // Header generation 216 | javah <<= ( 217 | (compile), 218 | javahPath, 219 | (classDirectory), (internalDependencyClasspath), (externalDependencyClasspath), 220 | jniClasses, 221 | javahOutputDirectory, javahOutputFile, 222 | streams) map (( 223 | _, // we only depend on a side effect (built classes) of compile 224 | javahPath, 225 | classDirectory, internalDependencyClasspath, externalDependencyClasspath, 226 | jniClasses, 227 | javahOutputDirectory, 228 | javahOutputFile, 229 | streams) => 230 | javahTask( 231 | javahPath, 232 | Seq(classDirectory) ++ internalDependencyClasspath.files ++ externalDependencyClasspath.files, 233 | jniClasses, 234 | javahOutputDirectory, javahOutputFile, 235 | streams) 236 | ), 237 | 238 | // NDK build task 239 | ndkBuild <<= ndkBuildTask, 240 | ndkBuild <<= ndkBuild dependsOn javah, 241 | 242 | // Add ndk-build to the build products 243 | nativeDirectories <++= ndkBuild, 244 | 245 | // Clean tasks 246 | javahClean <<= (javahOutputDirectory) map IO.delete, 247 | ndkClean <<= ndkCleanTask, 248 | clean <<= clean.dependsOn(ndkClean, javahClean) 249 | ) 250 | } 251 | -------------------------------------------------------------------------------- /src/main/scala/AndroidPath.scala: -------------------------------------------------------------------------------- 1 | package sbtandroid 2 | 3 | import sbt._ 4 | 5 | import Keys._ 6 | import AndroidPlugin._ 7 | import AndroidHelpers._ 8 | 9 | object AndroidPath { 10 | private def determineBuildToolsVersion(sdkPath: File): Option[String] = { 11 | // Find out which versions of the build tools are installed 12 | val buildToolsPath = (sdkPath / "build-tools") 13 | 14 | // If this path doesn't exist, just set the version to "" 15 | if (!buildToolsPath.exists) None 16 | 17 | // Else, sort the installed versions and take the most recent 18 | else { 19 | // List the files in the build-tools directory 20 | val files = buildToolsPath.listFiles.toList 21 | 22 | // List the different versions 23 | val versions = files map (_.name) filterNot (_.startsWith(".")) 24 | 25 | // Sort the versions by the most recent 26 | val sorted = versions.sortWith(compareVersions(_, _)) 27 | 28 | // Return the most recent 29 | sorted.headOption 30 | } 31 | } 32 | 33 | lazy val settings: Seq[Setting[_]] = { 34 | AndroidDefaults.settings ++ Seq ( 35 | osDxName <<= (dxName) (_ + osBatchSuffix), 36 | 37 | toolsPath <<= (sdkPath) (_ / "tools"), 38 | dbPath <<= (platformToolsPath, adbName) (_ / _), 39 | platformToolsPath <<= (sdkPath) (_ / "platform-tools"), 40 | buildToolsVersion <<= (sdkPath) (determineBuildToolsVersion _), 41 | buildToolsPath <<= (sdkPath, platformToolsPath, buildToolsVersion) { (sdkPath, platformToolsPath, buildToolsVersion) => 42 | buildToolsVersion match { 43 | case Some(v) => sdkPath / "build-tools" / v 44 | case None => platformToolsPath 45 | } 46 | }, 47 | aaptPath <<= (buildToolsPath, aaptName) (_ / _), 48 | idlPath <<= (buildToolsPath, aidlName) (_ / _), 49 | dxPath <<= (buildToolsPath, osDxName) (_ / _), 50 | 51 | sdkPath <<= (envs, baseDirectory) { determineAndroidSdkPath(_, _) }, 52 | 53 | // Add the Google repository 54 | resolvers <+= (sdkPath) { p => "Google Repository" at ( 55 | (p / "extras" / "google" / "m2repository").toURI.toString) }, 56 | resolvers <+= (sdkPath) { p => "Android Support Repository" at ( 57 | (p / "extras" / "android" / "m2repository").toURI.toString) } 58 | ) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/scala/AndroidPlugin.scala: -------------------------------------------------------------------------------- 1 | package sbtandroid 2 | 3 | import sbt._ 4 | import Keys._ 5 | import Defaults._ 6 | 7 | import AndroidHelpers.isWindows 8 | import complete.DefaultParsers._ 9 | import scala.xml.transform.RewriteRule 10 | 11 | object AndroidPlugin extends Plugin { 12 | 13 | /*************************************** 14 | * Default configurations and projects * 15 | ***************************************/ 16 | 17 | // Standard configurations 18 | val Preload = config("preload") 19 | val Release = config("release") 20 | 21 | // Android default targets 22 | val Target = AndroidDefaultTargets 23 | 24 | // Standard projects 25 | val AndroidProject = AndroidProjects.Standard 26 | val AndroidTestProject = AndroidProjects.Test 27 | 28 | // Standard configurations 29 | lazy val androidTest = AndroidTestProject.defaults 30 | lazy val androidDefaults = AndroidProject.defaults 31 | 32 | // Additional configuration for those using Java/Ant projects 33 | lazy val androidJavaLayout: Seq[Setting[_]] = 34 | AndroidJavaLayout.settings 35 | 36 | // Android SDK and emulator tasks/settings will be automatically loaded 37 | // for every project. 38 | override lazy val settings: Seq[Setting[_]] = 39 | AndroidPath.settings ++ AndroidEmulator.settings 40 | 41 | /****************** 42 | * Helper methods * 43 | ******************/ 44 | 45 | // ApkLib and AAR artifact definitions 46 | def apklib(module: ModuleID) = module artifacts(Artifact(module.name, "apklib", "apklib")) 47 | def aarlib(module: ModuleID) = module artifacts(Artifact(module.name, "aar", "aar")) 48 | 49 | // Common module filters 50 | def filterFilename(filename: String) = (f: Attributed[File]) => f.data.name contains filename 51 | def filterFile(file: File) = (f: Attributed[File]) => f.data == file 52 | def filterName(name: String) = (f: Attributed[File]) => f.get(moduleID.key) match { 53 | case Some(n) => n.name contains name 54 | case None => false 55 | } 56 | def filterModule(module: ModuleID) = (f: Attributed[File]) => f.get(moduleID.key) match { 57 | case Some(m) => m == module 58 | case None => false 59 | } 60 | 61 | /********************** 62 | * Public plugin keys * 63 | **********************/ 64 | 65 | /** Android target **/ 66 | val adbTarget = SettingKey[AndroidTarget]("adb-target", "Current Android target (device or emulator) connected to ADB") 67 | 68 | /** User Defines */ 69 | val platformName = SettingKey[String]("platform-name", "Targetted android platform") 70 | val keyalias = SettingKey[String]("key-alias") 71 | val versionCode = SettingKey[Int]("version-code") 72 | val versionName = TaskKey[String]("version-name") 73 | 74 | /** Packaging settings **/ 75 | val useProguard = SettingKey[Boolean]("use-proguard", "Use Proguard to package the app") 76 | val usePreloaded = SettingKey[Boolean]("use-preloaded", "Use preloaded libraries for development") 77 | val useDebug = SettingKey[Boolean]("use-debug", "Use debug settings when building an APK") 78 | val useTypedResources = SettingKey[Boolean]("use-typed-resources", "Use typed resources") 79 | val useTypedLayouts = SettingKey[Boolean]("use-typed-layouts", "Use typed layouts") 80 | 81 | /** ApkLib dependencies */ 82 | case class LibraryProject(pkgName: String, manifest: File, sources: Set[File], resDir: Option[File], assetsDir: Option[File]) 83 | val apklibPackage = TaskKey[File]("apklib-package") 84 | val apklibDependencies = TaskKey[Seq[LibraryProject]]("apklib-dependencies", "Unpack apklib dependencies") 85 | val apklibBaseDirectory = SettingKey[File]("apklib-base-directory", "Base directory for the ApkLib dependencies") 86 | val apklibSourceManaged = SettingKey[File]("apklib-source-managed", "Base directory for the ApkLib sources") 87 | val apklibResourceManaged = SettingKey[File]("apklib-resource-managed", "Base directory for the resources included in the ApkLibs") 88 | val apklibSources = TaskKey[Seq[File]]("apklib-sources", "Enumerate Java sources from apklibs") 89 | 90 | /** AAR dependencies */ 91 | val aarlibDependencies = TaskKey[Seq[LibraryProject]]("aarlib-dependencies", "Unpack aarlib dependencies") 92 | val aarlibBaseDirectory = SettingKey[File]("aarlib-base-directory", "Base directory for the aarLib dependencies") 93 | val aarlibLibManaged = SettingKey[File]("aarlib-lib-managed", "Base directoyr for the aarLib JAR libraries") 94 | val aarlibResourceManaged = SettingKey[File]("aarlib-resource-managed", "Base directory for the resources included in the aarLibs") 95 | 96 | /** General inputs for the APK **/ 97 | val inputClasspath = TaskKey[Seq[File]]("input-classpath", "All the classpath entries needed by the APK") 98 | val includedClasspath = TaskKey[Seq[File]]("included-classpath", "Classpath entries included in the final APK") 99 | val providedClasspath = TaskKey[Seq[File]]("provided-classpath", "Classpath entries provided by the running target") 100 | 101 | /** Proguard Settings **/ 102 | val proguardInJarsFilter = SettingKey[File => Traversable[String]]("proguard-in-jars-filter") 103 | val proguardOptions = SettingKey[Seq[String]]("proguard-options") 104 | val proguardOptimizations = SettingKey[Seq[String]]("proguard-optimizations") 105 | val proguardOutputPath = SettingKey[File]("proguard-output-path", "Path to Proguard's output JAR") 106 | val proguardConfiguration = TaskKey[Option[File]]("proguard-configuration", "Path to the Proguard configuration file") 107 | val proguard = TaskKey[Option[File]]("proguard", "Run Proguard on the class files") 108 | 109 | /** Dexing **/ 110 | val dxOutputPath = SettingKey[File]("dx-output-path") 111 | val dxInputs = TaskKey[Seq[File]]("dx-inputs", "Input class files included in the final APK") 112 | val dxPredex = TaskKey[Seq[File]]("dx-predex", "Paths that will be predexed before generating the final DEX") 113 | val dx = TaskKey[File]("dx", "Convert class files to DEX files") 114 | 115 | /** APK Generation **/ 116 | val apk = TaskKey[File]("apk", "Package and sign with a debug key.") 117 | val aaptPackage = TaskKey[File]("aapt-package", "Package resources and assets.") 118 | val cleanApk = TaskKey[Unit]("clean-apk", "Remove apk package") 119 | 120 | /** Install Scala on device/emulator **/ 121 | val preloadFilters = SettingKey[Seq[Attributed[File] => Boolean]]("preload-filters", "Filters the libraries that are to be preloaded") 122 | val preloadDevice = TaskKey[Unit]("preload-device", "Setup device for development by uploading predexed libraries") 123 | val preloadEmulator = InputKey[Unit]("preload-emulator", "Setup emulator for development by uploading predexed libraries") 124 | 125 | /** Installable Tasks */ 126 | val install = TaskKey[Unit]("install") 127 | val uninstall = TaskKey[Unit]("uninstall") 128 | 129 | /** Launch Tasks */ 130 | val start = TaskKey[Unit]("start", "Start package on device after installation") 131 | 132 | /** Modules that are preloaded on the device **/ 133 | val preinstalledModules = SettingKey[Seq[ModuleID]]("preinstalled-modules") 134 | 135 | /** Default Settings */ 136 | val adbName = SettingKey[String]("adb-name", "Name of the ADB command") 137 | val aaptName = SettingKey[String]("aapt-name") 138 | val aidlName = SettingKey[String]("aidl-name") 139 | val dxName = SettingKey[String]("dx-name") 140 | val manifestName = SettingKey[String]("manifest-name") 141 | val libraryJarName = SettingKey[String]("library-jar-name") 142 | val assetsDirectoryName = SettingKey[String]("assets-dir-name") 143 | val resDirectoryName = SettingKey[String]("res-dir-name") 144 | val classesMinJarName = SettingKey[String]("classes-min-jar-name") 145 | val classesDexName = SettingKey[String]("classes-dex-name") 146 | val resourcesApkName = SettingKey[String]("resources-apk-name") 147 | val generatedProguardConfigName = SettingKey[String]("generated-proguard-config-name") 148 | val dxMemory = SettingKey[String]("dx-memory") 149 | val manifestSchema = SettingKey[String]("manifest-schema") 150 | val envs = SettingKey[Seq[String]]("envs") 151 | val packageApkName = TaskKey[String]("package-apk-name") 152 | val packageApkLibName = TaskKey[String]("package-apklib-name") 153 | val osDxName = SettingKey[String]("os-dx-name") 154 | 155 | /** Path Settings */ 156 | val sdkPath = SettingKey[File]("sdk-path") 157 | val platformToolsPath = SettingKey[File]("platform-tools-path") 158 | val buildToolsVersion = SettingKey[Option[String]]("build-tools-version") 159 | val buildToolsPath = SettingKey[File]("build-tools-path") 160 | val toolsPath = SettingKey[File]("tools-path") 161 | val dbPath = SettingKey[File]("db-path") 162 | val aaptPath = SettingKey[File]("apt-path") 163 | val idlPath = SettingKey[File]("idl-path") 164 | val dxPath = SettingKey[File]("dx-path") 165 | val libraryJarPath = SettingKey[File]("library-jary-path") 166 | 167 | /** Base app manifest settings */ 168 | val platformPath = SettingKey[File]("platform-path") 169 | val manifestPackage = TaskKey[String]("manifest-package") 170 | val manifestPackageName = TaskKey[String]("manifest-package-name") 171 | val minSdkVersion = TaskKey[Option[Int]]("min-sdk-version") 172 | val maxSdkVersion = TaskKey[Option[Int]]("max-sdk-version") 173 | 174 | /** Project paths */ 175 | val manifestPath = TaskKey[Seq[File]]("manifest-path") 176 | val mainAssetsPath = SettingKey[File]("main-asset-path") 177 | val mainResPath = TaskKey[File]("main-res-path") 178 | val resPath = TaskKey[Seq[File]]("res-path") 179 | val managedJavaPath = SettingKey[File]("managed-java-path") 180 | val managedScalaPath = SettingKey[File]("managed-scala-path") 181 | val resourcesApkPath = SettingKey[File]("resources-apk-path") 182 | val generatedProguardConfigPath = SettingKey[File]("generated-proguard-config-path") 183 | val packageApkPath = TaskKey[File]("package-apk-path") 184 | val packageApkLibPath = TaskKey[File]("package-apklib-path") 185 | 186 | /** Native libraries */ 187 | val unmanagedNativePath = SettingKey[File]("unmanaged-native-path") 188 | val managedNativePath = SettingKey[File]("managed-native-path") 189 | val nativeDirectories = TaskKey[Seq[File]]("native-directories") 190 | 191 | /** Install Settings */ 192 | val packageConfig = TaskKey[ApkConfig]("package-config", 193 | "Generates the APK configuration") 194 | 195 | /** Typed Resource Settings */ 196 | val typedResource = TaskKey[File]("typed-resource", 197 | """Typed resource file to be generated, also includes 198 | interfaces to access these resources.""") 199 | val typedLayouts = TaskKey[File]("typed-layouts", 200 | """Typed resource file to be generated, also includes 201 | interfaces to access these resources.""") 202 | val layoutResources = TaskKey[Seq[File]]("layout-resources", 203 | """All files that are in res/layout. They will 204 | be accessable through TR.layouts._""") 205 | 206 | /** Market Publish Settings */ 207 | val keystorePath = SettingKey[File]("key-store-path") 208 | val zipAlignPath = SettingKey[File]("zip-align-path", "Path to zipalign") 209 | val packageAlignedName = TaskKey[String]("package-aligned-name") 210 | val packageAlignedPath = TaskKey[File]("package-aligned-path") 211 | 212 | val copyNativeLibraries = TaskKey[Unit]("copy-native-libraries", "Copy native libraries added to libraryDependencies") 213 | 214 | /********************* 215 | * Source generators * 216 | *********************/ 217 | 218 | val aaptGenerate = TaskKey[Seq[File]]("aapt-generate", "Generate R.java") 219 | val aidlGenerate = TaskKey[Seq[File]]("aidl-generate", 220 | "Generate Java classes from .aidl files.") 221 | val generateTypedResources = TaskKey[Seq[File]]("generate-typed-resources", 222 | """Produce a file TR.scala that contains typed 223 | references to layout resources.""") 224 | val generateTypedLayouts = TaskKey[Seq[File]]("generate-typed-layouts", 225 | """Produce a file typed_resource.scala that contains typed 226 | references to layout resources.""") 227 | val generateManifest = TaskKey[Seq[File]]("generate-manifest", 228 | """Generates a customized AndroidManifest.xml with 229 | current build number and debug settings.""") 230 | 231 | /********************** 232 | * Manifest generator * 233 | **********************/ 234 | 235 | val manifestTemplateName = SettingKey[String]("manifest-template-name") 236 | val manifestTemplatePath = SettingKey[File]("manifest-template-path") 237 | val manifestRewriteRules = TaskKey[Seq[RewriteRule]]("manifest-rewrite-rules", 238 | "Rules for transforming the contents of AndroidManifest.xml based on the project state and settings.") 239 | 240 | /******************* 241 | * Debugging tasks * 242 | *******************/ 243 | 244 | val stopBridge = TaskKey[Unit]("stop-bridge", 245 | "Terminates the ADB debugging bridge") 246 | val screenshotEmulator = TaskKey[File]("screenshot-emulator", 247 | "Take a screenshot from the emulator") 248 | val screenshotDevice = TaskKey[File]("screenshot-device", 249 | "Take a screenshot from the device") 250 | 251 | // hprof tasks are Unit because of async nature 252 | val hprofEmulator = TaskKey[Unit]("hprof-emulator", 253 | "Take a dump of the current heap from the emulator") 254 | val hprofDevice = TaskKey[Unit]("hprof-device", 255 | "Take a dump of the current heap from the device") 256 | 257 | val threadsEmulator = InputKey[Unit]("threads-emulator", 258 | "Show thread dump from the emulator") 259 | val threadsDevice = InputKey[Unit]("threads-device", 260 | "Show thread dump from the device") 261 | 262 | /*********************** 263 | * Store release tasks * 264 | ***********************/ 265 | 266 | val release = TaskKey[File]("release", "Prepare a release APK for Store publication.") 267 | val zipAlign = TaskKey[File]("zip-align", "Run zipalign on signed jar.") 268 | val signRelease = TaskKey[File]("sign-release", "Sign with key alias using key-alias and keystore path.") 269 | val cleanAligned = TaskKey[Unit]("clean-aligned", "Remove zipaligned jar") 270 | 271 | /****************** 272 | * Emulator tasks * 273 | ******************/ 274 | 275 | val emulatorStart = InputKey[Unit]("emulator-start", 276 | "Launches a user specified avd") 277 | val emulatorStop = TaskKey[Unit]("emulator-stop", 278 | "Kills the running emulator.") 279 | val listDevices = TaskKey[Unit]("list-devices", 280 | "List devices from the adb server.") 281 | val killAdb = TaskKey[Unit]("kill-server", 282 | "Kill the adb server if it is running.") 283 | 284 | /************** 285 | * Test tasks * 286 | **************/ 287 | 288 | val testRunner = TaskKey[String]("test-runner", "get the current test runner") 289 | val testEmulator = TaskKey[Unit]("test-emulator", "runs tests in emulator") 290 | val testDevice = TaskKey[Unit]("test-device", "runs tests on device") 291 | val testOnlyEmulator = InputKey[Unit]("test-only-emulator", "run a single test on emulator") 292 | val testOnlyDevice = InputKey[Unit]("test-only-device", "run a single test on device") 293 | 294 | /******************** 295 | * Password managed * 296 | ********************/ 297 | 298 | val cachePasswords = SettingKey[Boolean]("cache-passwords", "Cache passwords") 299 | val clearPasswords = TaskKey[Unit]("clear-passwords", "Clear cached passwords") 300 | 301 | /******************** 302 | * Android NDK keys * 303 | ********************/ 304 | 305 | val ndkBuildName = SettingKey[String]("ndk-build-name", "Name for the 'ndk-build' tool") 306 | val ndkBuildPath = SettingKey[Option[File]]("ndk-build-path", "Path to the 'ndk-build' tool") 307 | 308 | val ndkLibDirectoryName = SettingKey[String]("ndk-lib-directory-name", "Directory name for compiled native libraries.") 309 | val ndkJniDirectoryName = SettingKey[String]("ndk-jni-directory-name", "Directory name for native sources.") 310 | val ndkObjDirectoryName = SettingKey[String]("ndk-obj-directory-name", "Directory name for compiled native objects.") 311 | val ndkUnmanagedEnv = SettingKey[String]("ndk-unmanaged-env", 312 | "Name of the make environment variable to bind to the unmanaged-base directory") 313 | val ndkEnvs = SettingKey[Seq[String]]("ndk-envs", "List of environment variables to check for the NDK.") 314 | 315 | val ndkJniSourcePath = SettingKey[File]("jni-source-path", "Path to native sources. (with Android.mk)") 316 | val ndkNativeOutputPath = SettingKey[File]("native-output-path", "NDK output path") 317 | val ndkNativeObjectPath = SettingKey[File]("native-object-path", "Path to the compiled native objects") 318 | 319 | val ndkBuild = TaskKey[Seq[File]]("ndk-build", "Compile native C/C++ sources.") 320 | val ndkClean = TaskKey[Unit]("ndk-clean", "Clean resources built from native C/C++ sources.") 321 | 322 | /************** 323 | * Javah keys * 324 | **************/ 325 | 326 | val javahName = SettingKey[String]("javah-name", "The name of the javah command for generating JNI headers") 327 | val javahPath = SettingKey[String]("javah-path", "The path to the javah executable") 328 | val javah = TaskKey[Unit]("javah", "Produce C headers from Java classes with native methods") 329 | val javahClean = TaskKey[Unit]("javah-clean", "Clean C headers built from Java classes with native methods") 330 | 331 | val javahOutputDirectory = SettingKey[File]("javah-output-directory", 332 | "The directory where JNI headers are written to.") 333 | val javahOutputFile = SettingKey[Option[File]]("javah-output-file", 334 | "filename for the generated C header, relative to javah-output-directory") 335 | val javahOutputEnv = SettingKey[String]("javah-output-env", 336 | "Name of the make environment variable to bind to the javah-output-directory") 337 | 338 | val jniClasses = SettingKey[Seq[String]]("jni-classes", 339 | "Fully qualified names of classes with native methods for which JNI headers are to be generated.") 340 | 341 | /************************ 342 | * IntelliJ integration * 343 | ************************/ 344 | 345 | val ideaConfiguration = SettingKey[Configuration]("idea-configuration", "Configuration used by sbtidea to generate the IntelliJ project") 346 | } 347 | -------------------------------------------------------------------------------- /src/main/scala/AndroidPreload.scala: -------------------------------------------------------------------------------- 1 | package sbtandroid 2 | 3 | import sbt._ 4 | import Keys._ 5 | import AndroidPlugin._ 6 | import AndroidHelpers._ 7 | 8 | import scala.xml._ 9 | import scala.xml.transform._ 10 | 11 | object AndroidPreload { 12 | 13 | case class Library(val localJar: File, val name: String, val version: String = "unknown") { 14 | val fullName = name + "-" + version 15 | val deviceJarPath = "/system/framework/%s.jar".format(fullName) 16 | val devicePermissionPath = "/system/etc/permissions/%s.xml".format(fullName) 17 | 18 | def usesTag = 19 | 22 | 23 | def permissionTag = 24 | 25 | 28 | 29 | } 30 | 31 | def androidTarget(implicit emulator: Boolean) = 32 | if (emulator) AndroidDefaultTargets.Emulator else AndroidDefaultTargets.Device 33 | 34 | private def deviceDesignation(implicit emulator: Boolean) = 35 | if (emulator) "emulator" else "device" 36 | 37 | /** 38 | * Rewrite rule to add the uses-library tag to the Android manifest if the 39 | * preloaded library is needed. 40 | */ 41 | case class UsesLibraryRule(use: Boolean, libraries: Seq[Library]) extends RewriteRule { 42 | val namespacePrefix = "http://schemas.android.com/apk/res/android" 43 | 44 | override def transform(n: scala.xml.Node): Seq[scala.xml.Node] = n match { 45 | case Elem(namespace, "application", attribs, 46 | scope, children @ _*) if (use) => { 47 | 48 | // Create a new uses-library tag 49 | val tags = libraries map { _.usesTag } 50 | 51 | // Update the element 52 | Elem(namespace, "application", attribs, scope, children ++ tags: _*) 53 | } 54 | 55 | case other => other 56 | } 57 | } 58 | 59 | /**************** 60 | * State checks * 61 | ****************/ 62 | 63 | private def checkFileExists (db: File, s: TaskStreams, filename: String)(implicit emulator: Boolean) = { 64 | 65 | // Run the `ls` command on the device/emulator 66 | val flist = androidTarget.run(db, s, "shell", "ls", filename, "2>/dev/null") 67 | 68 | // Check if we found the file 69 | val found = flist.contains(filename) 70 | 71 | // Inform the user 72 | s.log.debug ("File " + filename + 73 | (if (found) " found on " else " does not exist on ") + deviceDesignation) 74 | 75 | // Return `true` if the file has been found 76 | found 77 | } 78 | 79 | private def checkPreloadedLibraryVersion (db: File, s: TaskStreams, library: Library)(implicit emulator: Boolean) = { 80 | import scala.xml._ 81 | 82 | // Retrieve the contents of the permission file 83 | val permissions = androidTarget.run(db, s, "shell", "cat " + library.devicePermissionPath) 84 | 85 | // Parse the library file 86 | val preloadedFile = ( 87 | try { Some(XML.loadString(permissions) \\ "permissions" \\ "library" \\ "@file") } 88 | catch { case _ => None } 89 | 90 | // Convert the XML node to a String 91 | ).map(_.text) 92 | 93 | // Check if this is the right library version 94 | .filter(_ == library.deviceJarPath) 95 | 96 | // Check if the library is present 97 | .filter(checkFileExists(db, s, _)) 98 | 99 | // Inform the user 100 | preloadedFile match { 101 | case Some(f) => 102 | s.log.info("Library " + library.fullName + " is already preloaded") 103 | case None => () 104 | } 105 | 106 | // Return the library name 107 | preloadedFile 108 | } 109 | 110 | /**************************** 111 | * Scala preloading process * 112 | ****************************/ 113 | 114 | private def doPreloadPermissions( 115 | db: File, s: TaskStreams, library: Library)(implicit emulator: Boolean): Unit = { 116 | 117 | // Inform the user 118 | s.log.info("Setting permissions for " + library.fullName) 119 | 120 | // Generate string from the XML 121 | val xmlString = scala.xml.Utility.toXML( 122 | scala.xml.Utility.trim(library.permissionTag), 123 | minimizeTags=true 124 | ).toString.replace("\"", "\\\"") 125 | 126 | // Load the file on the device 127 | androidTarget.run(db, s, 128 | "shell", "echo", xmlString, 129 | ">", library.devicePermissionPath 130 | ) 131 | } 132 | 133 | private def doPreloadJar( 134 | db: File, dx: File, s: TaskStreams, targetDir: File, library: Library)(implicit emulator: Boolean): Unit = { 135 | 136 | // This is the temporary JAR path 137 | val tempJarPath = (targetDir / library.localJar.name) 138 | 139 | // Dex current Scala library if necessary 140 | if (tempJarPath.lastModified < library.localJar.lastModified) { 141 | val dxCmd = Seq(dx.absolutePath, 142 | "-JXmx1024M", 143 | "-JXms1024M", 144 | "-JXss4M", 145 | "--no-optimize", 146 | "--debug", 147 | "--dex", 148 | "--output=" + tempJarPath.getAbsolutePath, 149 | library.localJar.getAbsolutePath 150 | ) 151 | s.log.info ("Dexing library %s".format(library.fullName)) 152 | s.log.debug (dxCmd.!!) 153 | } 154 | 155 | // Load the file on the device 156 | s.log.info("Installing library " + library.fullName) 157 | androidTarget.run(db, s, "push", 158 | tempJarPath.getAbsolutePath, 159 | library.deviceJarPath 160 | ) 161 | } 162 | 163 | private def doReboot (db: File, s: TaskStreams)(implicit emulator: Boolean) = { 164 | s.log.info("Rebooting " + deviceDesignation) 165 | if (emulator) 166 | androidTarget.run(db, s, "emu", "kill") 167 | else 168 | androidTarget.run(db, s, "reboot") 169 | () 170 | } 171 | 172 | private def doRemountReadWrite (db: File, s: TaskStreams)(implicit emulator: Boolean) = { 173 | s.log.info("Remounting /system as read-write") 174 | androidTarget.run(db, s, "root") 175 | androidTarget.run(db, s, "wait-for-device") 176 | androidTarget.run(db, s, "remount") 177 | } 178 | 179 | /*************************** 180 | * Emulator-specific stuff * 181 | ***************************/ 182 | 183 | private def doStartEmuReadWrite (db: File, s: TaskStreams, 184 | sdkPath: File, toolsPath: File, avdName: String, verbose: Boolean) = { 185 | 186 | // Find emulator config path 187 | val avdPath = Path.userHome / ".android" / "avd" / (avdName + ".avd") 188 | 189 | // Open config.ini 190 | val configFile = avdPath / "config.ini" 191 | 192 | // Read the contents and split by newline 193 | val configContents = scala.io.Source.fromFile(configFile).mkString 194 | 195 | // Regexp to match the system dir 196 | val sysre = "image.sysdir.1 *= *(.*)".r 197 | 198 | sysre findFirstIn configContents match { 199 | case Some(sysre(sys)) => 200 | 201 | // Copy system image to the emulator directory if needed 202 | val rosystem = sdkPath / sys / "system.img" 203 | val rwsystem = avdPath / "system.img" 204 | if (!rwsystem.exists) { 205 | s.log.info("Copying system image") 206 | "cp %s %s".format(rosystem.getAbsolutePath, rwsystem.getAbsolutePath).! 207 | } 208 | 209 | // Start the emulator with the local persistent system image 210 | s.log.info("Starting emulator with read-write system") 211 | s.log.info("This may take a while...") 212 | 213 | val rwemuCmdF = "%s/emulator -avd %s -no-boot-anim -no-snapshot -qemu -nand system,size=0x1f400000,file=%s -nographic -monitor null" 214 | val rwemuCmdV = "%s/emulator -avd %s -no-boot-anim -no-snapshot -verbose -qemu -nand system,size=0x1f400000,file=%s -nographic -show-kernel -monitor null" 215 | val rwemuCmd = (if (!verbose) rwemuCmdF else rwemuCmdV) 216 | .format(toolsPath, avdName, (avdPath / "system.img").getAbsolutePath) 217 | 218 | s.log.debug (rwemuCmd) 219 | rwemuCmd.run 220 | 221 | // Remount system as read-write 222 | implicit val emulator = true 223 | androidTarget.run(db, s, "wait-for-device") 224 | androidTarget.run(db, s, "root") 225 | androidTarget.run(db, s, "wait-for-device") 226 | androidTarget.run(db, s, "remount") 227 | 228 | case None => throw new Exception("Unable to find the system image") 229 | } 230 | } 231 | 232 | private def doKillEmu (db: File, s: TaskStreams)(implicit emulator: Boolean) = { 233 | if (emulator) 234 | androidTarget.run(db, s, "emu", "kill") 235 | () 236 | } 237 | 238 | private def filterLibraries 239 | (cp: Seq[Attributed[File]], filters: Seq[Attributed[File] => Boolean]) = 240 | cp filter (ce => filters exists (f => f(ce))) map (f => 241 | Library( 242 | f.data, 243 | f.get(moduleID.key).map(_.name) getOrElse f.data.name, 244 | f.get(moduleID.key).map(_.revision) getOrElse "unknown" 245 | ) 246 | ) 247 | 248 | /******************************* 249 | * Tasks related to preloading * 250 | *******************************/ 251 | 252 | private def preloadDeviceTask = 253 | (dbPath, dxPath, target, preloadFilters, managedClasspath, unmanagedClasspath, streams) map { 254 | (dbPath, dxPath, target, preloadFilters, mcp, umcp, streams) => 255 | 256 | // We're not using the emulator 257 | implicit val emulator = false 258 | 259 | // Retrieve libraries 260 | val libraries = filterLibraries(mcp ++ umcp, preloadFilters) 261 | 262 | // Wait for the device 263 | androidTarget.run(dbPath, streams, "wait-for-device") 264 | 265 | // Check for existing libraries 266 | val librariesToPreload = libraries filterNot { lib => 267 | checkPreloadedLibraryVersion(dbPath, streams, lib).isDefined 268 | } 269 | 270 | // Only do this if we have libraries to preload 271 | if (!librariesToPreload.isEmpty) { 272 | 273 | // Remount the device in read-write mode 274 | doRemountReadWrite (dbPath, streams) 275 | 276 | // Preload the libraries 277 | librariesToPreload map { lib => 278 | 279 | // Push files to the device 280 | doPreloadJar (dbPath, dxPath, streams, target, lib) 281 | doPreloadPermissions (dbPath, streams, lib) 282 | } 283 | 284 | // Reboot 285 | doReboot (dbPath, streams) 286 | } 287 | } 288 | 289 | private def preloadEmulatorTask(emulatorName: TaskKey[String]) = 290 | (emulatorName, toolsPath, sdkPath, dbPath, dxPath, target, preloadFilters, managedClasspath, unmanagedClasspath, streams) map { 291 | (emulatorName, toolsPath, sdkPath, dbPath, dxPath, target, preloadFilters, mcp, umcp, streams) => 292 | 293 | // We're using the emulator 294 | implicit val emulator = true 295 | 296 | // Retrieve libraries 297 | val libraries = filterLibraries(mcp ++ umcp, preloadFilters) 298 | 299 | // Check for existing libraries 300 | val librariesToPreload = libraries filterNot { lib => 301 | checkPreloadedLibraryVersion(dbPath, streams, lib).isDefined 302 | } 303 | 304 | // Only do this if we have libraries to preload 305 | if (!librariesToPreload.isEmpty) { 306 | 307 | // Kill any running emulator 308 | doKillEmu (dbPath, streams) 309 | 310 | // Restart the emulator in system read-write mode 311 | doStartEmuReadWrite (dbPath, streams, sdkPath, toolsPath, emulatorName, false) 312 | 313 | // Preload the libraries 314 | librariesToPreload map { lib => 315 | 316 | // Push files to the device 317 | doPreloadJar (dbPath, dxPath, streams, target, lib) 318 | doPreloadPermissions (dbPath, streams, lib) 319 | } 320 | 321 | // Reboot / Kill emulator 322 | doKillEmu (dbPath, streams) 323 | } 324 | } 325 | 326 | private def commandTask(command: String)(implicit emulator: Boolean) = 327 | (dbPath, streams) map { 328 | (d,s) => androidTarget.run(d, s, command) 329 | () 330 | } 331 | 332 | /************************* 333 | * Insert tasks into SBT * 334 | *************************/ 335 | 336 | lazy val settings: Seq[Setting[_]] = (Seq( 337 | // Automatically take care of AndroidManifest.xml when needed 338 | manifestRewriteRules <+= (usePreloaded, managedClasspath, unmanagedClasspath, preloadFilters) map 339 | { (u, mcp, umcp, l) => UsesLibraryRule(u, filterLibraries(mcp ++ umcp, l)) }, 340 | 341 | // Preload Scala on the device/emulator 342 | preloadDevice <<= preloadDeviceTask, 343 | preloadEmulator <<= InputTask( 344 | (sdkPath)(AndroidEmulator.installedAvds(_)))(preloadEmulatorTask) 345 | )) 346 | } 347 | -------------------------------------------------------------------------------- /src/main/scala/AndroidProjects.scala: -------------------------------------------------------------------------------- 1 | package sbtandroid 2 | 3 | import sbt._ 4 | import Keys._ 5 | import Defaults._ 6 | import AndroidPlugin._ 7 | 8 | object AndroidProjects { 9 | 10 | object Test { 11 | 12 | /** 13 | * Default test project settings 14 | */ 15 | lazy val defaults = 16 | AndroidBase.globalSettings ++ 17 | AndroidNdk.globalSettings ++ 18 | inConfig(Compile)( 19 | AndroidBase.settings ++ 20 | AndroidManifestGenerator.settings ++ 21 | AndroidPreload.settings ++ 22 | AndroidInstall.settings ++ 23 | AndroidDdm.settings ++ 24 | AndroidTest.settings ++ 25 | AndroidNdk.settings ++ 26 | TypedResources.settings ++ 27 | TypedLayouts.settings ++ 28 | Seq( 29 | // Test projects are Debug projects by default 30 | useDebug := true, 31 | 32 | // Test projects are usually pretty small, so no need for Proguard 33 | useProguard := false, 34 | 35 | // The Scala library is already imported by the main project 36 | usePreloaded := false, 37 | 38 | // It's highly unlikely that you'll need typed resources in test 39 | // projects, but you can enable it afterwards anyway, if you really 40 | // need it. 41 | useTypedResources := false, 42 | useTypedLayouts := false 43 | ) 44 | ) 45 | 46 | /** 47 | * Create a test Android project. 48 | * 49 | * See sbt.Project.apply definition: 50 | * http://www.scala-sbt.org/release/api/sbt/Project$.html 51 | */ 52 | def apply( 53 | id: String, 54 | base: File, 55 | aggregate: => Seq[ProjectReference] = Nil, 56 | dependencies: => Seq[ClasspathDep[ProjectReference]] = Nil, 57 | delegates: => Seq[ProjectReference] = Nil, 58 | settings: => Seq[sbt.Project.Setting[_]] = Seq.empty, 59 | configurations: Seq[Configuration] = Configurations.default 60 | ) = Project(id, 61 | base, 62 | aggregate, 63 | dependencies, 64 | delegates, 65 | defaultSettings ++ defaults ++ settings, 66 | configurations) 67 | } 68 | 69 | object Standard { 70 | 71 | // Default Android settings for standard projects 72 | // Standard presets : 73 | // 74 | // * androidPreload: 75 | // Does not include the Scala library, skips Proguard, 76 | // predexes external libraries and automatically sets 77 | // AndroidManifest.xml to require a preloaded Scala library. 78 | // 79 | // NOTE: The generated APK will NOT be compatible with stock devices 80 | // without a preloaded Scala library! 81 | // 82 | // * androidDebug: Will generate a debug APK compatible with stock Android devices. 83 | // * androidRelease: Will generate a realeas APK compatible with stock Android devices. 84 | 85 | /** 86 | * Standard Android defaults 87 | */ 88 | lazy val androidConfig: Seq[Setting[_]] = { 89 | AndroidBase.settings ++ 90 | AndroidManifestGenerator.settings ++ 91 | AndroidPreload.settings ++ 92 | AndroidInstall.settings ++ 93 | AndroidDdm.settings ++ 94 | AndroidLaunch.settings ++ 95 | AndroidNdk.settings ++ 96 | TypedResources.settings ++ 97 | TypedLayouts.settings 98 | } 99 | 100 | // Development settings 101 | lazy val androidPreload: Seq[Setting[_]] = { 102 | androidConfig ++ Seq( 103 | useDebug := true, 104 | useProguard := false, 105 | usePreloaded := true 106 | ) 107 | } 108 | 109 | // Debug settings 110 | lazy val androidDebug: Seq[Setting[_]] = { 111 | androidConfig ++ Seq( 112 | useDebug := true, 113 | useProguard := true, 114 | usePreloaded := false 115 | ) 116 | } 117 | 118 | // Release settings 119 | lazy val androidRelease: Seq[Setting[_]] = { 120 | androidConfig ++ 121 | AndroidRelease.settings ++ Seq( 122 | useDebug := false, 123 | useProguard := true, 124 | usePreloaded := false, 125 | release in Global <<= release 126 | ) 127 | } 128 | 129 | /** 130 | * Default settings, with 3 configurations : 131 | * - "Compile" will build a regular, Proguard-ed debug APK 132 | * - "Preload" will build an APK using preloaded libraries 133 | * - "Release" will build a release APK 134 | */ 135 | lazy val defaults = 136 | AndroidBase.globalSettings ++ 137 | AndroidNdk.globalSettings ++ 138 | inConfig(Compile)(androidDebug) ++ 139 | inConfig(Preload)(compileSettings ++ androidPreload) ++ 140 | inConfig(Release)(compileSettings ++ androidRelease) 141 | 142 | /** 143 | * Create a default Android project, already set up for standard 144 | * development. 145 | * 146 | * See sbt.Project.apply definition: 147 | * http://www.scala-sbt.org/release/api/sbt/Project$.html 148 | */ 149 | def apply( 150 | id: String, 151 | base: File, 152 | aggregate: => Seq[ProjectReference] = Nil, 153 | dependencies: => Seq[ClasspathDep[ProjectReference]] = Nil, 154 | delegates: => Seq[ProjectReference] = Nil, 155 | settings: => Seq[sbt.Project.Setting[_]] = Seq.empty, 156 | configurations: Seq[Configuration] = Configurations.default 157 | ) = Project(id, 158 | base, 159 | aggregate, 160 | dependencies, 161 | delegates, 162 | defaultSettings ++ defaults ++ settings, 163 | configurations ++ Seq(Preload, Release)) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/main/scala/AndroidRelease.scala: -------------------------------------------------------------------------------- 1 | package sbtandroid 2 | 3 | import sbt._ 4 | 5 | import Keys._ 6 | import AndroidPlugin._ 7 | 8 | object AndroidRelease { 9 | 10 | def zipAlignTask: Project.Initialize[Task[File]] = 11 | (zipAlignPath, packageApkPath, packageAlignedPath, streams) map { (zip, apkPath, pPath, s) => 12 | val zipAlign = Seq( 13 | zip.absolutePath, 14 | "-v", "4", 15 | apkPath.absolutePath, 16 | pPath.absolutePath) 17 | s.log.debug("Aligning "+zipAlign.mkString(" ")) 18 | s.log.debug(zipAlign !!) 19 | s.log.info("Aligned "+pPath) 20 | pPath 21 | } 22 | 23 | def signReleaseTask: Project.Initialize[Task[File]] = 24 | (keystorePath, keyalias, packageApkPath, streams, cachePasswords) map { (ksPath, ka, pPath, s, cache) => 25 | val jarsigner = Seq( 26 | "jarsigner", 27 | "-verbose", 28 | "-sigalg", "SHA1withRSA", 29 | "-digestalg", "SHA1", 30 | "-keystore", ksPath.absolutePath, 31 | "-storepass", PasswordManager.get( 32 | "keystore", ka, cache).getOrElse(sys.error("could not get password")), 33 | pPath.absolutePath, 34 | ka) 35 | s.log.debug("Signing "+jarsigner.mkString(" ")) 36 | val out = new StringBuffer 37 | val exit = jarsigner.run(new ProcessIO(input => (), 38 | output => out.append(IO.readStream(output)), 39 | error => out.append(IO.readStream(error)), 40 | inheritedInput => false) 41 | ).exitValue() 42 | if (exit != 0) sys.error("Error signing: "+out) 43 | s.log.debug(out.toString) 44 | s.log.info("Signed "+pPath) 45 | pPath 46 | } 47 | 48 | private def releaseTask = (packageAlignedPath, streams) map { (path, s) => 49 | s.log.success("Ready for publication: \n" + path) 50 | path 51 | } 52 | 53 | lazy val settings: Seq[Setting[_]] = (Seq( 54 | // Configuring Settings 55 | keystorePath := Path.userHome / ".keystore", 56 | zipAlignPath <<= (toolsPath) { _ / "zipalign" }, 57 | packageAlignedName <<= (artifact, versionName) map ((a,v) => 58 | String.format("%s-signed-%s.apk", a.name, v)), 59 | packageAlignedPath <<= (target, packageAlignedName) map ( _ / _ ), 60 | 61 | // Configuring Tasks 62 | cleanAligned <<= (packageAlignedPath) map (IO.delete(_)), 63 | 64 | release <<= releaseTask, 65 | release <<= release dependsOn zipAlign, 66 | 67 | zipAlign <<= zipAlignTask, 68 | zipAlign <<= zipAlign dependsOn (signRelease, cleanAligned), 69 | 70 | signRelease <<= signReleaseTask, 71 | signRelease <<= signRelease dependsOn apk 72 | )) 73 | } 74 | -------------------------------------------------------------------------------- /src/main/scala/AndroidTarget.scala: -------------------------------------------------------------------------------- 1 | package sbtandroid 2 | 3 | import sbt._ 4 | import Keys._ 5 | import AndroidHelpers._ 6 | 7 | /** 8 | * Android target base trait 9 | */ 10 | trait AndroidTarget { 11 | /** 12 | * Target-specific options send to ADB 13 | */ 14 | val options: Seq[String] 15 | 16 | /** 17 | * Runs ADB commands for the given adb executable path. 18 | */ 19 | def apply(adbPath: File, extra: String*) = { 20 | // Full command line 21 | val command = Seq(adbPath.absolutePath) ++ options ++ extra 22 | 23 | // Output buffer 24 | val output = new StringBuffer 25 | 26 | // Run the command and grab the exit value 27 | val exit = command.run(new ProcessIO( 28 | in => (), 29 | out => output.append(IO.readStream(out)), 30 | err => output.append(IO.readStream(err)), 31 | inheritedInput => false 32 | )).exitValue 33 | 34 | // Return the output and exit code 35 | (exit, output.toString) 36 | } 37 | 38 | /** 39 | * Returns a task that simply runs the specified ADB command, and sends the 40 | * output to the SBT logs. 41 | */ 42 | def run(adbPath: File, s: TaskStreams, extra: String*) = { 43 | // Display the command in the debug logs 44 | s.log.debug((Seq(adbPath.absolutePath) ++ options ++ extra).mkString(" ")) 45 | 46 | // Run the command 47 | val (exit, output) = this(adbPath, extra: _*) 48 | 49 | // Display the error and fail on ADB failure 50 | if (exit != 0 || output.contains("Failure")) { 51 | s.log.error(output) 52 | sys.error("Error executing ADB") 53 | 54 | // If the command succeeded, log the output to the debug stream 55 | } else s.log.debug(output) 56 | 57 | // Return the output 58 | output 59 | } 60 | 61 | /** 62 | * Starts the app on the target 63 | */ 64 | def startApp( 65 | adbPath: File, 66 | s: TaskStreams, 67 | manifestSchema: String, 68 | manifestPackage: String, 69 | manifestPath: Seq[java.io.File]) = { 70 | 71 | // Target activity (defined in the manifest) 72 | val activity = launcherActivity( 73 | manifestSchema, 74 | manifestPath.head, 75 | manifestPackage) 76 | 77 | // Full intent target 78 | val intentTarget = manifestPackage + "/" + activity 79 | 80 | // Run the command 81 | run(adbPath, s, 82 | "shell", "am", "start", 83 | "-a", "android.intent.action.MAIN", 84 | "-n", intentTarget 85 | ) 86 | } 87 | 88 | /** 89 | * Runs instrumentation tests on an app 90 | */ 91 | def testApp( 92 | adbPath: File, 93 | manifestPackage: String, 94 | testRunner: String, 95 | testName: Option[String] = None) = { 96 | 97 | // Full intent target 98 | val intentTarget = manifestPackage + "/" + testRunner 99 | 100 | // Run the command 101 | testName match { 102 | case Some(test) => 103 | this(adbPath, 104 | "shell", "am", "instrument", 105 | "-r", 106 | "-e", "class", test, 107 | "-w", intentTarget) 108 | 109 | case None => 110 | this(adbPath, 111 | "shell", "am", "instrument", 112 | "-r", 113 | "-w", intentTarget) 114 | } 115 | } 116 | 117 | /** 118 | * Installs or uninstalls a package on the target 119 | */ 120 | def installPackage(adbPath: File, streams: TaskStreams, apkPath: File) = 121 | run(adbPath, streams, "install", "-r", apkPath.absolutePath) 122 | def uninstallPackage(adbPath: File, streams: TaskStreams, packageName: String) = 123 | run(adbPath, streams, "uninstall", packageName) 124 | } 125 | 126 | /** 127 | * Some common Android target definitions 128 | */ 129 | object AndroidDefaultTargets { 130 | 131 | /** 132 | * Selects a connected Android target 133 | */ 134 | object Auto extends AndroidTarget { 135 | val options = Seq.empty 136 | } 137 | 138 | /** 139 | * Selects a connected Android device 140 | */ 141 | object Device extends AndroidTarget { 142 | val options = Seq("-d") 143 | } 144 | 145 | /** 146 | * Selects a connected Android emulator 147 | */ 148 | object Emulator extends AndroidTarget { 149 | val options = Seq("-e") 150 | } 151 | 152 | /** 153 | * Selects any Android device or emulator matching the given UID 154 | */ 155 | case class UID(val uid: String) extends AndroidTarget { 156 | val options = Seq("-s", uid) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/main/scala/AndroidTest.scala: -------------------------------------------------------------------------------- 1 | package sbtandroid 2 | 3 | import sbt._ 4 | import Keys._ 5 | 6 | import AndroidPlugin._ 7 | import AndroidHelpers._ 8 | import complete.DefaultParsers._ 9 | import complete.Parser 10 | import sbinary.DefaultProtocol.StringFormat 11 | import Cache.seqFormat 12 | import com.android.ddmlib.testrunner.{InstrumentationResultParser,ITestRunListener} 13 | 14 | object AndroidTest { 15 | 16 | /** 17 | * Test settings 18 | */ 19 | lazy val settings: Seq[Setting[_]] = 20 | (Seq ( 21 | testRunner <<= detectTestRunnerTask, 22 | test <<= instrumentationTestAction dependsOn install, 23 | testOnly <<= InputTask(loadForParser(definedTestNames in Test)( (s, i) => testParser(s, i getOrElse Nil))) { test => 24 | runSingleTest(test) 25 | } 26 | )) 27 | 28 | /** 29 | * Test runner detection 30 | */ 31 | val defaultTestRunner = "android.test.InstrumentationTestRunner" 32 | def detectTestRunnerTask = (manifestPath) map { (mp) => 33 | val instrumentations = (manifest(mp.head) \ "instrumentation").map(_.attribute( 34 | "http://schemas.android.com/apk/res/android", "name")) 35 | instrumentations.headOption.flatMap(_.map(_.toString)).getOrElse(defaultTestRunner) 36 | } 37 | 38 | /** 39 | * Task for starting tests on a target 40 | */ 41 | val instrumentationTestAction = 42 | (adbTarget, dbPath, manifestPackage, testRunner, streams) map { 43 | (adbTarget, dbPath, manifestPackage, testRunner, s) => 44 | 45 | // Run instrumentation tests 46 | val (exit, out) = adbTarget.testApp(dbPath, manifestPackage, testRunner) 47 | 48 | // Parse them if they succeeded 49 | if (exit == 0) parseTests(out, manifestPackage, s.log) 50 | 51 | // Else, display the error 52 | else sys.error("am instrument returned error %d\n\n%s".format(exit, out)) 53 | 54 | // Return Unit 55 | () 56 | } 57 | 58 | /** 59 | * Task for starting a single test on a target 60 | */ 61 | val runSingleTest = (test: TaskKey[String]) => 62 | (adbTarget, test, dbPath, manifestPackage, testRunner, streams) map { 63 | (adbTarget, test, dbPath, manifestPackage, testRunner, s) => 64 | 65 | // Run instrumentation tests 66 | val (exit, out) = adbTarget.testApp(dbPath, manifestPackage, testRunner, Some(test)) 67 | 68 | // Parse them if they succeeded 69 | if (exit == 0) parseTests(out, manifestPackage, s.log) 70 | 71 | // Else, display the error 72 | else sys.error("am instrument returned error %d\n\n%s".format(exit, out)) 73 | 74 | // Return Unit 75 | () 76 | } 77 | 78 | /** 79 | * Parse the test results and display them to the user 80 | */ 81 | def parseTests(out: String, name: String, log: Logger) { 82 | val listener = new TestListener(log) 83 | val parser = new InstrumentationResultParser(name, listener) 84 | parser.processNewLines(out.split("\\n").map(_.trim)) 85 | listener.errorMessage.map(sys.error(_)).orElse { 86 | log.success("All tests passed") 87 | None 88 | } 89 | } 90 | 91 | def testParser(s: State, tests:Seq[String]): Parser[String] = 92 | Space ~> tests.map(t => token(t)) 93 | .reduceLeftOption(_ | _) 94 | .getOrElse(token(NotSpace)) 95 | 96 | class TestListener(log: Logger) extends ITestRunListener { 97 | import com.android.ddmlib.testrunner.TestIdentifier 98 | import com.android.ddmlib.testrunner.ITestRunListener._ 99 | type Metrics = java.util.Map[String, String] 100 | 101 | val failedTests = new collection.mutable.ListBuffer[TestIdentifier] 102 | var runFailed:Option[String] = None 103 | 104 | def testRunStarted(runName: String, testCount: Int) { 105 | log.info("testing %s (%d test%s)".format(runName, testCount, if (testCount == 1) "" else "s")) 106 | } 107 | 108 | def testRunStopped(elapsedTime: Long) { 109 | log.info("testRunStopped (%d seconds)".format(elapsedTime)) 110 | } 111 | 112 | def testStarted(test: TestIdentifier) { 113 | log.debug("testStarted: "+test) 114 | } 115 | 116 | def testEnded(test: TestIdentifier, metrics: Metrics) { 117 | if (!failedTests.contains(test)) { 118 | val status = "%spassed%s: %s".format(scala.Console.GREEN, scala.Console.RESET, test) 119 | if (test.getTestName != "testAndroidTestCaseSetupProperly") { 120 | log.info(status) 121 | } else { 122 | log.debug(status) 123 | } 124 | } 125 | log.debug(metrics.toString) 126 | } 127 | 128 | def testFailed(status: TestFailure, test: TestIdentifier, trace: String) { 129 | log.error("failed: %s\n\n%s\n".format(test, trace)) 130 | failedTests += test 131 | } 132 | 133 | def testRunEnded(elapsedTime: Long, metrics: Metrics) { 134 | log.info("testRunEnded (%d seconds)".format(elapsedTime)) 135 | log.debug(metrics.toString) 136 | } 137 | 138 | def testRunFailed(message: String) { 139 | log.error("testRunFailed: "+message) 140 | runFailed = Some(message) 141 | } 142 | 143 | def errorMessage:Option[String] = { 144 | if (!failedTests.isEmpty) 145 | Some("Failed tests: "+failedTests.mkString(", ")) 146 | else runFailed 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/main/scala/ApkBuilder.scala: -------------------------------------------------------------------------------- 1 | package sbtandroid 2 | 3 | import sbt._ 4 | import classpath._ 5 | import java.io.{ByteArrayOutputStream, File, PrintStream} 6 | 7 | // Replaces the Installable argument 8 | case class ApkConfig( 9 | androidToolsPath: File, 10 | packageApkPath: File, 11 | resourcesApkPath: File, 12 | classesDexPath: File, 13 | nativeInputPaths: Seq[File], 14 | dexInputs: Seq[File], 15 | resourceDirectory: File 16 | ) 17 | 18 | /** 19 | * Build an APK - replaces the now-deprecated apkbuilder command-line executable. 20 | * 21 | * Google provides no supported means of building an APK from the command line. 22 | * Instead, we need to use the `ApkBuilder` class within `sdklib.jar`. 23 | * 24 | * The source for `ApkBuilder` is 25 | * [[http://android.git.kernel.org/?p=platform/sdk.git;a=blob;f=sdkmanager/libs/sdklib/src/com/android/sdklib/build/ApkBuilder.java here]]. 26 | * 27 | * The source for Google's Ant task that uses it is 28 | * [[http://android.git.kernel.org/?p=platform/sdk.git;a=blob;f=anttasks/src/com/android/ant/ApkBuilderTask.java here]]. 29 | */ 30 | class ApkBuilder(project: ApkConfig, debug: Boolean) { 31 | type JApkBuilder = Object 32 | // sdklib has not been packaged yet, need to load dynamically 33 | val klass = ClasspathUtilities.toLoader(project.androidToolsPath / "lib" / "sdklib.jar") 34 | .loadClass("com.android.sdklib.build.ApkBuilder") 35 | val outputStream = new ByteArrayOutputStream 36 | 37 | def build():Either[String, String] = try { 38 | val constructor = klass.getConstructor( 39 | classOf[File], classOf[File], classOf[File], classOf[String], classOf[PrintStream]) 40 | val builder:JApkBuilder = constructor.newInstance( 41 | project.packageApkPath, 42 | project.resourcesApkPath, 43 | project.classesDexPath, 44 | if (debug) getDebugKeystore else null, 45 | new PrintStream(outputStream) 46 | ).asInstanceOf[JApkBuilder] 47 | 48 | setDebugMode(builder, debug) 49 | 50 | for (path <- project.nativeInputPaths) 51 | addNativeLibraries(builder, path, null) 52 | 53 | for (file <- project.dexInputs; if file.isFile) 54 | addResourcesFromJar(builder, file) 55 | 56 | addSourceFolder(builder, project.resourceDirectory) 57 | sealApk(builder) 58 | 59 | Right("Packaging "+project.packageApkPath) 60 | } catch { 61 | case e: Throwable => Left( 62 | String.format("\n%s\nError packaging %s: %s", 63 | outputStream.toString, 64 | project.packageApkPath, 65 | if (e.getCause != null) e.getCause.getMessage else e.getMessage)) 66 | } 67 | 68 | def getDebugKeystore = klass.getMethod("getDebugKeystore").invoke(null).asInstanceOf[String] 69 | 70 | def setDebugMode(builder: JApkBuilder, debug: Boolean) { 71 | klass.getMethod("setDebugMode", classOf[Boolean]) 72 | .invoke(builder, debug.asInstanceOf[Object]) 73 | } 74 | 75 | def addNativeLibraries(builder: JApkBuilder, nativeFolder: File, abiFilter: String) { 76 | if (nativeFolder.exists && nativeFolder.isDirectory) { 77 | try { 78 | klass.getMethod("addNativeLibraries", classOf[File], classOf[String]) 79 | .invoke(builder, nativeFolder, abiFilter) 80 | } catch { 81 | case e: java.lang.NoSuchMethodException => { 82 | klass.getMethod("addNativeLibraries", classOf[File]) 83 | .invoke(builder, nativeFolder) 84 | } 85 | } 86 | } 87 | } 88 | 89 | /// Copy most non class files from the given standard java jar file 90 | /// 91 | /// (used to let classloader.getResource work for legacy java libs 92 | /// on android) 93 | def addResourcesFromJar(builder: JApkBuilder, jarFile: File) { 94 | if (jarFile.isFile) { 95 | def method = klass.getMethod("addResourcesFromJar", classOf[File]) 96 | method.invoke(builder, jarFile) 97 | } 98 | } 99 | 100 | def addSourceFolder(builder: JApkBuilder, folder: File) { 101 | if (folder.exists) { 102 | klass.getMethod("addSourceFolder", classOf[File]) 103 | .invoke(builder, folder) 104 | } 105 | } 106 | 107 | def sealApk(builder: JApkBuilder) { klass.getMethod("sealApk").invoke(builder) } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/scala/PasswordManager.scala: -------------------------------------------------------------------------------- 1 | package sbtandroid 2 | 3 | import sbt._ 4 | 5 | import Keys._ 6 | import AndroidPlugin._ 7 | 8 | object PasswordManager extends PWManager { 9 | lazy val settings: Seq[Setting[_]] = (Seq ( 10 | clearPasswords <<= (streams) map { (s) => 11 | clear() 12 | s.log.success("cleared passwords") 13 | } 14 | )) 15 | 16 | def fetch(service: String, account: String): Option[String] = impl.fetch(service, account) 17 | def store(service: String, account: String, pw: String): Option[String] = impl.store(service, account, pw) 18 | def delete(service: String, account: String): Boolean = impl.delete(service, account) 19 | def clear() { impl.clear() } 20 | 21 | lazy val impl: PWManager = { 22 | System.getProperty("os.name") match { 23 | case "Mac OS X" => OSXPasswordManager 24 | case unknown => FilePasswordManager 25 | } 26 | } 27 | } 28 | 29 | trait PWManager { 30 | def readPassword(service: String, account: String) = 31 | SimpleReader.readLine("\nEnter password for "+service+"/"+account+": ").get 32 | 33 | def get(service: String, account: String, cache: Boolean):Option[String] = { 34 | fetch(service, account).orElse { 35 | val pw = readPassword(service, account) 36 | if (cache) store(service, account, pw) else Some(pw) 37 | } 38 | } 39 | 40 | 41 | def fetch(service: String, account: String): Option[String] 42 | def store(service: String, account: String, pw: String): Option[String] 43 | def delete(service: String, account: String): Boolean 44 | def clear() 45 | } 46 | 47 | object OSXPasswordManager extends PWManager { 48 | val Label = "sbt-android-plugin" 49 | 50 | def fetch(service: String, account: String): Option[String] = { 51 | 52 | val buffer = new StringBuffer 53 | Seq("security", 54 | "find-generic-password", 55 | "-a", account, 56 | "-s", service, "-g").run(new ProcessIO(input => (), 57 | output => (), 58 | error => buffer.append(IO.readStream(error)), 59 | inheritedInput => false) 60 | ).exitValue() match { 61 | case 0 => 62 | (for (line <- buffer.toString.split("\r\n"); 63 | if (line.startsWith("password: "))) 64 | yield line.substring(line.indexOf('"') + 1, line.lastIndexOf('"'))).headOption 65 | case 44 => 66 | // password not stored yet 67 | None 68 | case _ => None 69 | } 70 | } 71 | 72 | def store(service: String, account: String, pw: String): Option[String] = { 73 | Seq("security", 74 | "add-generic-password", 75 | "-a", account, 76 | "-s", service, 77 | "-l", Label, 78 | "-w", pw).run(false).exitValue() match { 79 | case 0 => Some(pw) 80 | case _ => None 81 | } 82 | } 83 | 84 | def delete(service: String, account: String): Boolean = { 85 | Seq("security", 86 | "delete-generic-password", 87 | "-a", account, 88 | "-s", service).run(false).exitValue() == 0 89 | } 90 | 91 | def clear() { 92 | if (Seq("security", 93 | "delete-generic-password", 94 | "-l", Label 95 | ).run(new ProcessIO(input => (), output => (), error => (), inheritedInput => false)) 96 | .exitValue() == 0) clear() 97 | } 98 | } 99 | 100 | object EmptyPasswordManager extends PWManager { 101 | def fetch(service: String, account: String) = None 102 | def store(service: String, account: String, pw: String) = Some(pw) 103 | def delete(service: String, account: String) = false 104 | def clear() {} 105 | } 106 | 107 | object FilePasswordManager extends PWManager { 108 | val pwDir = new File(new File( 109 | System.getProperty("user.home"), ".sbt"), "sbt-android-plugin-passwords") 110 | 111 | def file(service: String) = { 112 | if (!pwDir.exists()) pwDir.mkdirs() 113 | new File(pwDir, service) 114 | } 115 | 116 | def fetch(service: String, account: String) = { 117 | val f = file(service) 118 | if (f.exists()) (for (line <- IO.readLines(f); 119 | if line.startsWith(account+"=")) 120 | yield line.substring(line.indexOf('=')+1)).headOption 121 | else None 122 | } 123 | 124 | def store(service: String, account: String, pw: String) = { 125 | val f = file(service) 126 | val buffer = new StringBuffer 127 | var replaced = false 128 | def appendPw() = buffer.append(account).append("=").append(pw).append("\n") 129 | if (f.exists()) { 130 | for (line <- IO.readLines(f)) 131 | if (line.startsWith(account+"=")) { 132 | appendPw() 133 | replaced = true 134 | } else buffer.append(line).append("\n") 135 | if (!replaced) appendPw() 136 | } else appendPw() 137 | IO.write(f, buffer.toString.getBytes) 138 | Some(pw) 139 | } 140 | 141 | def clear() { 142 | if (pwDir.exists()) { 143 | for (f <- pwDir.listFiles()) f.delete() 144 | pwDir.delete() 145 | } 146 | } 147 | 148 | def delete(service: String, account: String) = { 149 | val f = file(service) 150 | val buffer = new StringBuffer 151 | var found = false 152 | if (f.exists()) { 153 | for (line <- IO.readLines(f)) { 154 | if (!line.startsWith(account+"=")) { 155 | buffer.append(line).append("\n") 156 | } else found = true 157 | } 158 | IO.write(f, buffer.toString.getBytes) 159 | found 160 | } else false 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/main/scala/TypedLayouts.scala: -------------------------------------------------------------------------------- 1 | package sbtandroid 2 | 3 | import sbt.{stringToProcess => _, _} 4 | import classpath._ 5 | import scala.xml._ 6 | 7 | import Keys._ 8 | import AndroidPlugin._ 9 | 10 | // FIXME more appropriate name 11 | object TypedLayouts { 12 | /** Views with android:id. */ 13 | private case class NamedView(id: String, 14 | className: String, 15 | subViews: Iterable[NamedView]) { 16 | def flatten: Iterable[NamedView] = { 17 | Iterable(this) ++ subViews.flatMap(_.flatten) 18 | } 19 | } 20 | 21 | /** Returns tree nodes represents views with android:id */ 22 | private def buildTree(jarPath: File)(element: Elem): Iterable[NamedView] = { 23 | val androidJarLoader = ClasspathUtilities.toLoader(jarPath) 24 | 25 | /** Returns a class object if exits. */ 26 | def tryLoading(className: String): Option[Class[_]] = { 27 | try { 28 | Some(androidJarLoader.loadClass(className)) 29 | } catch { 30 | case e: Exception => None 31 | case e: LinkageError => None 32 | } 33 | } 34 | 35 | def attributeText(element: Elem, 36 | namespace: String, 37 | localName: String) = { 38 | element 39 | .attribute(namespace, localName) 40 | .map(_.map(_.text).mkString) 41 | } 42 | 43 | val androidNamespace = "http://schemas.android.com/apk/res/android" 44 | 45 | /** regexp extracting id */ 46 | val Id = """@\+id/(.*)""".r 47 | 48 | val idOption = 49 | attributeText(element, androidNamespace, "id") 50 | .collect({ case Id(id) => id }) 51 | 52 | // node.label is either fully qualified class name or 53 | // unqualified class name. 54 | val classNameOption = if (element.label.contains('.')) { 55 | Some(element.label) 56 | } else { 57 | List("android.widget.", "android.view.", "android.webkit.") 58 | .map(_ + element.label) 59 | .flatMap(tryLoading _) 60 | .headOption 61 | .map(_.getName) 62 | .map("_root_." + _) 63 | } 64 | 65 | val pairOption = for { 66 | id <- idOption 67 | className <- classNameOption 68 | } yield { 69 | (id, className) 70 | } 71 | 72 | val subViews = 73 | element.child.collect({ case e: Elem => e }).flatMap(buildTree(jarPath) _) 74 | 75 | pairOption match { 76 | case Some((id, className)) => 77 | Seq(NamedView(id, className, subViews)) 78 | case None => 79 | subViews 80 | } 81 | } 82 | 83 | /** Merges layout definitions having the same name. */ 84 | private def mergeLayouts(streams: std.TaskStreams[Project.ScopedKey[_]]) 85 | (layouts: Iterable[(String, Iterable[NamedView])]) = { 86 | def mergeViews(views: Iterable[NamedView]): Iterable[NamedView] = { 87 | views.groupBy(_.id).values.map(doMergeViews _) 88 | } 89 | 90 | def doMergeViews(views: Iterable[NamedView]): NamedView = { 91 | val id = views.head.id 92 | val className = views.head.className 93 | 94 | for (view <- views) { 95 | if (view.className != className) { 96 | streams.log.warn("Resource id '%s' mapped to %s and %s" 97 | .format(id, className, view.className)) 98 | } 99 | } 100 | 101 | NamedView(id, className, mergeViews(views.flatMap(_.subViews))) 102 | } 103 | 104 | def toMultimap[A, B](tuples: Iterable[(A, B)]) = { 105 | tuples.groupBy(_._1).mapValues(_.map(_._2)) 106 | } 107 | 108 | toMultimap(layouts).mapValues(_.flatten).mapValues(mergeViews _) 109 | } 110 | 111 | /** Reserved words of Scala language */ 112 | // from Scala Language Specification Version 2.9 Section 1.1 113 | private val reserved = 114 | Set("abstract", "case", "do", "else", "finally", "for", "import", 115 | "lazy", "object", "override", "return", "sealed", "trait", "try", 116 | "var", "while", "catch", "class", "extends", "false", "forSome", 117 | "if", "match", "new", "package", "private", "super", "this", 118 | "true", "type", "with", "yield", "def", "final", "implicit", 119 | "null", "protected", "throw", "val") 120 | 121 | /** Quotes a qualified/unqualified identifier if it is reserved */ 122 | private def quoteReserved(id: String) = { 123 | id.split("\\.").map(id => 124 | if (reserved.contains(id)) { 125 | "`" + id + "`" 126 | } else { 127 | id 128 | } 129 | ).mkString(".") 130 | } 131 | 132 | private def toCamelCase(name: String) = { 133 | "_(.)".r.replaceAllIn(name, _.group(1).toUpperCase) 134 | } 135 | 136 | private def toUpperCamelCase(name: String) = { 137 | toCamelCase(name).capitalize 138 | } 139 | 140 | // FIXME Name clashes occur if duplicated IDs exist. 141 | private def formatLayout(manifestPackage: String) 142 | (layout: (String, Iterable[NamedView])) = { 143 | val (layoutName, views) = layout 144 | 145 | // Since Activity, Dialog, and View does not have a common interface, 146 | // we generate four types: common trait, base trait for Activity, 147 | // base trait for Dialog, and view wrapper suitable for use with such as 148 | // SimplerAdapter.ViewBinder. 149 | """|trait %1$s { 150 | | // to avoid name shadowing in pathological cases. 151 | | `this` => 152 | | 153 | | def findViewById(id: _root_.scala.Int): _root_.android.view.View 154 | | 155 | | 156 | | %2$s 157 | | 158 | |} 159 | | 160 | |object %1$s { 161 | | trait Activity extends _root_.android.app.Activity with %4$s.Layout.%1$s { 162 | | protected override def onCreate(savedInstanceState: _root_.android.os.Bundle) { 163 | | super.onCreate(savedInstanceState) 164 | | 165 | | setContentView(%4$s.R.layout.%3$s) 166 | | } 167 | | } 168 | | 169 | | trait Dialog extends _root_.android.app.Dialog with %4$s.Layout.%1$s { 170 | | protected override def onCreate(savedInstanceState: _root_.android.os.Bundle) { 171 | | super.onCreate(savedInstanceState) 172 | | 173 | | setContentView(%4$s.R.layout.%3$s) 174 | | } 175 | | } 176 | | 177 | | class ViewWrapper(view: _root_.android.view.View) extends %4$s.Layout.ViewWrapper(view) with %4$s.Layout.%1$s { 178 | | override def findViewById(id: _root_.scala.Int) = { 179 | | view.findViewById(id) 180 | | } 181 | | } 182 | | 183 | | def apply(view: _root_.android.view.View) = new ViewWrapper(view) 184 | |} 185 | |""".stripMargin.format(toUpperCamelCase(layoutName), 186 | views 187 | .flatMap(_.flatten) 188 | .map(formatView(manifestPackage)(layoutName, _)) 189 | .mkString("\n") 190 | .lines 191 | .mkString("\n "), 192 | quoteReserved(layoutName), 193 | "_root_." + manifestPackage) 194 | } 195 | 196 | private def formatView(manifestPackage: String) 197 | (layoutName: String, view: NamedView) = { 198 | if (view.subViews.isEmpty) { 199 | "lazy val %1$s = `this`.findViewById(%3$s.R.id.%1$s).asInstanceOf[%2$s]" 200 | .format(quoteReserved(view.id), 201 | quoteReserved(view.className), 202 | "_root_." + manifestPackage) 203 | } else { 204 | """|object %1$s extends %4$s.Layout.ViewWrapper[%2$s](`this`.findViewById(%4$s.R.id.%1$s).asInstanceOf[%2$s]) { 205 | | 206 | | %3$s 207 | | 208 | |} 209 | |""".stripMargin.format(quoteReserved(view.id), 210 | quoteReserved(view.className), 211 | view 212 | .subViews 213 | .flatMap(_.flatten) 214 | .map(formatSubView(layoutName, _)) 215 | .mkString("\n") 216 | .lines 217 | .mkString("\n "), 218 | "_root_." + manifestPackage 219 | ) 220 | } 221 | } 222 | 223 | private def formatSubView(layoutName: String, view: NamedView) = { 224 | "def %1$s = `this`.%1$s".format(quoteReserved(view.id)) 225 | } 226 | 227 | private def generateTypedLayoutsTask = 228 | (useTypedLayouts, typedLayouts, layoutResources, libraryJarPath, manifestPackage, streams) map { 229 | (useTypedLayouts, typedLayouts, layoutResources, libraryJarPath, manifestPackage, s) => 230 | if (useTypedLayouts) { 231 | // e.g. main_activity.xml -> main_activity 232 | def baseName(path: File) = { 233 | val name = path.getName 234 | 235 | name.substring(0, name.lastIndexOf(".")) 236 | } 237 | 238 | val layouts = mergeLayouts(s)( 239 | layoutResources.get.map(path => 240 | (baseName(path), buildTree(libraryJarPath)(XML.loadFile(path))) 241 | ) 242 | ) 243 | 244 | IO.write(typedLayouts, 245 | """|package %s 246 | | 247 | |object Layout { 248 | | case class ViewWrapper[A <: _root_.android.view.View](view: A) 249 | | object ViewWrapper { 250 | | implicit def unwrap[A <: _root_.android.view.View](v: ViewWrapper[A]): A = v.view 251 | | } 252 | | 253 | | %s 254 | | 255 | |} 256 | |""" 257 | .stripMargin.format(manifestPackage, 258 | layouts 259 | .map(formatLayout(manifestPackage) _) 260 | .mkString("\n") 261 | .lines 262 | .mkString("\n ") 263 | ) 264 | ) 265 | s.log.info("Wrote %s".format(typedLayouts)) 266 | Seq(typedLayouts) 267 | } else { 268 | Seq.empty 269 | } 270 | } 271 | 272 | lazy val settings: Seq[Setting[_]] = (Seq ( 273 | typedLayouts <<= (manifestPackage, managedScalaPath) map { 274 | _.split('.').foldLeft(_) ((p, s) => p / s) / "typed_layouts.scala" 275 | }, 276 | layoutResources <<= (mainResPath) map { x => (x * "layout*" * "*.xml" get) }, 277 | 278 | generateTypedLayouts <<= generateTypedLayoutsTask, 279 | 280 | sourceGenerators <+= generateTypedLayouts, 281 | 282 | watchSources <++= (layoutResources) map (ls => ls) 283 | )) 284 | } 285 | -------------------------------------------------------------------------------- /src/main/scala/TypedResources.scala: -------------------------------------------------------------------------------- 1 | package sbtandroid 2 | 3 | import sbt._ 4 | import classpath._ 5 | import scala.xml._ 6 | 7 | import Keys._ 8 | import AndroidPlugin._ 9 | 10 | object TypedResources { 11 | private def generateTypedResourcesTask = 12 | (useTypedResources, typedResource, layoutResources, libraryJarPath, manifestPackage, streams) map { 13 | (useTypedResources, typedResource, layoutResources, libraryJarPath, manifestPackage, s) => 14 | 15 | if (useTypedResources) { 16 | val Id = """@\+id/(.*)""".r 17 | val androidJarLoader = ClasspathUtilities.toLoader(libraryJarPath) 18 | 19 | def tryLoading(className: String) = { 20 | try { 21 | Some(androidJarLoader.loadClass(className)) 22 | } catch { 23 | case _ => None 24 | } 25 | } 26 | 27 | val layouts: Set[String] = layoutResources.get.flatMap{ layout => 28 | val Name = "(.*)\\.[^\\.]+".r 29 | layout.getName match { 30 | case Name(name) => Some(name) 31 | case _ => None 32 | } 33 | }.toSet 34 | val reserved = List("extends", "trait", "type", "val", "var", "with") 35 | 36 | val resources = layoutResources.get.flatMap { path => 37 | XML.loadFile(path).descendant_or_self flatMap { node => 38 | // all nodes 39 | node.attribute("http://schemas.android.com/apk/res/android", "id") flatMap { 40 | // with android:id attribute 41 | _.headOption.map { _.text } flatMap { 42 | // if it looks like a full classname 43 | case Id(id) if node.label.contains('.') => Some(id, node.label) 44 | // otherwise it may be a widget or view 45 | case Id(id) => { 46 | List("android.widget.", "android.view.", "android.webkit.").map(pkg => 47 | tryLoading(pkg + node.label)).find(_.isDefined).flatMap(clazz => 48 | Some(id, clazz.get.getName) 49 | ) 50 | } 51 | case _ => None 52 | } 53 | } 54 | } 55 | }.foldLeft(Map.empty[String, String]) { 56 | case (m, (k, v)) => 57 | m.get(k).foreach { v0 => 58 | if (v0 != v) s.log.warn("Resource id '%s' mapped to %s and %s" format (k, v0, v)) 59 | } 60 | m + (k -> v) 61 | }.filterNot { 62 | case (id, _) => reserved.contains(id) 63 | } 64 | 65 | IO.write(typedResource, 66 | """ |package %s 67 | |import _root_.android.app.{Activity, Dialog} 68 | |import _root_.android.view.View 69 | |import scala.language.implicitConversions 70 | | 71 | |case class TypedResource[T](id: Int) 72 | |case class TypedLayout(id: Int) 73 | | 74 | |object TR { 75 | |%s 76 | | object layout { 77 | | %s 78 | | } 79 | |} 80 | |trait TypedViewHolder { 81 | | def findViewById( id: Int ): View 82 | | def findView[T](tr: TypedResource[T]) = findViewById(tr.id).asInstanceOf[T] 83 | |} 84 | |trait TypedView extends View with TypedViewHolder 85 | |trait TypedActivityHolder extends TypedViewHolder 86 | |trait TypedActivity extends Activity with TypedActivityHolder 87 | |trait TypedDialog extends Dialog with TypedViewHolder 88 | |object TypedResource { 89 | | implicit def layout2int(l: TypedLayout) = l.id 90 | | implicit def view2typed(v: View) = new TypedViewHolder { 91 | | def findViewById( id: Int ) = v.findViewById( id ) 92 | | } 93 | | implicit def activity2typed(a: Activity) = new TypedViewHolder { 94 | | def findViewById( id: Int ) = a.findViewById( id ) 95 | | } 96 | | implicit def dialog2typed(d: Dialog) = new TypedViewHolder { 97 | | def findViewById( id: Int ) = d.findViewById( id ) 98 | | } 99 | |} 100 | |""".stripMargin.format( 101 | manifestPackage, 102 | resources map { case (id, classname) => 103 | " val %s = TypedResource[%s](R.id.%s)".format(id, classname, id) 104 | } mkString "\n", 105 | layouts map { name => 106 | " val %s = TypedLayout(R.layout.%s)".format(name, name) 107 | } mkString "\n" 108 | ) 109 | ) 110 | s.log.info("Wrote %s" format(typedResource)) 111 | Seq(typedResource) 112 | } else Seq.empty 113 | } 114 | 115 | lazy val settings: Seq[Setting[_]] = (Seq ( 116 | typedResource <<= (manifestPackage, managedScalaPath) map { 117 | _.split('.').foldLeft(_) ((p, s) => p / s) / "TR.scala" 118 | }, 119 | layoutResources <<= (mainResPath) map { x=> (x * "layout*" * "*.xml" get) }, 120 | 121 | generateTypedResources <<= generateTypedResourcesTask, 122 | 123 | sourceGenerators <+= generateTypedResources, 124 | 125 | watchSources <++= (layoutResources) map (ls => ls) 126 | )) 127 | } 128 | -------------------------------------------------------------------------------- /src/main/scala/legacy/Github.scala: -------------------------------------------------------------------------------- 1 | package sbtandroid 2 | 3 | import java.io.{File,FileInputStream,OutputStreamWriter,PrintWriter} 4 | import java.net.{URL, HttpURLConnection} 5 | 6 | import scala.util.parsing.json.JSON 7 | import scala.xml.Node 8 | import sbt._ 9 | import Keys._ 10 | 11 | import AndroidPlugin._ 12 | 13 | object Github { 14 | val gitConfig = new File(System.getenv("HOME"), ".gitconfig") 15 | val apkMime = "application/vnd.android.package-archive" 16 | val gitDownloads = "https://api.github.com/repos/%s/downloads" 17 | 18 | /** Github keys **/ 19 | object Keys { 20 | val uploadGithub = TaskKey[Option[String]]("github-upload", "Upload file to github") 21 | val deleteGithub = TaskKey[Unit]("github-delete", "Delete file from github") 22 | val githubRepo = SettingKey[String]("github-repo", "Github repo") 23 | } 24 | 25 | import Keys._ 26 | 27 | lazy val settings: Seq[Setting[_]] = (Seq ( 28 | uploadGithub <<= (release, githubRepo, cachePasswords, streams) map { (path, repo, cache, s) => 29 | val (user, password) = credentials(cache) 30 | upload(Upload(path, "", apkMime), user, password, repo, s) 31 | }, 32 | deleteGithub <<= (packageAlignedPath, githubRepo, cachePasswords, streams) map { (path, repo, cache, s) => 33 | val (user, password) = credentials(cache) 34 | delete(path.getName, user, password, repo, s) 35 | }, 36 | githubRepo := "repo" 37 | )) 38 | 39 | def repoUrl(user: String, repo: String) = { 40 | if (repo.contains("/")) 41 | gitDownloads.format(repo) else 42 | gitDownloads.format(user+"/"+repo) 43 | } 44 | 45 | private def credentials(cache: Boolean) = { 46 | val user = github_user.getOrElse(sys.error("could not get github user - add [user] to "+ 47 | gitConfig.getAbsolutePath)) 48 | 49 | val password = PasswordManager.get("github", user, cache) 50 | .getOrElse(sys.error("could not get password")) 51 | (user, password) 52 | } 53 | 54 | private def getGitConfig(key: String):Option[String] = { 55 | if (gitConfig.exists) 56 | """(?s).*\s*%s\s*=\s*(\w+).*""".format(key).r.findFirstMatchIn(IO.read(gitConfig)).map(_.group(1).trim) 57 | else 58 | None 59 | } 60 | private def github_user = getGitConfig("user") 61 | private def github_token = getGitConfig("token") 62 | 63 | private def upload(upload: Upload, user: String, password: String, 64 | repo: String, s: TaskStreams): Option[String] = { 65 | val post = Post(repoUrl(user, repo), user, password) 66 | 67 | s.log.info("uploading to "+post.getURL) 68 | post.setRequestProperty("Content-Type", "application/json") 69 | 70 | val fw = new OutputStreamWriter(post.getOutputStream()) 71 | fw.write(upload.toJSON) 72 | fw.flush() 73 | fw.close() 74 | post.getResponseCode match { 75 | case 201 => 76 | JSON.parseFull(IO.readStream(post.getInputStream())) match { 77 | case Some(data) => 78 | s3_upload(upload, data.asInstanceOf[Map[String,Any]], s) map { (resp) => 79 | s.log.debug("s3: received "+resp) 80 | s.log.success("Uploaded to s3/github") 81 | (resp \\ "Location").text 82 | } 83 | case _ => None 84 | } 85 | case code => 86 | s.log.error("error (%d): %s".format(code, IO.readStream(post.getErrorStream()))) 87 | None 88 | } 89 | } 90 | 91 | 92 | private def delete(name: String, user: String, password: String, repo: String, s: TaskStreams) = { 93 | val downloads = Get(repoUrl(user,repo), user, password) 94 | s.log.debug("GET "+downloads.getURL) 95 | 96 | downloads.getResponseCode match { 97 | case 200 => 98 | JSON.parseFull(IO.readStream(downloads.getInputStream)).map { (data) => 99 | data.asInstanceOf[List[Map[String,Any]]] 100 | .find( e => e("name") == name) map { (item) => 101 | val id = item("id").asInstanceOf[Double].toInt 102 | val url = "%s/%d".format(repoUrl(user, repo), id) 103 | s.log.debug("DELETE "+url) 104 | val delete = Delete(url, user, password) 105 | delete.getResponseCode() match { 106 | case 204 => 107 | s.log.success("deleted "+name) 108 | case code => 109 | s.log.error("deletion failed (%d): %s" 110 | .format(code, 111 | if (delete.getErrorStream != null) 112 | IO.readStream(delete.getErrorStream) else "")) 113 | } 114 | } 115 | } 116 | case code => 117 | s.log.error("unexpected status %d: %s".format(code, 118 | IO.readStream(downloads.getErrorStream))) 119 | } 120 | } 121 | 122 | private def s3_upload(upload: Upload, data: Map[String,Any], s: TaskStreams):Option[Node] = { 123 | val s3_url = data("s3_url").toString 124 | val s3_post = Post(s3_url, null, null) 125 | 126 | s.log.debug("uploading to "+s3_url) 127 | Post.multipart(s3_post, upload, Seq( /* order matters for signature */ 128 | ("key" , data("path")), 129 | ("acl" , data("acl")), 130 | ("success_action_status", "201"), 131 | ("Filename" , data("name")), 132 | ("AWSAccessKeyId", data("accesskeyid")), 133 | ("Policy" , data("policy")), 134 | ("Signature" , data("signature")), 135 | ("Content-Type" , data("mime_type")) 136 | )).getResponseCode match { 137 | case 201 => Some(scala.xml.XML.load(s3_post.getInputStream())) 138 | case code => 139 | s.log.error("unexpected status code %d: %s ".format( 140 | code, IO.readStream(s3_post.getErrorStream))) 141 | None 142 | } 143 | } 144 | 145 | trait HttpMethod { 146 | def setAuth(conn: HttpURLConnection, user: String, password: String) { 147 | if (user != null && password != null) { 148 | conn.setRequestProperty("Authorization", "Basic "+ 149 | (new sun.misc.BASE64Encoder().encode("%s:%s".format(user, password).getBytes))) 150 | } 151 | } 152 | } 153 | 154 | object Get extends HttpMethod { 155 | def apply(url: String, user: String, password: String):HttpURLConnection = { 156 | val _url = new URL(url) 157 | val get = _url.openConnection().asInstanceOf[HttpURLConnection] 158 | get.setRequestMethod("GET") 159 | setAuth(get, user, password) 160 | get 161 | } 162 | } 163 | 164 | object Post extends HttpMethod { 165 | val CHARSET = "UTF-8" 166 | val CRLF = "\r\n" 167 | 168 | def apply(url: String, user: String, password: String):HttpURLConnection = { 169 | val _url = new URL(url) 170 | val post = _url.openConnection().asInstanceOf[HttpURLConnection] 171 | post.setRequestMethod("POST") 172 | post.setDoOutput(true) 173 | post.setDoInput(true) 174 | setAuth(post, user, password) 175 | post 176 | } 177 | 178 | def multipart(conn: HttpURLConnection, upload: Upload, params: Seq[(String,Any)]) = { 179 | val boundary = java.lang.Long.toHexString(System.currentTimeMillis()) 180 | conn.setRequestProperty("Content-Type", "multipart/form-data; boundary="+boundary) 181 | var writer:PrintWriter = null 182 | try { 183 | val output = conn.getOutputStream() 184 | writer = new PrintWriter(new OutputStreamWriter(output, CHARSET), true) 185 | for ((name,value) <- params) 186 | writer.append("--").append(boundary).append(CRLF) 187 | .append("Content-Disposition: form-data; name=\"").append(name).append('"').append(CRLF) 188 | .append(CRLF) 189 | .append(value.toString) 190 | .append(CRLF) 191 | 192 | writer.flush() 193 | writer.append("--").append(boundary).append(CRLF) 194 | .append("Content-Disposition: form-data; name=\"file\"; filename=\"") 195 | .append(upload.f.getName).append('"').append(CRLF) 196 | .append("Content-Type: ").append(upload.contentType).append(CRLF) 197 | .append("Content-Transfer-Encoding: binary").append(CRLF) 198 | .append(CRLF).flush() 199 | 200 | IO.transferAndClose(new FileInputStream(upload.f), output) 201 | 202 | output.flush() 203 | writer.append(CRLF).flush() 204 | writer.append("--" + boundary + "--").append(CRLF); 205 | } finally { 206 | if (writer != null) writer.close() 207 | } 208 | conn 209 | } 210 | } 211 | 212 | object Delete extends HttpMethod { 213 | def apply(url: String, user: String, password: String):HttpURLConnection = { 214 | val _url = new URL(url) 215 | val delete = _url.openConnection().asInstanceOf[HttpURLConnection] 216 | delete.setRequestMethod("DELETE") 217 | delete.setDoOutput(false) 218 | setAuth(delete, user, password) 219 | delete 220 | } 221 | } 222 | 223 | case class Upload(f: File, description:String, contentType:String) { 224 | def toJSON = """{ 225 | "name": "%s", 226 | "size": %d, 227 | "description": "%s", 228 | "content_type": "%s" 229 | }""".format(f.getName, f.length(), description, contentType) 230 | } 231 | } 232 | --------------------------------------------------------------------------------