├── notes
├── 0.1.1.markdown
├── 0.1.3.markdown
├── 0.1.4.markdown
├── 0.1.2.markdown
├── 0.1.markdown
└── about.markdown
├── src
├── main
│ ├── original-assets
│ │ ├── octocat.xcf
│ │ ├── send-flow.xcf
│ │ ├── gist-it-logo.png
│ │ ├── gist-it-logo.xcf
│ │ └── gist-it-logo_128.png
│ ├── res
│ │ ├── drawable
│ │ │ ├── octocat_big.png
│ │ │ ├── private_gist.png
│ │ │ ├── octocat_bg.xml
│ │ │ └── drop_shadow_down.xml
│ │ ├── drawable-hdpi
│ │ │ └── gist_it_logo.png
│ │ ├── drawable-mdpi
│ │ │ └── gist_it_logo.png
│ │ ├── layout
│ │ │ ├── login.xml
│ │ │ ├── gist_row.xml
│ │ │ └── upload_gist.xml
│ │ ├── values
│ │ │ ├── colors.xml
│ │ │ ├── keys.xml
│ │ │ ├── acra.xml
│ │ │ └── strings.xml
│ │ ├── xml
│ │ │ └── authenticator.xml
│ │ ├── menu
│ │ │ └── menu.xml
│ │ └── layout-land
│ │ │ └── gist_row.xml
│ ├── scala
│ │ └── com
│ │ │ └── zegoggles
│ │ │ └── gist
│ │ │ ├── User.scala
│ │ │ ├── Logger.scala
│ │ │ ├── JsonModel.scala
│ │ │ ├── Implicits.scala
│ │ │ ├── AuthenticatorService.scala
│ │ │ ├── App.scala
│ │ │ ├── Gist.scala
│ │ │ ├── Utils.scala
│ │ │ ├── LoggingWebViewClient.scala
│ │ │ ├── Login.scala
│ │ │ ├── GistList.scala
│ │ │ ├── Api.scala
│ │ │ └── UploadGist.scala
│ └── AndroidManifest.xml
└── test
│ └── scala
│ └── com
│ └── zegoggles
│ └── gist
│ ├── ApiSpec.scala
│ ├── UtilsSpec.scala
│ ├── UserSpec.scala
│ ├── JsonModelSpec.scala
│ └── GistSpec.scala
├── tests
└── src
│ └── main
│ ├── res
│ └── values
│ │ └── strings.xml
│ └── AndroidManifest.xml
├── NOTICE
├── .gitignore
├── project
├── plugins.sbt
└── build.scala
├── LICENSE
└── README.md
/notes/0.1.1.markdown:
--------------------------------------------------------------------------------
1 | * Fix for Android 2.1
2 |
--------------------------------------------------------------------------------
/notes/0.1.3.markdown:
--------------------------------------------------------------------------------
1 | * Fix for Android 2.1
2 |
--------------------------------------------------------------------------------
/notes/0.1.4.markdown:
--------------------------------------------------------------------------------
1 | * Fix for Honeycomb / ICS
2 |
--------------------------------------------------------------------------------
/notes/0.1.2.markdown:
--------------------------------------------------------------------------------
1 | * Accidentally released with wrong keys
2 | * Changed app logo (not allowed to use octocat)
3 |
--------------------------------------------------------------------------------
/src/main/original-assets/octocat.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jberkel/gist-it/HEAD/src/main/original-assets/octocat.xcf
--------------------------------------------------------------------------------
/src/main/original-assets/send-flow.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jberkel/gist-it/HEAD/src/main/original-assets/send-flow.xcf
--------------------------------------------------------------------------------
/src/main/res/drawable/octocat_big.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jberkel/gist-it/HEAD/src/main/res/drawable/octocat_big.png
--------------------------------------------------------------------------------
/src/main/res/drawable/private_gist.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jberkel/gist-it/HEAD/src/main/res/drawable/private_gist.png
--------------------------------------------------------------------------------
/tests/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | gist-it tests
3 |
4 |
--------------------------------------------------------------------------------
/src/main/original-assets/gist-it-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jberkel/gist-it/HEAD/src/main/original-assets/gist-it-logo.png
--------------------------------------------------------------------------------
/src/main/original-assets/gist-it-logo.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jberkel/gist-it/HEAD/src/main/original-assets/gist-it-logo.xcf
--------------------------------------------------------------------------------
/notes/0.1.markdown:
--------------------------------------------------------------------------------
1 | * Initial release: send files to gist, edit existing gists
2 |
3 | * Load/upload gists from other apps via intents
4 |
--------------------------------------------------------------------------------
/src/main/res/drawable-hdpi/gist_it_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jberkel/gist-it/HEAD/src/main/res/drawable-hdpi/gist_it_logo.png
--------------------------------------------------------------------------------
/src/main/res/drawable-mdpi/gist_it_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jberkel/gist-it/HEAD/src/main/res/drawable-mdpi/gist_it_logo.png
--------------------------------------------------------------------------------
/src/main/original-assets/gist-it-logo_128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jberkel/gist-it/HEAD/src/main/original-assets/gist-it-logo_128.png
--------------------------------------------------------------------------------
/NOTICE:
--------------------------------------------------------------------------------
1 | gist-it
2 | Copyright 2011 Jan Berkel
3 |
4 | acra - Application Crash Report for Android
5 | Copyright 2010 Emmanuel Astier & Kevin Gaudin
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | target
2 | lib_managed
3 | src_managed
4 | project/boot
5 | project/plugins/project
6 | gen
7 | .idea
8 | *.iml
9 | *.swp
10 | tmp
11 | tags
12 | keys_market.xml
13 |
--------------------------------------------------------------------------------
/notes/about.markdown:
--------------------------------------------------------------------------------
1 | [gist-it][1] is an open source Android [gist][2] API client written in Scala
2 | ([Android Market link][3]).
3 |
4 | [1]: https://github.com/jberkel/gist-it
5 | [2]: https://gist.github.com/
6 | [3]: https://market.android.com/details?id=com.zegoggles.gist
7 |
--------------------------------------------------------------------------------
/src/main/res/drawable/octocat_bg.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
--------------------------------------------------------------------------------
/src/main/res/layout/login.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | #AAE2E2E2
5 | #00ffffff
6 | #fffeeb
7 | #e9e9e9
8 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | resolvers ++= Seq(
2 | Resolver.file(System.getProperty("user.home") + "/.ivy2/local"),
3 | Resolver.url("scalasbt snapshots", new
4 | URL("http://scalasbt.artifactoryonline.com/scalasbt/sbt-plugin-snapshots"))(Resolver.ivyStylePatterns)
5 | )
6 |
7 | addSbtPlugin("org.scala-sbt" % "sbt-android-plugin" % "0.6.1-SNAPSHOT")
8 |
--------------------------------------------------------------------------------
/src/main/res/values/keys.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 | e9a53e8a136ac429a4e9
6 | 69f3a4728277be9e4669e53afe3a2add1e2786e7
7 | http://zegoggl.es/oauth/gist-it
8 |
9 |
--------------------------------------------------------------------------------
/src/main/res/xml/authenticator.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/src/main/res/drawable/drop_shadow_down.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
--------------------------------------------------------------------------------
/src/main/scala/com/zegoggles/gist/User.scala:
--------------------------------------------------------------------------------
1 | package com.zegoggles.gist
2 |
3 | import org.json.JSONObject
4 |
5 | object User extends JsonModel[User] {
6 | def apply(j: JSONObject):Option[User] = {
7 | val root = if (j.has("user")) j.getJSONObject("user") else j
8 | Some(User(root.getInt("id"),
9 | root.getString("name"),
10 | root.getString("login"),
11 | root.getString("email")))
12 | }
13 | }
14 | case class User(id: Int, name:String, login:String, email:String)
15 |
--------------------------------------------------------------------------------
/src/main/scala/com/zegoggles/gist/Logger.scala:
--------------------------------------------------------------------------------
1 | package com.zegoggles.gist
2 |
3 | import android.util.Log
4 |
5 | trait Logger {
6 | def log(msg: String) { Log.d(App.TAG, msg) }
7 | def warn(msg: String, e:Exception*) {
8 | if (e.isEmpty)
9 | Log.w(App.TAG, msg)
10 | else
11 | Log.w(App.TAG, msg, e.headOption.get)
12 | }
13 | }
14 |
15 | trait StdoutLogger extends Logger {
16 | override def log(msg: String) {
17 | System.err.println(msg)
18 | }
19 |
20 | override def warn(msg: String, e:Exception*) {
21 | System.err.println(msg)
22 | }
23 | }
--------------------------------------------------------------------------------
/src/main/res/menu/menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 |
--------------------------------------------------------------------------------
/src/main/res/layout/gist_row.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
15 |
--------------------------------------------------------------------------------
/src/main/res/layout-land/gist_row.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
15 |
--------------------------------------------------------------------------------
/tests/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/main/res/values/acra.xml:
--------------------------------------------------------------------------------
1 |
2 | Epic fail
3 | gist-it has crashed
4 | Please click here to help fix the issue.
5 |
6 |
7 | An unexpected error occurred forcing the application to stop.
8 | Please help us fix this by sending us a crash report.
9 |
10 |
11 |
12 | gist-it has crashed
13 |
14 | Comment (optional):
15 |
16 | Cheers!
17 |
18 |
--------------------------------------------------------------------------------
/src/test/scala/com/zegoggles/gist/ApiSpec.scala:
--------------------------------------------------------------------------------
1 | package com.zegoggles.gist
2 |
3 | import org.specs2.mutable._
4 | class ApiSpec extends Specification {
5 | "the API" should {
6 |
7 | "parse the token response" in {
8 | val token = Api.parseTokenResponse("access_token=807e750b891b3fc47b0c951b4c11c0b610195b73&token_type=bearer")
9 | token must beSome
10 | token.get.access must be equalTo "807e750b891b3fc47b0c951b4c11c0b610195b73"
11 | }
12 |
13 | "return none if it cannot be parsed" in {
14 | Api.parseTokenResponse("foo=bar&token_type=bearer") must beNone
15 | Api.parseTokenResponse("") must beNone
16 | }
17 |
18 | "generate JSON from a Scala Map structure" in {
19 | val input = Map("foo"->"bar", "bool"->true, "int"->10, "baz"->Map("buu"->"hello"),"double"->10.44d)
20 |
21 | """{"baz":{"buu":"hello"},"int":10,"foo":"bar","bool":true,"double":10.44}""" must
22 | be equalTo Api.map2Json(input).toString
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/test/scala/com/zegoggles/gist/UtilsSpec.scala:
--------------------------------------------------------------------------------
1 | package com.zegoggles.gist
2 |
3 | import org.specs2.mutable.Specification
4 |
5 | class UtilsSpec extends Specification {
6 | "human readable size" should {
7 | "return human readable values" in {
8 | "323 bytes" must be equalTo Utils.readableSize(323)
9 | "2 kb" must be equalTo Utils.readableSize(2048)
10 | "1 mb" must be equalTo Utils.readableSize(1024 * 1024 + 20)
11 | "3 gb" must be equalTo Utils.readableSize(1024 * 1024 * 1024 * 3L)
12 | }
13 | }
14 |
15 | "human readable time" should {
16 | "return human readable values" in {
17 | "just now" must be equalTo Utils.readableTime(5)
18 | "50 seconds ago" must be equalTo Utils.readableTime(50)
19 | "3 minutes ago" must be equalTo Utils.readableTime(190)
20 | "1 hour ago" must be equalTo Utils.readableTime(3800)
21 | "4 days ago" must be equalTo Utils.readableTime(86400 * 4 + 3000)
22 | "2 months ago" must be equalTo Utils.readableTime(86400 * 62)
23 | "1 year ago" must be equalTo Utils.readableTime(86400 * 30 * 12)
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | (The MIT License)
2 |
3 | Copyright (c) 2011 Jan Berkel
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | 'Software'), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/src/main/scala/com/zegoggles/gist/JsonModel.scala:
--------------------------------------------------------------------------------
1 | package com.zegoggles.gist
2 |
3 | import scala.Some
4 | import org.json.{JSONArray, JSONObject, JSONException}
5 | import java.text.SimpleDateFormat
6 | import java.util.TimeZone
7 |
8 | trait JsonModel[T] {
9 | lazy val iso8601 = {
10 | val format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
11 | format.setTimeZone(TimeZone.getTimeZone("UTC"))
12 | format
13 | }
14 | def getDate(s: String ) = iso8601.parse(s)
15 |
16 | def apply(j: JSONObject): Option[T]
17 | def apply(s: String):Option[T] = try {
18 | apply(new JSONObject(s))
19 | } catch {
20 | case e:JSONException => None
21 | }
22 |
23 | def list(s: String) = JSONArrayWrapper(s, apply(_:JSONObject))
24 | def list(l: JSONArray) = JSONArrayWrapper(l, apply(_:JSONObject))
25 | }
26 |
27 | object JSONArrayWrapper {
28 | def apply[T](l: JSONArray, transform: JSONObject => Option[T]):Option[JSONArrayWrapper[T]] =
29 | Some(new JSONArrayWrapper[T](l, transform))
30 |
31 | def apply[T](s: String, transform: JSONObject => Option[T]):Option[JSONArrayWrapper[T]] =
32 | try {
33 | apply(new JSONArray(s), transform)
34 | } catch {
35 | case e:JSONException => None
36 | }
37 | }
38 |
39 | class JSONArrayWrapper[+T](val list: JSONArray, val transform: JSONObject => Option[T]) extends IndexedSeq[T] {
40 | def apply(idx: Int) = transform(list.getJSONObject(idx)).get
41 | def length = list.length()
42 | }
43 |
--------------------------------------------------------------------------------
/src/test/scala/com/zegoggles/gist/UserSpec.scala:
--------------------------------------------------------------------------------
1 | package com.zegoggles.gist
2 |
3 | import org.specs2.mutable._
4 |
5 | class UserSpec extends Specification {
6 | "A user" should {
7 | val user = """
8 | {
9 | "user": {
10 | "gravatar_id": "b8dbb1987e8e5318584865f880036796",
11 | "company": "GitHub",
12 | "name": "Chris Wanstrath",
13 | "created_at": "2007/10/19 22:24:19 -0700",
14 | "location": "San Francisco, CA",
15 | "public_repo_count": 98,
16 | "public_gist_count": 270,
17 | "blog": "http://chriswanstrath.com/",
18 | "following_count": 196,
19 | "id": 2,
20 | "type": "User",
21 | "permission": null,
22 | "followers_count": 1692,
23 | "login": "defunkt",
24 | "email": "chris@wanstrath.com"
25 | }
26 | }
27 | """
28 |
29 | "be parseable from JSON" in {
30 | val u = User(user).get
31 | u.name must be equalTo "Chris Wanstrath"
32 | u.id must be equalTo 2
33 | u.login must be equalTo "defunkt"
34 | u.email must be equalTo "chris@wanstrath.com"
35 | }
36 |
37 | "return None if not parseable" in {
38 | User("bla") must beNone
39 | }
40 |
41 | "be pattern matchable" in {
42 | User(user) match {
43 | case Some(User(2, _, login, _)) => login must be equalTo "defunkt"
44 | case _ => error("default case")
45 | }
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/src/main/scala/com/zegoggles/gist/Implicits.scala:
--------------------------------------------------------------------------------
1 | package com.zegoggles.gist
2 |
3 | import org.apache.http.HttpEntity
4 | import io.Source
5 | import android.view.View.OnClickListener
6 | import android.view.{KeyEvent, View}
7 | import android.widget.TextView.OnEditorActionListener
8 | import org.json.JSONObject
9 | import android.content.DialogInterface
10 | import android.widget.{CheckBox, TextView}
11 |
12 | object Implicits {
13 | implicit def funToRunnable(f: => Unit) = new Runnable { def run() { f }}
14 | implicit def funToRunnable2(f: () => Unit) = new Runnable { def run() { f() }}
15 |
16 | implicit def textViewToString(tv: TextView):String = tv.getText.toString
17 | implicit def mapToJSON(map: Map[String,Any]):JSONObject = Api.map2Json(map)
18 | implicit def jsonToString(json: JSONObject):String = json.toString
19 |
20 | implicit def funToDialogOnClickListener[F](f: (DialogInterface, Int) => F)
21 | = new DialogInterface.OnClickListener {
22 | def onClick(dialog: DialogInterface, which: Int) {
23 | f(dialog, which)
24 | }
25 | }
26 |
27 | implicit def funToDialogOnClickListener[F](f: () => F)
28 | = new DialogInterface.OnClickListener {
29 | def onClick(dialog: DialogInterface, which: Int) {
30 | f()
31 | }
32 | }
33 |
34 | implicit def funToOnClickListener[F](f: View => F) = new OnClickListener {
35 | def onClick(v: View) { f(v) }
36 | }
37 |
38 | implicit def funToOnEditorActionListener(f: (View,Int,KeyEvent) => Boolean) = new OnEditorActionListener {
39 | def onEditorAction(v: TextView, actionId: Int, event: KeyEvent) = { f(v,actionId,event) }
40 | }
41 |
42 | implicit def entityToString(e: HttpEntity): String =
43 | Source.fromInputStream(e.getContent).getLines().mkString
44 | }
--------------------------------------------------------------------------------
/src/main/scala/com/zegoggles/gist/AuthenticatorService.scala:
--------------------------------------------------------------------------------
1 | package com.zegoggles.gist
2 |
3 | import android.accounts._
4 | import android.app.Service
5 | import android.os.Bundle
6 | import java.lang.String
7 | import android.content.{Context, Intent}
8 |
9 | class AuthenticatorService extends Service {
10 | lazy val authenticator = new GithubAuthenticator(this)
11 |
12 | def onBind(intent: Intent) = {
13 | if (intent.getAction == AccountManager.ACTION_AUTHENTICATOR_INTENT) {
14 | authenticator.getIBinder
15 | } else {
16 | null
17 | }
18 | }
19 |
20 | class GithubAuthenticator(val context: Context) extends AbstractAccountAuthenticator(context) {
21 | def addAccount(resp: AccountAuthenticatorResponse, acctype: String, tokenType: String, features: Array[String], options: Bundle) = {
22 | val reply = new Bundle()
23 | val intent = (new Intent(context, classOf[Login]))
24 | .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
25 | .putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, resp)
26 | reply.putParcelable(AccountManager.KEY_INTENT, intent)
27 | reply
28 | }
29 |
30 | def hasFeatures(p1: AccountAuthenticatorResponse, p2: Account, p3: Array[String]) = null
31 | def updateCredentials(p1: AccountAuthenticatorResponse, p2: Account, p3: String, p4: Bundle) = null
32 | def getAuthTokenLabel(p1: String) = ""
33 | def getAuthToken(p1: AccountAuthenticatorResponse, p2: Account, p3: String, p4: Bundle) = null
34 | def confirmCredentials(p1: AccountAuthenticatorResponse, p2: Account, p3: Bundle) = null
35 | def editProperties(p1: AccountAuthenticatorResponse, p2: String) = null
36 | }
37 | }
38 |
39 |
--------------------------------------------------------------------------------
/src/main/scala/com/zegoggles/gist/App.scala:
--------------------------------------------------------------------------------
1 | package com.zegoggles.gist
2 |
3 | import android.accounts.{Account, OnAccountsUpdateListener, AccountManager}
4 | import org.acra.annotation.ReportsCrashes
5 | import org.acra.{ReportingInteractionMode, ACRA}
6 | import android.os.StrictMode.ThreadPolicy
7 | import android.os.{Build, StrictMode}
8 |
9 | @ReportsCrashes(formUri = "https://bugsense.appspot.com/api/acra?api_key=02946e8e", formKey = "",
10 | mode = ReportingInteractionMode.NOTIFICATION,
11 | resNotifTickerText = R.string.crash_notif_ticker_text,
12 | resNotifTitle = R.string.crash_notif_title,
13 | resNotifText = R.string.crash_notif_text,
14 | resDialogText = R.string.crash_dialog_text,
15 | resDialogTitle = R.string.crash_dialog_title,
16 | resDialogCommentPrompt = R.string.crash_dialog_comment_prompt,
17 | resDialogOkToast = R.string.crash_dialog_ok_toast)
18 | class App extends android.app.Application with Logger with ApiHolder {
19 | var preloadedList:Option[Seq[Gist]] = None
20 |
21 | override def onCreate() {
22 | ACRA.init(this)
23 |
24 | // reload current token and clear list when accounts get changed
25 | AccountManager.get(this).addOnAccountsUpdatedListener(new OnAccountsUpdateListener {
26 | def onAccountsUpdated(accounts: Array[Account]) {
27 | api.token = token
28 | preloadedList = None
29 | }
30 | }, /*handler*/ null, /*updateImmediately*/ false)
31 |
32 |
33 | // emulate honeycomb+ network crash behaviour in Gingerbread
34 | if (Build.VERSION.SDK_INT >= 9 && Build.VERSION.SDK_INT < 11) {
35 | StrictMode.setThreadPolicy(new ThreadPolicy.Builder().detectNetwork().penaltyDeath().build())
36 | }
37 |
38 | super.onCreate()
39 | }
40 | }
41 |
42 | object App {
43 | val TAG = "gist-it"
44 | }
45 |
--------------------------------------------------------------------------------
/project/build.scala:
--------------------------------------------------------------------------------
1 | import sbt._
2 | import Keys._
3 | import AndroidKeys._
4 | import Github._
5 |
6 | object General {
7 | val settings = Defaults.defaultSettings ++ Seq (
8 | version := "0.1.4",
9 | versionCode := 5,
10 | organization := "com.zegoggles",
11 | scalaVersion := "2.8.1" // 2.9.1 fail with DEXOPT install errors
12 | )
13 |
14 | val androidSettings =
15 | settings ++
16 | Seq (
17 | platformName := "android-10"
18 | )
19 |
20 | val androidProjectSettings =
21 | androidSettings ++
22 | AndroidProject.androidSettings
23 |
24 | val androidFullProjectSettings =
25 | androidProjectSettings ++
26 | TypedResources.settings ++
27 | AndroidMarketPublish.settings ++
28 | AndroidManifestGenerator.settings ++
29 | Github.settings
30 | }
31 |
32 | object AndroidBuild extends Build {
33 | // MainProject
34 | lazy val app = Project (
35 | "gist-it",
36 | file("."),
37 | settings = General.androidFullProjectSettings ++ Seq (
38 | keyalias in Android := "jberkel",
39 | libraryDependencies ++= Seq(
40 | "org.acra" % "acra" % "4.2.3"
41 | //"com.github.jbrechtel" %% "robospecs" % "0.1-SNAPSHOT" % "test"
42 | ),
43 | compileOrder := CompileOrder.JavaThenScala,
44 | useProguard in Android := true,
45 | githubRepo in Android := "gist-it",
46 | cachePasswords in Android := true,
47 | resolvers ++= Seq(
48 | MavenRepository("acra release repository", "http://acra.googlecode.com/svn/repository/releases"),
49 | MavenRepository("robospecs snapshots", "http://jbrechtel.github.com/repo/snapshots"),
50 | MavenRepository("scala tools snapshots", "http://scala-tools.org/repo-snapshots")
51 | )
52 | ) ++ AndroidInstall.settings
53 | )
54 | }
55 |
--------------------------------------------------------------------------------
/src/test/scala/com/zegoggles/gist/JsonModelSpec.scala:
--------------------------------------------------------------------------------
1 | package com.zegoggles.gist
2 |
3 | import org.specs2.mutable.Specification
4 | import org.json.JSONObject
5 |
6 | class JsonModelSpec extends Specification {
7 | "a valid json user list" should {
8 | "return a list of users" in {
9 | val l = """
10 | [
11 | {
12 | "user": {
13 | "gravatar_id": "b8dbb1987e8e5318584865f880036796",
14 | "company": "GitHub",
15 | "name": "Chris Wanstrath",
16 | "created_at": "2007/10/19 22:24:19 -0700",
17 | "location": "San Francisco, CA",
18 | "public_repo_count": 98,
19 | "public_gist_count": 270,
20 | "blog": "http://chriswanstrath.com/",
21 | "following_count": 196,
22 | "id": 2,
23 | "type": "User",
24 | "permission": null,
25 | "followers_count": 1692,
26 | "login": "defunkt",
27 | "email": "chris@wanstrath.com"
28 | }
29 | },
30 | {
31 | "user": {
32 | "gravatar_id": "12345",
33 | "company": "GitHub",
34 | "name": "Foo Bar",
35 | "created_at": "2007/10/19 22:24:19 -0700",
36 | "location": "San Foo, CA",
37 | "public_repo_count": 98,
38 | "public_gist_count": 270,
39 | "blog": "http://chriswanstrath.com/",
40 | "following_count": 196,
41 | "id": 3,
42 | "type": "User",
43 | "permission": null,
44 | "followers_count": 1692,
45 | "login": "defunkt",
46 | "email": "chris@wanstrath.com"
47 | }
48 | }
49 | ]
50 | """
51 | val list = JSONArrayWrapper(l, User(_:JSONObject)).get
52 | list.size must be equalTo 2
53 | }
54 | }
55 |
56 | "an invalid list" should {
57 | "return None" in {
58 | JSONArrayWrapper("""invalid""", User(_:JSONObject)) must beNone
59 | }
60 | }
61 | }
--------------------------------------------------------------------------------
/src/main/scala/com/zegoggles/gist/Gist.scala:
--------------------------------------------------------------------------------
1 | package com.zegoggles.gist
2 |
3 | import java.io.IOException
4 | import scala.Some
5 | import java.net.URL
6 | import org.json.JSONObject
7 | import android.os.Bundle
8 | import android.net.Uri
9 |
10 | object Gist extends JsonModel[Gist] {
11 | def apply(j: JSONObject) = {
12 | val files = j.getJSONObject("files").names()
13 | val file = j.getJSONObject("files").getJSONObject(files.getString(0))
14 | val history = j.optJSONArray("history")
15 | val last_modified = if (history != null && history.length() > 0)
16 | getDate(history.getJSONObject(0).getString("committed_at"))
17 | else
18 | getDate(j.getString("created_at"))
19 |
20 | Some(Gist(j.getString("id"),
21 | if (j.isNull("description")) null else j.getString("description"),
22 | file.getString("filename"),
23 | file.getLong("size"),
24 | j.getBoolean("public"),
25 | j.getString("url"),
26 | file.getString("raw_url"),
27 | file.optString("content"),
28 | last_modified.getTime / 1000L))
29 | }
30 | }
31 |
32 | case class Gist(id: String, description: String,
33 | filename: String, size: Long,
34 | public: Boolean, url: String, raw_url: String,
35 | content: String, last_modified: Long) {
36 |
37 | override def toString = "Gist %d".format(id)
38 | def asHtml = {filename} ({size_in_words}, {last_modified_ago})
39 | lazy val public_url = "https://gist.github.com/" + url.substring(url.lastIndexOf("/")+1)
40 | lazy val public_uri = Uri.parse(public_url)
41 | lazy val uri = Uri.parse(raw_url)
42 |
43 | def raw_content:Option[String] = try { Some(load)} catch { case e:IOException => None }
44 | def load = io.Source.fromURL(new URL(raw_url)).mkString
45 |
46 | def size_in_words = Utils.readableSize(size)
47 | def last_modified_ago = Utils.readableTime(System.currentTimeMillis() / 1000L - last_modified)
48 | def color = if (public) R.color.public_gist else R.color.private_gist
49 |
50 | def asBundle = {
51 | val b = new Bundle()
52 | b.putString("id", id)
53 | b.putString("filename", filename)
54 | b.putString("description", description)
55 | b.putString("raw_url", raw_url)
56 | b.putString("content", content)
57 | b.putBoolean("public", public)
58 | b
59 | }
60 | }
61 |
62 |
--------------------------------------------------------------------------------
/src/main/scala/com/zegoggles/gist/Utils.scala:
--------------------------------------------------------------------------------
1 | package com.zegoggles.gist
2 |
3 | import android.widget.TextView
4 | import android.text.method.LinkMovementMethod
5 | import android.view.View
6 | import android.text.style.ClickableSpan
7 | import android.text.{SpannableString, Spanned, Spannable}
8 |
9 | object Utils {
10 | /**
11 | * Adapted from the {@link android.text.util.Linkify} class. Changes the
12 | * first instance of {@code link} into a clickable link attached to the given function.
13 | */
14 | def clickify(v: TextView, clickableText: String, listener: => Unit):Boolean = {
15 | val text = v.getText
16 | val string = text.toString
17 | val span = new ClickableSpan {
18 | def onClick(widget: View) {
19 | listener
20 | }
21 | }
22 | val start = string.indexOf(clickableText)
23 | if (start != -1) {
24 | val end = start + clickableText.length()
25 | text match {
26 | case spannable:Spannable => spannable.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
27 | case other =>
28 | val s = SpannableString.valueOf(other)
29 | s.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
30 | v.setText(s)
31 | }
32 | val m = v.getMovementMethod
33 | if (m == null || !(m.isInstanceOf[LinkMovementMethod])) {
34 | v.setMovementMethod(LinkMovementMethod.getInstance())
35 | }
36 | true
37 | } else false
38 | }
39 |
40 | def readableSize(b: Long) = b match {
41 | case c if c > 0 && c < 1024 => b+" bytes"
42 | case c if c > 1024 && c < 1024*1024 => (b/1024).round+" kb"
43 | case c if c > 1024*1024 && c < 1024*1024*1024 => (b/1024/1024).round+" mb"
44 | case c => (b/1024/1024/1024).round+" gb"
45 | }
46 |
47 | def readableTime(t: Long) = {
48 | def format(s: Long, divisor:Int, name:String) = {
49 | val value = (s/divisor).round
50 | value+" "+((if (value == 1) name else name+"s")+ " ago")
51 | }
52 | t match {
53 | case s if s < 10 => "just now"
54 | case s if s < 60 => format(s, 1, "second")
55 | case s if s < 3600 => format(s, 60, "minute")
56 | case s if s < 3600*24 => format(s, 3600, "hour")
57 | case s if s < 86400*30 => format(s, 86400, "day")
58 | case s if s < 86400*30*12 => format(s, 86400*30, "month")
59 | case s => format(s, 86400*30*12, "year")
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [gist-it][]
2 |
3 | ![icon][]
4 |
5 | Open source Android gist API client written in Scala.
6 |
7 | The Android app uses the new [github api][] to provide a "send to gist"
8 | feature for most applications which have a "Send" or "Share" menu.
9 |
10 | Check the following screenshot to get an idea of the flow (this example uses the
11 | [ColorNote Notepad][] app)
12 |
13 | ![flow][]
14 |
15 | By default gists are created anonymously - you can add your github account
16 | using Android's "Accounts & Sync" settings or follow the instructions in the
17 | gist app itself.
18 |
19 | With an associated account you also have the ability to edit existing gists -
20 | Use "Load gist" from the menu, make changes and upload it again.
21 |
22 | ## Usage from other apps
23 |
24 | If your are developing an Android app and want to make use of the gist api you
25 | can do so with intents. At the moment there are two actions exposed:
26 |
27 | ### picking/loading a gist
28 |
29 | Intent intent = new Intent("com.zegoggles.gist.PICK");
30 | intent.putExtra("load_gist", false); // load gist content, defaults to true
31 | startActivityForResult(intent, 0)
32 |
33 | ### uploading a gist
34 |
35 | startActivityForResult(new Intent("com.zegoggles.gist.UPLOAD")
36 | .putExtra(Intent.EXTRA_TEXT, "text123")
37 | .putExtra("public", false)
38 | .putExtra("description", "testing gist upload via intent"), 0);
39 |
40 | ## Building from source
41 |
42 | You need [sbt][] (simple-build-tool, >= 0.11.2 ) in order to
43 | build the project,
44 |
45 | $ export ANDROID_HOME=/path/to/sdk # or ANDROID_SDK_{HOME,ROOT}
46 | $ sbt android:package-debug
47 |
48 | To run tests:
49 |
50 | $ sbt test
51 |
52 | Pull requests welcome, especially the design needs some love (hint, hint).
53 |
54 | ## Credits / License
55 |
56 | See LICENSE. Post it graphic by [christianalm][].
57 |
58 | [![][FlattrButton]][FlattrLink]
59 |
60 | [gist-it]: https://market.android.com/details?id=com.zegoggles.gist
61 | [gist]: https://github.com/blog/118-here-s-the-gist-of-it
62 | [github api]: http://developer.github.com/v3/gists/
63 | [ColorNote Notepad]: https://market.android.com/details?id=com.socialnmobile.dictapps.notepad.color.note
64 | [sbt]: http://code.google.com/p/simple-build-tool/
65 | [sbt-android-plugin]: https://github.com/jberkel/android-plugin
66 | [flow]: https://github.com/downloads/jberkel/gist-it/send_flow.png
67 | [icon]: https://github.com/downloads/jberkel/gist-it/gist-it-logo_128.png
68 | [christianalm]: http://graphicriver.net/user/cristianalm
69 | [FlattrLink]: https://flattr.com/thing/304054/gist-it
70 | [FlattrButton]: http://api.flattr.com/button/button-static-50x60.png
71 |
--------------------------------------------------------------------------------
/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | gist-it
3 | gist-it
4 | com.zegoggles.gist
5 | Loading
6 | Authenticating
7 | Token exchange failed
8 | Loading gists
9 | Gist it
10 | Uploading
11 | Public
12 | Gist uploaded
13 | Gist description…
14 | name this file…
15 | Make this gist visible to everybody
16 | Content
17 | Empty
18 | The gist is empty. Are you sure you want to upload it?
19 |
20 | Error accessing content (permission denied)
21 | Uploading failed"
22 | Loading gist failed: %s
23 | Getting gists failed"
24 |
25 | Error while connecting
26 | The URL of the newly created gist will be available in the clipboard after a successful upload.
27 |
28 |
29 | There is currently no registered gist account so the content will be uploaded anonymously.
30 | %s to post authenticated gists.
31 |
32 |
33 |
34 | Add an account
35 |
36 |
37 | Gist is a simple way to share snippets and pastes with others.
38 | All gists are git repositories, so they are automatically versioned, forkable and usable
39 | as a git repository.
40 | See gist.github.com for more information.
41 |
42 |
43 | Replace gist
44 | Refresh
45 |
46 | Fork me
47 | Clear
48 | Load gist
49 | Loading gist
50 | https://github.com/jberkel/gist-it
51 | Gist copied to clipboard
52 |
53 |
--------------------------------------------------------------------------------
/src/main/scala/com/zegoggles/gist/LoggingWebViewClient.scala:
--------------------------------------------------------------------------------
1 | package com.zegoggles.gist
2 |
3 | import android.graphics.Bitmap
4 | import java.lang.String
5 | import android.webkit._
6 | import android.os.Message
7 | import android.net.http.SslError
8 | import android.view.KeyEvent
9 |
10 | class LoggingWebViewClient extends WebViewClient with Logger {
11 | override def shouldOverrideUrlLoading(view: WebView, url: String): Boolean = {
12 | log("shouldOverrideUrlLoading(" + url + ")")
13 | super.shouldOverrideUrlLoading(view, url)
14 | }
15 |
16 | override def onPageStarted(view: WebView, url: String, favicon: Bitmap) {
17 | log("onPageStarted(" + url + ")")
18 | super.onPageStarted(view, url, favicon)
19 | }
20 |
21 | override def onPageFinished(view: WebView, url: String) {
22 | log("onPageFinished(" + url + ")")
23 | super.onPageFinished(view, url)
24 | }
25 |
26 | override def onLoadResource(view: WebView, url: String) {
27 | log("onLoadResource(" + url + ")")
28 | super.onLoadResource(view, url)
29 | }
30 |
31 | override def onTooManyRedirects(view: WebView, cancelMsg: Message, continueMsg: Message) {
32 | log("onTooManyRedirects(" + cancelMsg + "," + continueMsg + ")")
33 | super.onTooManyRedirects(view, cancelMsg, continueMsg)
34 | }
35 |
36 | override def onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) {
37 | log("onReceivedError(" + errorCode + "," + description + "," + failingUrl + ")")
38 | super.onReceivedError(view, errorCode, description, failingUrl)
39 | }
40 |
41 | override def onFormResubmission(view: WebView, dontResend: Message, resend: Message) {
42 | log("onFormResubmission(" + dontResend + "," + resend + ")")
43 | super.onFormResubmission(view, dontResend, resend)
44 | }
45 |
46 | override def doUpdateVisitedHistory(view: WebView, url: String, isReload: Boolean) {
47 | log("doUpdateVisitedHistory(" + url + "," + isReload + ")")
48 | super.doUpdateVisitedHistory(view, url, isReload)
49 | }
50 |
51 | override def onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
52 | log("onReceivedSslError(" + handler + "," + error + ")")
53 | super.onReceivedSslError(view, handler, error)
54 | }
55 |
56 | override def onReceivedHttpAuthRequest(view: WebView, handler: HttpAuthHandler, host: String, realm: String) {
57 | log("onReceivedHttpAuthRequest(" + handler + "," + host + "," + realm + ")")
58 | super.onReceivedHttpAuthRequest(view, handler, host, realm)
59 | }
60 |
61 | override def shouldOverrideKeyEvent(view: WebView, event: KeyEvent): Boolean = {
62 | log("shouldOverrideKeyEvent(" + event + ")")
63 | super.shouldOverrideKeyEvent(view, event)
64 | }
65 |
66 | override def onUnhandledKeyEvent(view: WebView, event: KeyEvent) {
67 | log("onUnhandledKeyEvent(" + event + ")")
68 | super.onUnhandledKeyEvent(view, event)
69 | }
70 |
71 | override def onScaleChanged(view: WebView, oldScale: Float, newScale: Float) {
72 | log("onScaleChanged(" + oldScale + "," + newScale + ")")
73 | super.onScaleChanged(view, oldScale, newScale)
74 | }
75 | }
76 |
77 |
78 |
--------------------------------------------------------------------------------
/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
50 |
51 |
52 |
53 |
54 |
57 |
58 |
59 |
60 |
61 |
62 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/src/test/scala/com/zegoggles/gist/GistSpec.scala:
--------------------------------------------------------------------------------
1 | package com.zegoggles.gist
2 |
3 | import org.specs2.mutable.Specification
4 |
5 | class GistSpec extends Specification {
6 | "A valid gist" should {
7 | val valid_gist = """
8 | {
9 | "url": "https://api.github.com/gists/1",
10 | "id": "1",
11 | "description": "description of gist",
12 | "public": true,
13 | "user": {
14 | "login": "octocat",
15 | "id": 1,
16 | "gravatar_url": "https://github.com/images/error/octocat_happy.gif",
17 | "url": "https://api.github.com/users/octocat"
18 | },
19 | "files": {
20 | "ring.erl": {
21 | "size": 932,
22 | "filename": "ring.erl",
23 | "raw_url": "https://gist.github.com/raw/365370/8c4d2d43d178df44f4c03a7f2ac0ff512853564e/ring.erl",
24 | "content": "contents of gist"
25 | }
26 | },
27 | "comments": 0,
28 | "git_pull_url": "git://gist.github.com/1.git",
29 | "git_push_url": "git@gist.github.com:1.git",
30 | "created_at": "2010-04-14T02:15:15Z",
31 | "forks": [
32 | {
33 | "user": {
34 | "login": "octocat",
35 | "id": 1,
36 | "gravatar_url": "https://github.com/images/error/octocat_happy.gif",
37 | "url": "https://api.github.com/users/octocat"
38 | },
39 | "url": "https://api.github.com/gists/5",
40 | "created_at": "2011-04-14T16:00:49Z"
41 | }
42 | ],
43 | "history": [
44 | {
45 | "url": "https://api.github.com/gists/1/57a7f021a713b1c5a6a199b54cc514735d2d462f",
46 | "version": "57a7f021a713b1c5a6a199b54cc514735d2d462f",
47 | "user": {
48 | "login": "octocat",
49 | "id": 1,
50 | "gravatar_url": "https://github.com/images/error/octocat_happy.gif",
51 | "url": "https://api.github.com/users/octocat"
52 | },
53 | "change_status": {
54 | "deletions": 0,
55 | "additions": 180,
56 | "total": 180
57 | },
58 | "committed_at": "2010-04-14T02:15:15Z"
59 | }
60 | ]
61 | }
62 | """
63 |
64 | lazy val gist = Gist(valid_gist).get
65 |
66 | "be parseable from JSON" in {
67 | gist.id must be equalTo "1"
68 | gist.description must be equalTo "description of gist"
69 | gist.public must be equalTo true
70 | gist.url must be equalTo "https://api.github.com/gists/1"
71 | gist.size must be equalTo 932
72 | gist.filename must be equalTo "ring.erl"
73 | gist.raw_url must be equalTo "https://gist.github.com/raw/365370/8c4d2d43d178df44f4c03a7f2ac0ff512853564e/ring.erl"
74 | gist.content must be equalTo "contents of gist"
75 | }
76 |
77 |
78 | "have the last modified timestamp" in {
79 | gist.last_modified must be equalTo 1271211315
80 | }
81 |
82 | "show the last edit in a nice form" in {
83 | gist.last_modified_ago must be equalTo "1 year ago"
84 | }
85 |
86 | "have a public url" in {
87 | gist.public_url must be equalTo "https://gist.github.com/1"
88 | }
89 |
90 | "be pattern matchable" in {
91 | Gist(valid_gist) match {
92 | case Some(Gist("1", _, _, size, _, _, _, _, _)) => size must be equalTo 932
93 | case _ => error("default case")
94 | }
95 | }
96 |
97 | "have a method to access the backing content" in {
98 | gist.raw_content.get.size must be equalTo 932
99 | }
100 |
101 | "be parseable without content" in {
102 | val short_gist = """
103 | {
104 | "url": "https://api.github.com/gists/1",
105 | "id": "1",
106 | "description": null,
107 | "public": true,
108 | "user": {
109 | "login": "octocat",
110 | "id": 1,
111 | "gravatar_url": "https://github.com/images/error/octocat_happy.gif",
112 | "url": "https://api.github.com/users/octocat"
113 | },
114 | "files": {
115 | "ring.erl": {
116 | "size": 932,
117 | "filename": "ring.erl",
118 | "raw_url": "https://gist.github.com/raw/365370/8c4d2d43d178df44f4c03a7f2ac0ff512853564e/ring.erl",
119 | }
120 | },
121 | "comments": 0,
122 | "git_pull_url": "git://gist.github.com/1.git",
123 | "git_push_url": "git@gist.github.com:1.git",
124 | "created_at": "2010-04-14T02:15:15Z"
125 | }
126 | """
127 | val gist = Gist(short_gist).get
128 | gist.id must be equalTo "1"
129 | gist.last_modified must be equalTo 1271211315
130 | gist.description must beNull
131 | }
132 | }
133 |
134 | "An invalid gist" should {
135 | "should return none" in {
136 | Gist("""invalid""") must beNone
137 | }
138 | }
139 | }
--------------------------------------------------------------------------------
/src/main/scala/com/zegoggles/gist/Login.scala:
--------------------------------------------------------------------------------
1 | package com.zegoggles.gist
2 |
3 | import android.graphics.Bitmap
4 | import android.accounts.{Account, AccountManager, AccountAuthenticatorActivity}
5 | import android.webkit._
6 | import java.lang.String
7 | import android.os.{Handler, Bundle}
8 | import actors.Futures
9 | import Implicits._
10 | import android.webkit.WebSettings.ZoomDensity
11 | import android.app.{AlertDialog, ProgressDialog}
12 | import android.net.Uri
13 | import android.text.TextUtils
14 | import org.apache.http.HttpStatus
15 | import java.io.IOException
16 | import android.widget.Toast
17 |
18 | class Login extends AccountAuthenticatorActivity with Logger with ApiActivity with TypedActivity {
19 | val handler = new Handler()
20 | lazy val view = findView(TR.webview)
21 |
22 | override def onCreate(savedInstanceState: Bundle) {
23 | super.onCreate(savedInstanceState)
24 | setContentView(R.layout.login)
25 |
26 | view.getSettings.setDefaultZoom(ZoomDensity.FAR)
27 | view.getSettings.setJavaScriptEnabled(true)
28 | view.getSettings.setBlockNetworkImage(false)
29 | view.getSettings.setLoadsImagesAutomatically(true)
30 |
31 | val progress = ProgressDialog.show(this, null, getString(R.string.loading_login), false)
32 | view.setWebViewClient(new WebViewClient() {
33 |
34 | override def onPageStarted(view: WebView, url: String, favicon: Bitmap) {
35 | super.onPageStarted(view, url, favicon)
36 | progress.show()
37 | if (android.os.Build.VERSION.SDK_INT <= 7) {
38 | // in 2.1 shouldOverrideUrlLoading doesn't get called
39 | if (shouldOverrideUrlLoading(view, url)) {
40 | view.stopLoading()
41 | }
42 | }
43 | }
44 |
45 | override def shouldOverrideUrlLoading(view: WebView, url: String) = {
46 | super.shouldOverrideUrlLoading(view, url)
47 | if (url.startsWith(api.redirect_uri)) {
48 | Uri.parse(url).getQueryParameter("code") match {
49 | case code:String => exchangeToken(code)
50 | case _ => warn("no code found in redirect uri")
51 | }
52 | true
53 | } else false
54 | }
55 |
56 | override def onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) {
57 | super.onReceivedError(view, errorCode, description, failingUrl)
58 | showConnectionError(if (TextUtils.isEmpty(description)) None else Some(description))
59 | }
60 |
61 |
62 | override def onPageFinished(view: WebView, url: String) {
63 | super.onPageFinished(view, url)
64 | view.loadUrl("javascript:document.getElementById('login_field').focus();")
65 | try { progress.dismiss() } catch { case e:IllegalArgumentException => warn("", e) }
66 | }
67 | })
68 |
69 | if (isConnected) {
70 | removeAllCookies()
71 | view.loadUrl(api.authorizeUrl)
72 | } else showConnectionError(None)
73 | }
74 |
75 | def exchangeToken(code: String) {
76 | val progress = ProgressDialog.show(this, null, getString(R.string.loading_token), false)
77 | Futures.future {
78 | try {
79 | val token = api.exchangeToken(code).getOrElse(throw new IOException("could not get token"))
80 | val resp = api.get(Request("/user", "access_token"->token.access))
81 | resp.getStatusLine.getStatusCode match {
82 | case HttpStatus.SC_OK =>
83 | User(resp.getEntity).map { user =>
84 | handler.post {
85 | setAccountAuthenticatorResult(
86 | addAccount(user.login, token,
87 | "id" -> user.id.toString,
88 | "name" -> user.name,
89 | "email" -> user.email))
90 | }
91 | }
92 | case c =>
93 | log("invalid status ("+c+") "+resp.getStatusLine)
94 | Toast.makeText(this, R.string.loading_token_failed, Toast.LENGTH_LONG).show()
95 | }
96 | }
97 | catch {
98 | case e:IOException => warn("error", e)
99 | Toast.makeText(this, R.string.loading_token_failed, Toast.LENGTH_LONG).show()
100 | }
101 | finally { handler.post { progress.dismiss(); finish() } }
102 | }
103 | }
104 |
105 | def addAccount(name: String, token: Token, data: (String, String)*): Bundle = {
106 | val account = new Account(name, accountType)
107 | val am = AccountManager.get(this)
108 | am.addAccountExplicitly(account, token.access, null)
109 | am.setAuthToken(account, "access", token.access)
110 | for ((k, v) <- data) am.setUserData(account, k, v)
111 | val b = new Bundle()
112 | b.putString(AccountManager.KEY_ACCOUNT_NAME, name)
113 | b.putString(AccountManager.KEY_ACCOUNT_TYPE, accountType)
114 | b
115 | }
116 |
117 | def showConnectionError(message: Option[String]) {
118 | var error = getString(R.string.login_error_no_connection_message)
119 | message.map(m => error += " (" + m + ")")
120 | new AlertDialog.Builder(this)
121 | .setMessage(error)
122 | .setIcon(android.R.drawable.ic_dialog_alert)
123 | .setPositiveButton(android.R.string.ok, () => finish())
124 | .show()
125 | }
126 |
127 | def removeAllCookies() {
128 | CookieSyncManager.createInstance(this)
129 | CookieManager.getInstance().removeAllCookie()
130 | }
131 |
132 | override def onDestroy() {
133 | view.stopLoading()
134 | super.onDestroy()
135 | }
136 | }
137 |
138 |
--------------------------------------------------------------------------------
/src/main/scala/com/zegoggles/gist/GistList.scala:
--------------------------------------------------------------------------------
1 | package com.zegoggles.gist
2 |
3 | import android.os.Bundle
4 | import org.apache.http.HttpStatus
5 | import com.zegoggles.gist.Implicits._
6 | import android.app.{Activity, ProgressDialog, ListActivity}
7 | import android.content.{Intent, Context}
8 | import android.text.Html
9 | import actors.Futures
10 | import java.io.IOException
11 | import scala.Left
12 | import android.widget._
13 | import android.view._
14 |
15 | class GistList extends ListActivity with ApiActivity with Logger {
16 | override def onCreate(bundle: Bundle) {
17 | super.onCreate(bundle)
18 |
19 | if (getLastNonConfigurationInstance != null) {
20 | setListAdapter(getLastNonConfigurationInstance.asInstanceOf[ListAdapter])
21 | } else {
22 | setListAdapter(new GistAdapter())
23 |
24 | app.preloadedList match {
25 | case Some(gists) => getListAdapter.setGists(gists)
26 | case None => loadGists(pathFromIntent(getIntent))
27 | }
28 | }
29 | }
30 |
31 | def loadGist(gist: Gist)(whenDone: Either[IOException,String] => Unit) = Futures.future {
32 | val result = try {
33 | Right(gist.load)
34 | } catch {
35 | case e: IOException => Left(e)
36 | }
37 | runOnUiThread(whenDone(result))
38 | }
39 |
40 | def loadGists(path: String) {
41 | val pd = ProgressDialog.show(this, null, getString(R.string.loading_gists), true)
42 | executeAsync(api.get(_),
43 | Request(path),
44 | HttpStatus.SC_OK, Some(pd))(resp => Gist.list(resp.getEntity).map(onGistLoaded(_))) {
45 | error => error match {
46 | case Left(e) => warn("error getting gists", e)
47 | case Right(r) => warn("unexpected status code: "+r.getStatusLine)
48 | }
49 | Toast.makeText(this, R.string.list_failed, Toast.LENGTH_LONG).show()
50 | finish()
51 | }
52 | }
53 |
54 | def onGistLoaded(g: Seq[Gist]) {
55 | app.preloadedList = Some(g)
56 | getListAdapter.setGists(g)
57 | }
58 |
59 | override def onListItemClick(l: ListView, v: View, position: Int, id: Long) {
60 | val gist = getListAdapter.getItem(position)
61 | val extras = gist.asBundle
62 |
63 | def done() {
64 | setResult(Activity.RESULT_OK, new Intent().putExtras(extras).setData(gist.uri))
65 | finish()
66 | }
67 |
68 | if (shouldLoadBody(getIntent)) {
69 | val progress = ProgressDialog.show(this, null, getString(R.string.loading_gist), true)
70 | loadGist(gist) { r =>
71 | progress.dismiss()
72 | r match {
73 | case Right(body) => extras.putString("body", body)
74 | case Left(exception) =>
75 | warn("error fetching content", exception)
76 | Toast.makeText(this,
77 | getString(R.string.loading_gist_failed, exception.getMessage),
78 | Toast.LENGTH_SHORT).show()
79 | }
80 | done()
81 | }
82 | } else done()
83 | }
84 |
85 | override def onCreateOptionsMenu(menu: Menu) = {
86 | menu.add(Menu.NONE, 1, Menu.NONE, R.string.refresh).setIcon(android.R.drawable.ic_menu_rotate)
87 | super.onCreateOptionsMenu(menu)
88 | }
89 |
90 | override def onOptionsItemSelected(item: MenuItem) = item.getItemId match {
91 | case 1 => loadGists(pathFromIntent(getIntent)); true
92 | case _ => false
93 | }
94 |
95 | def pathFromIntent(intent: Intent): String =
96 | if (intent.getData != null) intent.getData.getPath else "/gists"
97 |
98 | def shouldLoadBody(intent: Intent) = intent.getBooleanExtra("load_gist", true)
99 |
100 | override def onRetainNonConfigurationInstance() = getListAdapter
101 | override def getListAdapter:GistAdapter = super.getListAdapter.asInstanceOf[GistAdapter]
102 | }
103 |
104 | class GistAdapter extends BaseAdapter {
105 | var gists: IndexedSeq[Gist] = Vector.empty
106 |
107 | def findView[T](v: View, tr: TypedResource[T]) = v.findViewById(tr.id).asInstanceOf[T]
108 | def getView(position: Int, convertView: View, parent: ViewGroup) = {
109 | val view = if (convertView == null)
110 | parent.getContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE)
111 | .asInstanceOf[LayoutInflater].inflate(R.layout.gist_row, parent, false)
112 | else convertView
113 |
114 | val gist = getItem(position)
115 | val description = findView(view, TR.gist_description)
116 | description.setText(Html.fromHtml(gist.asHtml.toString()))
117 | view.setBackgroundColor(view.getResources.getColor(gist.color))
118 | view
119 | }
120 |
121 | def getItemId(position: Int) = getItem(position).id.hashCode()
122 | def getItem(position: Int):Gist = gists(position)
123 | def getCount = gists.size
124 | override def hasStableIds = true
125 |
126 | def setGists(l: Traversable[Gist]) {
127 | gists = Vector[Gist](l.toSeq:_*)
128 | notifyDataSetChanged()
129 | }
130 | }
131 |
132 | package examples {
133 | // how to start from a console:
134 | // am start -a android.intent.action.MAIN -n com.zegoggles.gist/.examples.Pick
135 | class Pick extends Activity {
136 | override def onCreate(bundle: Bundle) {
137 | super.onCreate(bundle)
138 |
139 | // this is how you would call the upload activity from another app
140 | startActivityForResult(new Intent(Intents.PICK_GIST), 0)
141 |
142 | // here's how you would fetch only starred gists
143 | //startActivityForResult(new Intent(Intents.PICK_GIST)
144 | // .setData(Uri.parse("gist://github.com/gists/starred")), 0)
145 | }
146 |
147 | override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
148 | if (resultCode == Activity.RESULT_OK && data.getData != null) {
149 | Toast.makeText(this, "Picked "+data.getData,Toast.LENGTH_SHORT).show()
150 | } else {
151 | Toast.makeText(this, "Canceled",Toast.LENGTH_SHORT).show()
152 | }
153 | }
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/src/main/scala/com/zegoggles/gist/Api.scala:
--------------------------------------------------------------------------------
1 | package com.zegoggles.gist
2 |
3 | import org.apache.http.impl.client.DefaultHttpClient
4 | import org.apache.http.client.methods._
5 | import collection.mutable.ListBuffer
6 | import org.apache.http.client.utils.URLEncodedUtils
7 | import java.net.URI
8 | import Implicits._
9 | import android.accounts.{AccountManager, Account}
10 | import java.lang.Boolean
11 | import android.net.http.AndroidHttpClient
12 | import org.apache.http.client.HttpClient
13 | import android.content.Context
14 | import android.content.pm.PackageManager
15 | import org.apache.http.params.HttpConnectionParams
16 | import org.json.JSONObject
17 | import org.apache.http.message.{BasicHeader, BasicNameValuePair}
18 | import java.io.IOException
19 | import actors.Futures
20 | import android.app.{Dialog, Activity}
21 | import android.net.ConnectivityManager
22 | import org.apache.http._
23 | import entity.{BufferedHttpEntity, StringEntity}
24 |
25 | object Request {
26 | def apply(s: String, p: (String, String)*) = {
27 | val request = new Request(s)
28 | for ((k, v) <- p) request.add(k, v)
29 | request
30 | }
31 | implicit def string2Request(s: String): Request = new Request(s)
32 | }
33 |
34 | class Request(val url: String) {
35 | import collection.JavaConversions._
36 |
37 | lazy val params: ListBuffer[NameValuePair] = ListBuffer()
38 | var body: Option[String] = None
39 |
40 | def add(key: String, value: Any): Request = {
41 | params += new BasicNameValuePair(key, value.toString)
42 | this
43 | }
44 |
45 | def add(params: Map[String, Any]): Request = {
46 | for ((k, v) <- params) add(k, v)
47 | this
48 | }
49 |
50 | def body(b: String): Request = { body = Some(b); this }
51 | def body(j: JSONObject):Request = body(j.toString)
52 | def queryString = URLEncodedUtils.format(params, "UTF-8")
53 |
54 | def toURI = URI.create(url + (if (params.isEmpty) "" else "?" + queryString))
55 |
56 | def toHTTPRequest[T <: HttpRequestBase](r: Class[T]): T = {
57 | val req = r.newInstance
58 | req.setURI(req match {
59 | case e: HttpEntityEnclosingRequestBase =>
60 | e.setEntity(new StringEntity(body.getOrElse {
61 | e.setHeader("Content-Type", "application/x-www-form-urlencoded")
62 | queryString
63 | }))
64 | URI.create(url)
65 | case _ => toURI
66 | })
67 | req
68 | }
69 | }
70 |
71 | case class Token(access: String)
72 | object Api {
73 | type Success = HttpResponse
74 | type Error = Either[Exception,HttpResponse]
75 |
76 | def map2Json(m: Map[String, Any]): JSONObject = {
77 | val obj: JSONObject = new JSONObject()
78 | for ((k, v) <- m) {
79 | obj.put(k, v match {
80 | case m: Map[String,Any] => map2Json(m)
81 | case b: Boolean => b
82 | case i: Int => i
83 | case l: Long => l
84 | case d: Double => d
85 | case o: Any => o
86 | })
87 | }
88 | obj
89 | }
90 |
91 | def parseTokenResponse(s: String): Option[Token] = {
92 | //"access_token=807e750b891b3fc47b0c951b4c11c0b610195b73&token_type=bearer"
93 | val token = for (
94 | fields <- s.split('&').map(_.split('='))
95 | if (fields(0) == "access_token")
96 | ) yield (fields(1))
97 | token.headOption.map(Token(_))
98 | }
99 | }
100 |
101 | class Api(val client_id: String, val client_secret: String, val redirect_uri: String, var token: Option[Token]) extends StdoutLogger {
102 | lazy val client = makeClient
103 | val baseHost = new HttpHost("api.github.com", -1, "https")
104 | val authorizeUrl = "https://github.com/login/oauth/authorize?client_id=" +
105 | client_id + "&scope=user,gist&redirect_uri=" + redirect_uri
106 |
107 | def get(req: Request) = execute(req, classOf[HttpGet])
108 | def put(req: Request) = execute(req, classOf[HttpPut])
109 | def post(req: Request) = execute(req, classOf[HttpPost])
110 | def patch(req: Request) = execute(req, classOf[HttpPatch])
111 |
112 | def execute[T <: HttpRequestBase](request: Request, reqType: Class[T]) = (hostFor(request) match {
113 | case Some(host) => client.execute(host, _:HttpUriRequest)
114 | case None => client.execute(_:HttpUriRequest)
115 | })(withAuthHeader(request.toHTTPRequest(reqType)))
116 |
117 | def hostFor(r: Request):Option[HttpHost] =
118 | if (r.url.startsWith("http://")||r.url.startsWith("https://")) None else Some(baseHost)
119 |
120 | def exchangeToken(code: String): Option[Token] = {
121 | val resp = post(Request("https://github.com/login/oauth/access_token",
122 | "client_id" -> client_id,
123 | "client_secret" -> client_secret,
124 | "code" -> code,
125 | "redirect_uri" -> redirect_uri))
126 |
127 | resp.getStatusLine.getStatusCode match {
128 | case HttpStatus.SC_OK =>
129 | token = Api.parseTokenResponse(resp.getEntity)
130 | token
131 | case _ => log("Invalid status code " + resp.getStatusLine); None
132 | }
133 | }
134 |
135 | def withAuthHeader(req:HttpUriRequest) = {
136 | token.map(t => req.addHeader(new BasicHeader("Authorization", "token "+t.access)))
137 | req
138 | }
139 | def makeClient:HttpClient = new DefaultHttpClient()
140 | }
141 |
142 | trait ApiActivity extends Activity with TokenHolder {
143 | def app = getApplication.asInstanceOf[App]
144 | def api = app.api
145 |
146 | def executeAsync(call: Request => HttpResponse, req: Request, expected: Int, progress: Option[Dialog])
147 | (success: HttpResponse => Any)
148 | (error: Api.Error => Any) = {
149 |
150 | def onUiThread(f: => Unit) {
151 | runOnUiThread(new Runnable() { def run() { progress.map(_.dismiss()); f } } )
152 | }
153 | progress.map(_.show())
154 | Futures.future {
155 | try {
156 | val resp = call(req)
157 | resp.setEntity(new BufferedHttpEntity(resp.getEntity))
158 | resp.getEntity.consumeContent()
159 |
160 | resp.getStatusLine.getStatusCode match {
161 | case code if code == expected => onUiThread { success(resp)}
162 | case other => onUiThread { error(Right(resp))}
163 | }
164 | } catch {
165 | case e: IOException => onUiThread { error(Left(e)) }
166 | }
167 | }
168 | }
169 |
170 | def isConnected = {
171 | val manager = getSystemService(Context.CONNECTIVITY_SERVICE).asInstanceOf[ConnectivityManager]
172 | val info = manager.getActiveNetworkInfo
173 | info != null && info.isConnectedOrConnecting
174 | }
175 | }
176 |
177 | trait TokenHolder extends Context {
178 | lazy val accountType = getString(R.string.account_type)
179 | def account: Option[Account] = AccountManager.get(this).getAccountsByType(accountType).headOption
180 | def token: Option[Token] = account.map(a => Token(AccountManager.get(this).getPassword(a)))
181 |
182 | def addAccount(a:Activity) =
183 | AccountManager.get(this).addAccount(accountType, "access_token", null, null, a, null, null)
184 | }
185 |
186 | trait ApiHolder extends TokenHolder {
187 | lazy val info = getPackageManager.getPackageInfo(getClass.getPackage.getName, PackageManager.GET_META_DATA)
188 | lazy val userAgent = getPackageManager.getApplicationLabel(info.applicationInfo)+" ("+info.versionName+")"
189 | val timeout = 10 * 1000
190 |
191 | lazy val api = new Api(
192 | getString(R.string.client_id),
193 | getString(R.string.client_secret),
194 | getString(R.string.redirect_uri),
195 | token) {
196 | override def makeClient = {
197 | if (android.os.Build.VERSION.SDK_INT >= 8) {
198 | val client = AndroidHttpClient.newInstance(userAgent, ApiHolder.this)
199 | HttpConnectionParams.setConnectionTimeout(client.getParams, timeout)
200 | HttpConnectionParams.setSoTimeout(client.getParams, timeout)
201 | client
202 | } else super.makeClient
203 | }
204 | }
205 | }
206 |
207 | /** @see PATCH Method for HTTP: http://tools.ietf.org/html/rfc5789 */
208 | class HttpPatch extends HttpEntityEnclosingRequestBase {
209 | def getMethod = "PATCH"
210 | }
211 |
--------------------------------------------------------------------------------
/src/main/res/layout/upload_gist.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
13 |
14 |
17 |
18 |
25 |
26 |
34 |
35 |
43 |
44 |
53 |
54 |
55 |
60 |
61 |
62 |
63 |
78 |
79 |
94 |
95 |
96 |
112 |
113 |
121 |
122 |
132 |
133 |
143 |
144 |
145 |
146 |
147 |
152 |
158 |
159 |
167 |
168 |
169 |
172 |
173 |
174 |
--------------------------------------------------------------------------------
/src/main/scala/com/zegoggles/gist/UploadGist.scala:
--------------------------------------------------------------------------------
1 | package com.zegoggles.gist
2 | import Implicits._
3 |
4 | import android.widget.Toast
5 | import android.net.Uri
6 | import android.text.{TextUtils, ClipboardManager}
7 | import android.view.{MenuItem, Menu, KeyEvent, View}
8 | import org.apache.http.HttpStatus
9 | import android.view.inputmethod.{InputMethodManager, EditorInfo}
10 | import android.app.{Activity, AlertDialog, ProgressDialog}
11 | import android.os.{BatteryManager, Bundle}
12 | import android.content.{IntentFilter, BroadcastReceiver, Intent, Context}
13 | import java.lang.Boolean
14 |
15 | class UploadGist extends Activity
16 | with Logger with ApiActivity with TypedActivity with BatteryAware {
17 | lazy val (public, filename, description, content) =
18 | (findView(TR.public_gist), findView(TR.filename), findView(TR.description), findView(TR.content))
19 |
20 | var previousGist:Option[Bundle] = None
21 |
22 | override def onCreate(bundle: Bundle) {
23 | super.onCreate(bundle)
24 | setContentView(R.layout.upload_gist)
25 | setBackground(findView(TR.upload_root), R.drawable.octocat_bg, 15)
26 |
27 | content.requestFocus()
28 | content.setText(textFromIntent(getIntent))
29 | content.setOnEditorActionListener((v: View, id: Int, e: KeyEvent) =>
30 | if (id == EditorInfo.IME_ACTION_DONE)
31 | findViewById(R.id.upload_btn).performClick()
32 | else false
33 | )
34 |
35 | if (getIntent.getAction == Intents.UPLOAD_GIST) {
36 | // prefill some fields if we got called from our own intent
37 | public.setChecked(getIntent.getBooleanExtra(Extras.PUBLIC, true))
38 | description.setText(getIntent.getStringExtra(Extras.DESCRIPTION))
39 | }
40 |
41 | findView(TR.upload_btn).setOnClickListener { v: View =>
42 | hideSoftKeyboard(content)
43 | val doUpload = () => upload(previousGist, public.isChecked, filename, description, content)
44 | if (!TextUtils.isEmpty(content))
45 | doUpload()
46 | else
47 | new AlertDialog.Builder(this)
48 | .setTitle(R.string.empty_gist_title)
49 | .setMessage(R.string.empty_gist)
50 | .setIcon(android.R.drawable.ic_dialog_alert)
51 | .setPositiveButton(android.R.string.ok, doUpload)
52 | .setNegativeButton(android.R.string.cancel, ()=>())
53 | .show()
54 | }
55 |
56 | findView(TR.anon).setText(getString(R.string.anon_upload, getString(R.string.set_up_an_account)))
57 | Utils.clickify(findView(TR.anon), getString(R.string.set_up_an_account), addAccount(this))
58 |
59 | preloadGists()
60 | }
61 |
62 | override def onResume() {
63 | super.onResume()
64 | findView(TR.anon).setVisibility(if (account.isDefined) View.GONE else View.VISIBLE)
65 | }
66 |
67 | override def onSaveInstanceState(outState: Bundle) {
68 | super.onSaveInstanceState(outState)
69 | previousGist.map(b => outState.putBundle(UploadGist.ExtraPreviousGist, b))
70 | }
71 |
72 | override def onRestoreInstanceState(state: Bundle) {
73 | super.onRestoreInstanceState(state)
74 | previousGist = if (state.getBundle(UploadGist.ExtraPreviousGist) != null)
75 | Some(state.getBundle(UploadGist.ExtraPreviousGist))
76 | else
77 | None
78 | }
79 |
80 | def upload(previous: Option[Bundle], public: Boolean, filename:String, description: String, content: String) = {
81 | val name = if (TextUtils.isEmpty(filename)) UploadGist.DefaultFileName else filename
82 | val params = Map(
83 | "description" -> description,
84 | "public" -> public,
85 | "files" -> Map(name -> Map("content" -> content)))
86 |
87 | val progress = ProgressDialog.show(this, null, getString(R.string.uploading), true)
88 | if (previous.isEmpty) {
89 | executeAsync(
90 | api.post(_),
91 | Request("/gists").body(params),
92 | HttpStatus.SC_CREATED, Some(progress))(onSuccess)(onError)
93 | } else {
94 | executeAsync(
95 | api.patch(_),
96 | Request("/gists/"+previous.get.getString("id")).body(params),
97 | HttpStatus.SC_OK, Some(progress))(onSuccess)(onError)
98 | }
99 | }
100 |
101 | def onSuccess(r: Api.Success) {
102 | Gist(r.getEntity).map { g =>
103 | Toast.makeText(this, R.string.gist_uploaded, Toast.LENGTH_SHORT).show()
104 | if (launchedViaIntent) {
105 | setResult(Activity.RESULT_OK, new Intent()
106 | .setData(g.public_uri)
107 | .putExtra("location", g.url)
108 | .putExtra("url", g.public_url))
109 | finish()
110 | }
111 | copyToClipboard(g.public_url)
112 | }
113 | }
114 |
115 | def onError(error: Api.Error) {
116 | error match {
117 | case Left(exception) => warn("error", exception)
118 | case Right(resp) => warn("unexpected status code: " + resp.getStatusLine)
119 | }
120 | Toast.makeText(this, R.string.uploading_failed, Toast.LENGTH_LONG).show()
121 | }
122 |
123 | def preloadGists() {
124 | // load gists eagerly if there's battery+connection
125 | if (isConnected && hasBattery)
126 | executeAsync(api.get(_),
127 | Request("/gists"),
128 | HttpStatus.SC_OK, None)(resp => app.preloadedList = Gist.list(resp.getEntity))(error => ())
129 | }
130 |
131 | def textFromIntent(intent: Intent):String = {
132 | if (intent == null)
133 | ""
134 | else if (intent.hasExtra(Intent.EXTRA_TEXT))
135 | intent.getStringExtra(Intent.EXTRA_TEXT)
136 | else if (intent.hasExtra(Intent.EXTRA_STREAM)) {
137 | val uri:Uri = intent getParcelableExtra(Intent.EXTRA_STREAM)
138 | try {
139 | io.Source.fromInputStream(
140 | getContentResolver.openAssetFileDescriptor(uri, "r").createInputStream())
141 | .getLines().mkString
142 | } catch {
143 | case e:SecurityException =>
144 | Toast.makeText(this,
145 | getString(R.string.security_exception), Toast.LENGTH_LONG).show()
146 | ""
147 | }
148 | } else ""
149 | }
150 |
151 | def copyToClipboard(c: CharSequence) {
152 | getSystemService(Context.CLIPBOARD_SERVICE)
153 | .asInstanceOf[ClipboardManager]
154 | .setText(c)
155 | }
156 |
157 | override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
158 | if (resultCode == Activity.RESULT_OK) {
159 | val body = data.getStringExtra("body")
160 | if (!TextUtils.isEmpty(body)) {
161 | copyToClipboard(body)
162 | Toast.makeText(this, R.string.gist_clipboard, Toast.LENGTH_SHORT).show()
163 | }
164 | description.setText(data.getStringExtra(Extras.DESCRIPTION))
165 | filename.setText(data.getStringExtra(Extras.FILENAME))
166 | public.setChecked(data.getBooleanExtra(Extras.PUBLIC, true))
167 | content.setText(body)
168 | previousGist = Some(data.getExtras)
169 | }
170 | }
171 |
172 | override def onCreateOptionsMenu(menu: Menu) = {
173 | getMenuInflater.inflate(R.menu.menu, menu)
174 | true
175 | }
176 |
177 | override def onOptionsItemSelected(item: MenuItem) = {
178 | item.getItemId match {
179 | case R.id.menu_fork_me => forkMe(); true
180 | case R.id.menu_load_gist => startLoadGist(); true
181 | case R.id.menu_clear => clear(); true
182 | case _ => false
183 | }
184 | }
185 |
186 | def forkMe() {
187 | startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.github_url))))
188 | }
189 |
190 | def clear() {
191 | List(content, filename, description).foreach(_.setText(""))
192 | public.setChecked(true)
193 | previousGist = None
194 | }
195 |
196 | def startLoadGist() {
197 | startActivityForResult(new Intent(Intents.PICK_GIST), 0)
198 | }
199 |
200 | def setBackground(v: View, resId: Int, alpha: Int) {
201 | val bg = getResources.getDrawable(resId)
202 | bg.setAlpha(alpha)
203 | v.setBackgroundDrawable(bg)
204 | }
205 |
206 | def launchedViaIntent = getIntent != null && getIntent.getAction != Intent.ACTION_MAIN
207 | def hideSoftKeyboard(v:View) =
208 | getSystemService(Context.INPUT_METHOD_SERVICE).asInstanceOf[InputMethodManager]
209 | .hideSoftInputFromWindow(v.getWindowToken, 0)
210 | }
211 |
212 | object UploadGist {
213 | val ExtraPreviousGist = "previousGist"
214 | val DefaultFileName = "gistfile1.txt"
215 | }
216 |
217 | object Extras {
218 | val DESCRIPTION = "description"
219 | val PUBLIC = "public"
220 | val FILENAME = "filename"
221 | }
222 |
223 | object Intents {
224 | val PICK_GIST = "com.zegoggles.gist.PICK"
225 | val UPLOAD_GIST = "com.zegoggles.gist.UPLOAD"
226 | }
227 |
228 |
229 | trait BatteryAware extends Activity {
230 | case class BatteryInfo(var level:Int, var scale:Int, var voltage:Int, var temp:Int) {}
231 | lazy val batteryStatus = new BatteryInfo(-1,-1,-1,-1)
232 | val receiver = new BroadcastReceiver() {
233 | override def onReceive(context:Context, intent:Intent) {
234 | batteryStatus.level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
235 | batteryStatus.scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
236 | batteryStatus.temp = intent.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, -1)
237 | batteryStatus.voltage = intent.getIntExtra(BatteryManager.EXTRA_VOLTAGE, -1)
238 | }
239 | }
240 | def hasBattery = batteryStatus.level / batteryStatus.scale.toFloat > 0.7f
241 | override def onResume() {
242 | super.onResume()
243 | registerReceiver(receiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED))
244 | }
245 |
246 | override def onPause() {
247 | super.onPause()
248 | unregisterReceiver(receiver)
249 | }
250 | }
251 |
252 | package examples {
253 | // how to start from a console:
254 | // am start -a android.intent.action.MAIN -n com.zegoggles.gist/.examples.Upload
255 | class Upload extends Activity {
256 | override def onCreate(bundle: Bundle) {
257 | super.onCreate(bundle)
258 |
259 | // this is how you would call the upload activity from another app
260 | startActivityForResult(new Intent(Intents.UPLOAD_GIST)
261 | .putExtra(Intent.EXTRA_TEXT, "text123")
262 | .putExtra(Extras.PUBLIC, false)
263 | .putExtra(Extras.DESCRIPTION, "testing gist upload via intent"), 0)
264 | }
265 |
266 | override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
267 | if (resultCode == Activity.RESULT_OK && data.getData != null) {
268 | Toast.makeText(this, "Uploaded to "+data.getData,Toast.LENGTH_SHORT).show()
269 | } else {
270 | Toast.makeText(this, "Canceled",Toast.LENGTH_SHORT).show()
271 | }
272 | }
273 | }
274 | }
275 |
--------------------------------------------------------------------------------