Commit 04ec29c9 authored by Max Lv's avatar Max Lv

Merge pull request #603 from Mygod/master

Import/export one/all ss urls
parents 86da4f53 16e39f8b
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:id="@+id/action_export"
android:title="@string/action_export"
android:showAsAction="never" />
<item android:id="@+id/action_import"
android:title="@string/action_import"
android:showAsAction="never" />
android:title="@string/action_import"
android:showAsAction="never"
android:alphabeticShortcut="i"
android:numericShortcut="1"/>
<item android:id="@+id/action_export"
android:title="@string/action_export"
android:showAsAction="never"
android:alphabeticShortcut="e"
android:numericShortcut="2"/>
</menu>
......@@ -15,6 +15,14 @@
android:title="@string/add_profile_methods_manual_settings"
android:alphabeticShortcut="m"
android:numericShortcut="2"/>
<item android:id="@+id/action_import"
android:title="@string/action_import"
android:alphabeticShortcut="i"
android:numericShortcut="3"/>
<item android:id="@+id/action_export"
android:title="@string/action_export"
android:alphabeticShortcut="e"
android:numericShortcut="4"/>
</menu>
</item>
</menu>
......@@ -90,6 +90,7 @@
<string name="profile_manager_dialog_content">&#8226; 点击选择配置文件;\n&#8226; 滑动删除;\n&#8226;
长按拖动改变顺序。</string>
<string name="gotcha">好的</string>
<string name="copy_url">复制 URL</string>
<string name="add_profile_dialog">为影梭添加此配置文件?</string>
<string name="add_profile_methods_scan_qr_code">扫描二维码</string>
<string name="add_profile_methods_manual_settings">手动设置</string>
......
......@@ -113,6 +113,7 @@
<string name="add_profile_dialog">Add this Shadowsocks Profile?</string>
<string name="add_profile_methods_scan_qr_code">Scan QR code</string>
<string name="add_profile_methods_manual_settings">Manual Settings</string>
<string name="copy_url">Copy URL</string>
<plurals name="removed">
<item quantity="one">Removed</item>
<item quantity="other">%d items removed</item>
......
......@@ -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
}
}
}
......
......@@ -50,9 +50,8 @@ import com.github.shadowsocks.utils.Parser
class ParserActivity extends AppCompatActivity {
override def onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
val data = getIntent.getData.toString
val profile = Parser.parse(data)
if (profile.isEmpty) { // ignore
val profiles = Parser.findAll(getIntent.getData.toString)
if (profiles.isEmpty) { // ignore
finish()
return
}
......@@ -60,11 +59,10 @@ class ParserActivity extends AppCompatActivity {
val dialog = new AlertDialog.Builder(this)
.setTitle(R.string.add_profile_dialog)
.setCancelable(false)
.setPositiveButton(android.R.string.yes, ((dialog: DialogInterface, id: Int) => {
ShadowsocksApplication.profileManager.createProfile(profile.get)
}): DialogInterface.OnClickListener)
.setPositiveButton(android.R.string.yes, ((_, _) =>
profiles.foreach(ShadowsocksApplication.profileManager.createProfile)): DialogInterface.OnClickListener)
.setNegativeButton(android.R.string.no, null)
.setMessage(data)
.setMessage(profiles.mkString("\n"))
.create()
dialog.setOnDismissListener((dialog: DialogInterface) => finish()) // builder method was added in API 17
dialog.show()
......
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
......@@ -14,7 +14,7 @@ import android.view.{LayoutInflater, MenuItem, View, ViewGroup}
import android.widget.{CheckedTextView, ImageView, LinearLayout, Toast}
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.utils.{Parser, TrafficMonitor, Utils}
import com.github.shadowsocks.utils.{Key, Parser, TrafficMonitor, Utils}
import com.github.shadowsocks.widget.UndoSnackbarManager
import com.google.zxing.integration.android.IntentIntegrator
import net.glxn.qrgen.android.QRCode
......@@ -24,13 +24,7 @@ import scala.collection.mutable.ArrayBuffer
/**
* @author Mygod
*/
object ProfileManagerActivity {
private final val profileTip = "profileTip"
}
class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListener with ServiceBoundContext {
import ProfileManagerActivity._
private class ProfileViewHolder(val view: View) extends RecyclerView.ViewHolder(view) with View.OnClickListener {
var item: Profile = _
private val text = itemView.findViewById(android.R.id.text1).asInstanceOf[CheckedTextView]
......@@ -38,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()
......@@ -150,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)
......@@ -193,8 +192,8 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
if (selectedItem != null) selectedItem.updateText(txTotal, rxTotal)
})
if (ShadowsocksApplication.settings.getBoolean(profileTip, true)) {
ShadowsocksApplication.settings.edit.putBoolean(profileTip, false).commit
if (ShadowsocksApplication.settings.getBoolean(Key.profileTip, true)) {
ShadowsocksApplication.settings.edit.putBoolean(Key.profileTip, false).commit
new AlertDialog.Builder(this).setTitle(R.string.profile_manager_dialog)
.setMessage(R.string.profile_manager_dialog_content).setPositiveButton(R.string.gotcha, null).create.show
}
......@@ -218,13 +217,11 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
val scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
if (scanResult != null) Parser.parse(scanResult.getContents) match {
case Some(profile) => ShadowsocksApplication.profileManager.createProfile(profile)
case _ => // ignore
}
if (scanResult != null)
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)
......@@ -237,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
}
}
......@@ -41,8 +41,12 @@ class ShadowsocksNotification(private val service: BaseService, profileName: Str
.setSmallIcon(R.drawable.ic_stat_shadowsocks)
.addAction(android.R.drawable.ic_menu_close_clear_cancel, service.getString(R.string.stop),
PendingIntent.getBroadcast(service, 0, new Intent(Action.CLOSE), 0))
.addAction(android.R.drawable.ic_menu_manage, service.getString(R.string.quick_switch),
PendingIntent.getActivity(service, 0, new Intent(Action.QUICK_SWITCH), 0))
ShadowsocksApplication.profileManager.getAllProfiles match {
case Some(profiles) => if (profiles.length > 1)
builder.addAction(android.R.drawable.ic_menu_manage, service.getString(R.string.quick_switch),
PendingIntent.getActivity(service, 0, new Intent(Action.QUICK_SWITCH), 0))
case _ =>
}
private lazy val style = new BigTextStyle(builder)
private val showOnUnlock = visible && Utils.isLollipopOrAbove
private var isVisible = true
......
......@@ -80,6 +80,8 @@ object Key {
val encMethod = "encMethod"
val remotePort = "remotePortNum"
val localPort = "localPortNum"
val profileTip = "profileTip"
}
object Scheme {
......
......@@ -39,44 +39,35 @@
package com.github.shadowsocks.utils
import com.github.shadowsocks.database.Profile
import java.util.Locale
import android.net.Uri
import android.util.{Log, Base64}
import android.util.{Base64, Log}
import com.github.shadowsocks.database.Profile
object Parser {
val TAG = "ShadowParser"
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 parse (data: String): Option[Profile] = {
try {
Log.d(TAG, data)
val uri = Uri.parse(data.trim)
if (uri.getScheme == Scheme.SS) {
val encoded = data.replace(Scheme.SS + "://", "")
val content = new String(Base64.decode(encoded, Base64.NO_PADDING), "UTF-8")
val methodIdx = content.indexOf(':')
val passwordIdx = content.lastIndexOf('@')
val hostIdx = content.lastIndexOf(':')
val method = content.substring(0, methodIdx)
val password = content.substring(methodIdx + 1, passwordIdx)
val host = content.substring(passwordIdx + 1, hostIdx)
val port = content.substring(hostIdx + 1)
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('@')
val hostIdx = content.lastIndexOf(':')
val host = content.substring(passwordIdx + 1, hostIdx)
val profile = new Profile
profile.name = host
profile.host = host
profile.remotePort = port.toInt
profile.localPort = 1080
profile.method = method.toLowerCase
profile.password = password
return Some(profile)
}
} catch {
case ex : Exception => Log.e(TAG, "parser error", ex)// Ignore
}
None
}
val profile = new Profile
profile.name = host
profile.host = host
profile.remotePort = content.substring(hostIdx + 1).toInt
profile.method = content.substring(0, methodIdx).toLowerCase
profile.password = content.substring(methodIdx + 1, passwordIdx)
profile
} catch {
case ex: Exception =>
Log.e(TAG, "parser error: " + m.source, ex)// Ignore
null
}).filter(_ != null)
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment