diff --git a/src/main/res/menu/app_manager_menu.xml b/src/main/res/menu/app_manager_menu.xml
index f6a20e01..ef91b2ce 100644
--- a/src/main/res/menu/app_manager_menu.xml
+++ b/src/main/res/menu/app_manager_menu.xml
@@ -1,8 +1,12 @@
diff --git a/src/main/res/menu/profile_manager_menu.xml b/src/main/res/menu/profile_manager_menu.xml
index 6d080c8f..a5f305df 100644
--- a/src/main/res/menu/profile_manager_menu.xml
+++ b/src/main/res/menu/profile_manager_menu.xml
@@ -15,6 +15,14 @@
android:title="@string/add_profile_methods_manual_settings"
android:alphabeticShortcut="m"
android:numericShortcut="2"/>
+
+
diff --git a/src/main/res/values-zh/strings.xml b/src/main/res/values-zh/strings.xml
index bd08a6ef..01274188 100755
--- a/src/main/res/values-zh/strings.xml
+++ b/src/main/res/values-zh/strings.xml
@@ -90,6 +90,7 @@
• 点击选择配置文件;\n• 滑动删除;\n•
长按拖动改变顺序。
好的
+ 复制 URL
为影梭添加此配置文件?
扫描二维码
手动设置
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index 1c6f63e8..57d5a710 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -113,6 +113,7 @@
Add this Shadowsocks Profile?
Scan QR code
Manual Settings
+ Copy URL
- Removed
- %d items removed
diff --git a/src/main/scala/com/github/shadowsocks/AppManager.scala b/src/main/scala/com/github/shadowsocks/AppManager.scala
index 9f0d9bba..85427dd4 100644
--- a/src/main/scala/com/github/shadowsocks/AppManager.scala
+++ b/src/main/scala/com/github/shadowsocks/AppManager.scala
@@ -154,26 +154,22 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC
return true
case R.id.action_import =>
if (clipboard.hasPrimaryClip) {
- val clipdata = clipboard.getPrimaryClip
- val label = clipdata.getDescription.getLabel
- if (label == Key.proxied) {
- val proxiedAppSequence = clipdata.getItemAt(0).getText
- if (proxiedAppSequence != null) {
- val proxiedAppString = proxiedAppSequence.toString
- if (!proxiedAppString.isEmpty) {
- val editor = prefs.edit
- val i = proxiedAppString.indexOf('\n')
- if (i < 0)
- editor.putBoolean(Key.isBypassApps, proxiedAppString.toBoolean).putString(Key.proxied, "").apply()
- else editor.putBoolean(Key.isBypassApps, proxiedAppString.substring(0, i).toBoolean)
- .putString(Key.proxied, proxiedAppString.substring(i + 1)).apply()
- Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
- // Restart activity
- appListView.setVisibility(View.GONE)
- loadingView.setVisibility(View.VISIBLE)
- reloadApps()
- return true
- }
+ val proxiedAppSequence = clipboard.getPrimaryClip.getItemAt(0).getText
+ if (proxiedAppSequence != null) {
+ val proxiedAppString = proxiedAppSequence.toString
+ if (!proxiedAppString.isEmpty) {
+ val editor = prefs.edit
+ val i = proxiedAppString.indexOf('\n')
+ if (i < 0)
+ editor.putBoolean(Key.isBypassApps, proxiedAppString.toBoolean).putString(Key.proxied, "").apply()
+ else editor.putBoolean(Key.isBypassApps, proxiedAppString.substring(0, i).toBoolean)
+ .putString(Key.proxied, proxiedAppString.substring(i + 1)).apply()
+ Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
+ // Restart activity
+ appListView.setVisibility(View.GONE)
+ loadingView.setVisibility(View.VISIBLE)
+ reloadApps()
+ return true
}
}
}
diff --git a/src/main/scala/com/github/shadowsocks/ProfileManagerActivity.scala b/src/main/scala/com/github/shadowsocks/ProfileManagerActivity.scala
index 4df76605..a28fe9bd 100644
--- a/src/main/scala/com/github/shadowsocks/ProfileManagerActivity.scala
+++ b/src/main/scala/com/github/shadowsocks/ProfileManagerActivity.scala
@@ -1,6 +1,6 @@
package com.github.shadowsocks
-import android.content.Intent
+import android.content._
import android.os.{Bundle, Handler}
import android.support.v7.app.{AlertDialog, AppCompatActivity}
import android.support.v7.widget.RecyclerView.ViewHolder
@@ -32,17 +32,20 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
{
val qrcode = itemView.findViewById(R.id.qrcode)
- qrcode.setOnClickListener((v: View) => {
+ qrcode.setOnClickListener(_ => {
+ val url = Parser.generate(item)
val image = new ImageView(ProfileManagerActivity.this)
image.setLayoutParams(new LinearLayout.LayoutParams(-1, -1))
- val qrcode = QRCode.from(Parser.generate(item))
+ val qrcode = QRCode.from(url)
.withSize(Utils.dpToPx(ProfileManagerActivity.this, 250), Utils.dpToPx(ProfileManagerActivity.this, 250))
.asInstanceOf[QRCode].bitmap()
image.setImageBitmap(qrcode)
new AlertDialog.Builder(ProfileManagerActivity.this)
.setCancelable(true)
- .setNegativeButton(R.string.close, null)
+ .setPositiveButton(R.string.close, null)
+ .setNegativeButton(R.string.copy_url, ((_, _) =>
+ clipboard.setPrimaryClip(ClipData.newPlainText(null, url))): DialogInterface.OnClickListener)
.setView(image)
.create()
.show()
@@ -144,6 +147,8 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
private lazy val profilesAdapter = new ProfilesAdapter
private var undoManager: UndoSnackbarManager[Profile] = _
+ private lazy val clipboard = getSystemService(Context.CLIPBOARD_SERVICE).asInstanceOf[ClipboardManager]
+
override def onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_profiles)
@@ -216,7 +221,7 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
Parser.findAll(scanResult.getContents).foreach(ShadowsocksApplication.profileManager.createProfile)
}
- def onMenuItemClick(item: MenuItem) = item.getItemId match {
+ def onMenuItemClick(item: MenuItem): Boolean = item.getItemId match {
case R.id.scan_qr_code =>
val integrator = new IntentIntegrator(this)
val list = new java.util.ArrayList(IntentIntegrator.TARGET_ALL_KNOWN)
@@ -229,6 +234,25 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
ShadowsocksApplication.switchProfile(ShadowsocksApplication.profileManager.save.id)
finish
true
+ case R.id.action_import =>
+ if (clipboard.hasPrimaryClip) {
+ val profiles = Parser.findAll(clipboard.getPrimaryClip.getItemAt(0).getText)
+ if (profiles.nonEmpty) {
+ profiles.foreach(ShadowsocksApplication.profileManager.createProfile)
+ Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show
+ return true
+ }
+ }
+ Toast.makeText(this, R.string.action_import_err, Toast.LENGTH_SHORT).show
+ true
+ case R.id.action_export =>
+ ShadowsocksApplication.profileManager.getAllProfiles match {
+ case Some(profiles) =>
+ clipboard.setPrimaryClip(ClipData.newPlainText(null, profiles.map(Parser.generate).mkString("\n")))
+ Toast.makeText(this, R.string.action_export_msg, Toast.LENGTH_SHORT).show
+ case _ => Toast.makeText(this, R.string.action_export_err, Toast.LENGTH_SHORT).show
+ }
+ true
case _ => false
}
}
diff --git a/src/main/scala/com/github/shadowsocks/utils/Parser.scala b/src/main/scala/com/github/shadowsocks/utils/Parser.scala
index 5ffc3eed..3784fc8d 100644
--- a/src/main/scala/com/github/shadowsocks/utils/Parser.scala
+++ b/src/main/scala/com/github/shadowsocks/utils/Parser.scala
@@ -46,12 +46,12 @@ import com.github.shadowsocks.database.Profile
object Parser {
val TAG = "ShadowParser"
- private val pattern = "(?i)ss://([A-Za-z0-9+/=]+)".r
+ private val pattern = "(?i)ss://([A-Za-z0-9+-/=_]+)".r
def generate (profile: Profile): String = "ss://" + Base64.encodeToString("%s:%s@%s:%d".formatLocal(Locale.ENGLISH,
- profile.method, profile.password, profile.host, profile.remotePort).getBytes, Base64.NO_PADDING)
+ profile.method, profile.password, profile.host, profile.remotePort).getBytes, Base64.NO_PADDING | Base64.NO_WRAP)
- def findAll(data: String) = pattern.findAllMatchIn(data).map(m => try {
+ def findAll(data: CharSequence) = pattern.findAllMatchIn(data).map(m => try {
val content = new String(Base64.decode(m.group(1), Base64.NO_PADDING), "UTF-8")
val methodIdx = content.indexOf(':')
val passwordIdx = content.lastIndexOf('@')