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)