diff --git a/build.sbt b/build.sbt
index bfd02734..e632d518 100644
--- a/build.sbt
+++ b/build.sbt
@@ -28,6 +28,8 @@ libraryDependencies ++= Seq(
)
libraryDependencies ++= Seq(
+ "com.joanzapata.android" % "android-iconify" % "1.0.9",
+ "net.glxn.qrgen" % "android" % "2.0",
"net.simonvt.menudrawer" % "menudrawer" % "3.0.6",
"com.google.android.gms" % "play-services-base" % "6.5.87",
"com.google.android.gms" % "play-services-ads" % "6.5.87",
@@ -48,6 +50,7 @@ proguardOptions in Android ++= Seq("-keep class android.support.v4.app.** { *; }
"-keep class org.jraf.android.backport.** { *; }",
"-keep class com.github.shadowsocks.** { *; }",
"-keep class * extends com.j256.ormlite.** { *; }",
+ "-keep class com.joanzapata.** { *; }",
"-keepattributes *Annotation*",
"-dontwarn org.xbill.**",
"-dontwarn com.actionbarsherlock.**")
diff --git a/src/main/res/values-zh/strings.xml b/src/main/res/values-zh/strings.xml
index 47fa30b8..a465c305 100755
--- a/src/main/res/values-zh/strings.xml
+++ b/src/main/res/values-zh/strings.xml
@@ -49,6 +49,7 @@
正在关闭...
后台服务启动失败:%s
确定
+ 关闭
本地端口号应大于1024
端口号不能为空
代理服务器地址不能为空
@@ -67,6 +68,7 @@
影梭(shadowsocks)%s
刷新 DNS 缓存
刷新中...
+ 二维码
添加配置文件
启用 NAT 模式.
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index e454e9e3..c16eb855 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -65,6 +65,7 @@
OK
+ Close
The local port number should be greater than 1024
Port should not be empty
Proxy should not be empty
@@ -84,6 +85,7 @@
Reset
About
Shadowsocks %s
+ QR Code
Flush DNS Cache
Flushing
Add Profile
diff --git a/src/main/scala/com/github/shadowsocks/Shadowsocks.scala b/src/main/scala/com/github/shadowsocks/Shadowsocks.scala
index 5c5c0d74..da698610 100644
--- a/src/main/scala/com/github/shadowsocks/Shadowsocks.scala
+++ b/src/main/scala/com/github/shadowsocks/Shadowsocks.scala
@@ -41,6 +41,7 @@ package com.github.shadowsocks
import java.util
import java.io.{OutputStream, InputStream, ByteArrayInputStream, ByteArrayOutputStream, IOException, FileOutputStream}
import java.util.Locale
+import java.lang.Math
import android.app.backup.BackupManager
import android.app.{Activity, AlertDialog, ProgressDialog}
@@ -66,6 +67,10 @@ import com.google.android.gms.analytics.HitBuilders
import com.google.zxing.integration.android.IntentIntegrator
import com.nostra13.universalimageloader.core.download.BaseImageDownloader
import net.simonvt.menudrawer.MenuDrawer
+import com.joanzapata.android.iconify.Iconify
+import com.joanzapata.android.iconify.Iconify.IconValue
+import com.joanzapata.android.iconify.IconDrawable
+import net.glxn.qrgen.android.QRCode
import scala.collection.mutable.{ArrayBuffer, ListBuffer}
import scala.concurrent.ops._
@@ -711,7 +716,7 @@ class Shadowsocks
def getProfileList: List[Item] = {
val list = profileManager.getAllProfiles getOrElse List[Profile]()
- list.map(p => new Item(p.id, p.name, -1, updateProfile, delProfile))
+ list.map(p => new IconItem(p.id, p.name, -1, updateProfile, delProfile))
}
def getMenuList: List[Any] = {
@@ -723,11 +728,13 @@ class Shadowsocks
buf ++= getProfileList
buf +=
- new Item(-400, getString(R.string.add_profile), android.R.drawable.ic_menu_add, newProfile)
+ new DrawableItem(-400, getString(R.string.add_profile), new IconDrawable(this, IconValue.fa_plus_circle)
+ .colorRes(android.R.color.darker_gray).sizeDp(26), newProfile)
buf += new Category(getString(R.string.settings))
- buf += new Item(-100, getString(R.string.recovery), android.R.drawable.ic_menu_revert, _ => {
+ buf += new DrawableItem(-100, getString(R.string.recovery), new IconDrawable(this, IconValue.fa_recycle)
+ .colorRes(android.R.color.darker_gray).sizeDp(26), _ => {
// send event
application.tracker.send(new HitBuilders.EventBuilder()
.setCategory(Shadowsocks.TAG)
@@ -738,7 +745,8 @@ class Shadowsocks
})
buf +=
- new Item(-200, getString(R.string.flush_dnscache), android.R.drawable.ic_menu_delete, _ => {
+ new DrawableItem(-200, getString(R.string.flush_dnscache), new IconDrawable(this, IconValue.fa_refresh)
+ .colorRes(android.R.color.darker_gray).sizeDp(26), _ => {
// send event
application.tracker.send(new HitBuilders.EventBuilder()
.setCategory(Shadowsocks.TAG)
@@ -748,7 +756,20 @@ class Shadowsocks
flushDnsCache()
})
- buf += new Item(-300, getString(R.string.about), android.R.drawable.ic_menu_info_details, _ => {
+ buf +=
+ new DrawableItem(-300, getString(R.string.qrcode), new IconDrawable(this, IconValue.fa_qrcode)
+ .colorRes(android.R.color.darker_gray).sizeDp(26), _ => {
+ // send event
+ application.tracker.send(new HitBuilders.EventBuilder()
+ .setCategory(Shadowsocks.TAG)
+ .setAction("qrcode")
+ .setLabel(getVersionName)
+ .build())
+ showQrCode()
+ })
+
+ buf += new DrawableItem(-400, getString(R.string.about), new IconDrawable(this, IconValue.fa_info_circle)
+ .colorRes(android.R.color.darker_gray).sizeDp(26), _ => {
// send event
application.tracker.send(new HitBuilders.EventBuilder()
.setCategory(Shadowsocks.TAG)
@@ -881,6 +902,31 @@ class Shadowsocks
}
}
+ private def dp2px(dp: Int): Int = {
+ val displayMetrics = getBaseContext.getResources.getDisplayMetrics()
+ Math.round(dp * (displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT))
+ }
+
+ private def showQrCode() {
+ val image = new ImageView(this)
+ image.setLayoutParams(new LinearLayout.LayoutParams(-1, -1))
+ val qrcode = QRCode.from(Parser.generate(currentProfile))
+ .withSize(dp2px(250), dp2px(250)).asInstanceOf[QRCode]
+ image.setImageBitmap(qrcode.bitmap())
+
+ new AlertDialog.Builder(this)
+ .setTitle(getString(R.string.qrcode))
+ .setCancelable(true)
+ .setNegativeButton(getString(R.string.close), new DialogInterface.OnClickListener() {
+ override def onClick(dialog: DialogInterface, id: Int) {
+ dialog.cancel()
+ }
+ })
+ .setView(image)
+ .create()
+ .show()
+ }
+
private def flushDnsCache() {
val h = showProgress(getString(R.string.flushing))
spawn {
diff --git a/src/main/scala/com/github/shadowsocks/database/MenuAdapter.scala b/src/main/scala/com/github/shadowsocks/database/MenuAdapter.scala
index cf8f52f8..a5bda327 100644
--- a/src/main/scala/com/github/shadowsocks/database/MenuAdapter.scala
+++ b/src/main/scala/com/github/shadowsocks/database/MenuAdapter.scala
@@ -45,11 +45,24 @@ import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.TextView
+import android.graphics.drawable.Drawable
import com.github.shadowsocks.{R}
import android.view.View.{OnLongClickListener, OnClickListener}
-case class Item(id: Int, title: String, iconRes: Int,
+abstract class Item {
+ def id: Int
+ def title: String
+ def click: Int => Unit
+ def longClick: Int => Boolean
+}
+
+case class IconItem(id: Int, title: String, iconRes: Int,
click: Int => Unit, longClick: Int => Boolean = _ => false)
+ extends Item
+
+case class DrawableItem(id: Int, title: String, iconRes: Drawable,
+ click: Int => Unit, longClick: Int => Boolean = _ => false)
+ extends Item
case class Category(title: String)
@@ -89,7 +102,8 @@ class MenuAdapter(context: Context, var items: List[Any]) extends BaseAdapter {
@Override override def getItemViewType(position: Int): Int = {
getItem(position) match {
case Category => MenuAdapter.CATEGORY
- case Item => MenuAdapter.ITEM
+ case IconItem => MenuAdapter.ITEM
+ case DrawableItem => MenuAdapter.ITEM
case _ => MenuAdapter.UNDEFINED
}
}
@@ -121,9 +135,18 @@ class MenuAdapter(context: Context, var items: List[Any]) extends BaseAdapter {
}
val tv: TextView = v.asInstanceOf[TextView]
tv.setText(value.title)
- if (value.iconRes != -1) {
- tv.setCompoundDrawablesWithIntrinsicBounds(value.iconRes, 0, 0, 0)
+
+ value match {
+ case iconItem: IconItem =>
+ if (iconItem.iconRes != -1) {
+ tv.setCompoundDrawablesWithIntrinsicBounds(iconItem.iconRes, 0, 0, 0)
+ }
+ case drawableItem: DrawableItem =>
+ if (drawableItem.iconRes != null) {
+ tv.setCompoundDrawablesWithIntrinsicBounds(drawableItem.iconRes, null, null, null)
+ }
}
+
tv.setOnClickListener(new OnClickListener {
def onClick(view: View) {
val item = view.getTag(R.id.mdItem).asInstanceOf[Item]
diff --git a/src/main/scala/com/github/shadowsocks/utils/Parser.scala b/src/main/scala/com/github/shadowsocks/utils/Parser.scala
index ade339ef..6cc4ab2b 100644
--- a/src/main/scala/com/github/shadowsocks/utils/Parser.scala
+++ b/src/main/scala/com/github/shadowsocks/utils/Parser.scala
@@ -40,11 +40,19 @@
package com.github.shadowsocks.utils
import com.github.shadowsocks.database.Profile
+import java.util.Locale
import android.net.Uri
import android.util.{Log, Base64}
object Parser {
val TAG = "ShadowParser"
+
+ def generate (profile: Profile): String = {
+ val path = "%s:%s@%s:%d".formatLocal(Locale.ENGLISH,
+ profile.method, profile.password, profile.host, profile.remotePort)
+ return "ss://" + Base64.encodeToString(path.getBytes, Base64.NO_PADDING)
+ }
+
def parse (data: String): Option[Profile] = {
try {
Log.d(TAG, data)