Unverified Commit 0bd0207b authored by Mygod's avatar Mygod Committed by GitHub

Merge pull request #2211 from shadowsocks/fix-2209

Fix #2209
parents 4fd44a32 d4f211b5
......@@ -4,6 +4,7 @@ apply plugin: 'com.github.ben-manes.versions'
buildscript {
ext {
javaVersion = JavaVersion.VERSION_1_8
kotlinVersion = '1.3.31'
minSdkVersion = 21
sdkVersion = 28
......
......@@ -28,6 +28,11 @@ android {
}
}
compileOptions {
sourceCompatibility javaVersion
targetCompatibility javaVersion
}
externalNativeBuild {
ndkBuild {
path 'src/main/jni/Android.mk'
......@@ -47,6 +52,7 @@ def lifecycleVersion = '2.0.0'
def roomVersion = '2.0.0'
dependencies {
api project(':plugin')
api "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
api "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion"
api "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
api 'androidx.preference:preference:1.1.0-alpha05'
......
......@@ -55,12 +55,23 @@
android:exported="false">
</service>
<activity
android:name="com.github.shadowsocks.UrlImportActivity"
android:theme="@style/Theme.AppCompat.Translucent"
android:excludeFromRecents="true">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="ss"/>
</intent-filter>
</activity>
<activity
android:name="com.github.shadowsocks.VpnRequestActivity"
android:theme="@style/Theme.AppCompat.Translucent"
android:excludeFromRecents="true"
android:taskAffinity=""
android:launchMode="singleTask"/>
android:taskAffinity=""/>
<receiver android:name="com.github.shadowsocks.BootReceiver"
android:process=":bg"
......
/*******************************************************************************
* *
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks
import android.content.DialogInterface
import android.os.Bundle
import android.os.Parcelable
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.github.shadowsocks.core.R
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.AlertDialogFragment
import com.github.shadowsocks.plugin.Empty
import kotlinx.android.parcel.Parcelize
class UrlImportActivity : AppCompatActivity() {
@Parcelize
data class ProfilesArg(val profiles: List<Profile>) : Parcelable
class ImportProfilesDialogFragment : AlertDialogFragment<ProfilesArg, Empty>() {
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
setTitle(R.string.add_profile_dialog)
setPositiveButton(R.string.yes, listener)
setNegativeButton(R.string.no, listener)
setMessage(arg.profiles.joinToString("\n"))
}
override fun onClick(dialog: DialogInterface?, which: Int) {
if (which == DialogInterface.BUTTON_POSITIVE) arg.profiles.forEach { ProfileManager.createProfile(it) }
requireActivity().finish()
}
override fun onDismiss(dialog: DialogInterface) {
requireActivity().finish()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
when (val dialog = handleShareIntent()) {
null -> {
Toast.makeText(this, R.string.profile_invalid_input, Toast.LENGTH_SHORT).show()
finish()
}
else -> dialog.show(supportFragmentManager, null)
}
}
private fun handleShareIntent() = intent.data?.toString()?.let { sharedStr ->
val profiles = Profile.findAllUrls(sharedStr, Core.currentProfile?.first).toList()
if (profiles.isEmpty()) null else ImportProfilesDialogFragment().withArg(ProfilesArg(profiles))
}
}
/*******************************************************************************
* *
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.utils
import androidx.activity.ComponentActivity
import androidx.annotation.MainThread
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
/**
* See also: https://stackoverflow.com/a/30821062/2245107
*/
object SingleInstanceActivity : DefaultLifecycleObserver {
private val active = mutableSetOf<Class<LifecycleOwner>>()
@MainThread
fun register(activity: ComponentActivity) = if (active.add(activity.javaClass)) apply {
activity.lifecycle.addObserver(this)
} else {
activity.finish()
null
}
override fun onDestroy(owner: LifecycleOwner) {
check(active.remove(owner.javaClass)) { "Double destroy?" }
}
}
......@@ -114,7 +114,7 @@
<string name="profile_config">Profile config</string>
<string name="delete">Remove</string>
<string name="delete_confirm_prompt">Are you sure you want to remove this profile?</string>
<string name="share_qr_nfc">QR code/NFC</string>
<string name="share_qr_nfc">QR code</string>
<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>
......
......@@ -39,6 +39,10 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility javaVersion
targetCompatibility javaVersion
}
packagingOptions.exclude '**/*.kotlin_*'
splits {
abi {
......
......@@ -3,13 +3,10 @@
package="com.github.shadowsocks"
tools:ignore="MissingLeanbackSupport">
<uses-permission android:name="android.permission.NFC" />
<uses-permission-sdk-23 android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.touchscreen"
android:required="false"/>
<uses-feature android:name="android.hardware.nfc"
android:required="false"/>
<uses-feature android:name="android.hardware.camera"
android:required="false"/>
......@@ -21,7 +18,7 @@
android:name=".MainActivity"
android:label="@string/app_name"
android:theme="@style/Theme.Shadowsocks.Navigation"
android:launchMode="singleTask">
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
......@@ -30,17 +27,6 @@
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="ss"/>
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="ss" />
</intent-filter>
<meta-data android:name="android.app.shortcuts"
android:resource="@xml/shortcuts"/>
</activity>
......@@ -48,22 +34,19 @@
<activity
android:name=".ProfileConfigActivity"
android:excludeFromRecents="true"
android:label="@string/profile_config"
android:launchMode="singleTask"/>
android:label="@string/profile_config"/>
<activity
android:name=".AppManager"
android:label="@string/proxied_apps"
android:parentActivityName=".ProfileConfigActivity"
android:excludeFromRecents="true"
android:launchMode="singleTask"/>
android:excludeFromRecents="true"/>
<activity
android:name=".UdpFallbackProfileActivity"
android:label="@string/udp_fallback"
android:parentActivityName=".ProfileConfigActivity"
android:excludeFromRecents="true"
android:launchMode="singleTask"/>
android:excludeFromRecents="true"/>
<activity
android:name=".ScannerActivity"
......@@ -84,8 +67,7 @@
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:excludeFromRecents="true"
android:taskAffinity=""
android:process=":bg"
android:launchMode="singleTask">
android:process=":bg">
<intent-filter>
<action android:name="android.intent.action.CREATE_SHORTCUT" />
</intent-filter>
......
......@@ -49,6 +49,7 @@ import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.DirectBoot
import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.SingleInstanceActivity
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.layout_apps.*
import kotlinx.android.synthetic.main.layout_apps_item.view.*
......@@ -208,6 +209,7 @@ class AppManager : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
SingleInstanceActivity.register(this) ?: return
setContentView(R.layout.layout_apps)
setSupportActionBar(toolbar)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
......
......@@ -23,20 +23,15 @@ package com.github.shadowsocks
import android.app.Activity
import android.app.backup.BackupManager
import android.content.ActivityNotFoundException
import android.content.DialogInterface
import android.content.Intent
import android.net.VpnService
import android.nfc.NdefMessage
import android.nfc.NfcAdapter
import android.os.Bundle
import android.os.DeadObjectException
import android.os.Handler
import android.os.Parcelable
import android.util.Log
import android.view.KeyCharacterMap
import android.view.KeyEvent
import android.view.MenuItem
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.browser.customtabs.CustomTabsIntent
import androidx.coordinatorlayout.widget.CoordinatorLayout
......@@ -51,18 +46,14 @@ import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.aidl.ShadowsocksConnection
import com.github.shadowsocks.aidl.TrafficStats
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.AlertDialogFragment
import com.github.shadowsocks.plugin.Empty
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener
import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.SingleInstanceActivity
import com.github.shadowsocks.widget.ServiceButton
import com.github.shadowsocks.widget.StatsBar
import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.parcel.Parcelize
class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPreferenceDataStoreChangeListener,
NavigationView.OnNavigationItemSelectedListener {
......@@ -73,17 +64,6 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
var stateListener: ((BaseService.State) -> Unit)? = null
}
@Parcelize
data class ProfilesArg(val profiles: List<Profile>) : Parcelable
class ImportProfilesDialogFragment : AlertDialogFragment<ProfilesArg, Empty>() {
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
setTitle(R.string.add_profile_dialog)
setPositiveButton(R.string.yes) { _, _ -> arg.profiles.forEach { ProfileManager.createProfile(it) } }
setNegativeButton(R.string.no, null)
setMessage(arg.profiles.joinToString("\n"))
}
}
// UI
private lateinit var fab: ServiceButton
private lateinit var stats: StatsBar
......@@ -167,6 +147,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
SingleInstanceActivity.register(this) ?: return
setContentView(R.layout.layout_main)
stats = findViewById(R.id.stats)
stats.setOnClickListener { if (state == BaseService.State.Connected) stats.testConnection() }
......@@ -184,29 +165,6 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
changeState(BaseService.State.Idle) // reset everything to init state
connection.connect(this, this)
DataStore.publicStore.registerChangeListener(this)
val intent = this.intent
if (intent != null) handleShareIntent(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleShareIntent(intent)
}
private fun handleShareIntent(intent: Intent) {
val sharedStr = when (intent.action) {
Intent.ACTION_VIEW -> intent.data?.toString()
NfcAdapter.ACTION_NDEF_DISCOVERED -> {
val rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
if (rawMsgs != null && rawMsgs.isNotEmpty()) String((rawMsgs[0] as NdefMessage).records[0].payload)
else null
}
else -> null
}
if (sharedStr.isNullOrEmpty()) return
val profiles = Profile.findAllUrls(sharedStr, Core.currentProfile?.first).toList()
if (profiles.isEmpty()) snackbar().setText(R.string.profile_invalid_input).show()
else ImportProfilesDialogFragment().withArg(ProfilesArg(profiles)).show(supportFragmentManager, null)
}
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) {
......
......@@ -32,6 +32,7 @@ import com.github.shadowsocks.plugin.AlertDialogFragment
import com.github.shadowsocks.plugin.Empty
import com.github.shadowsocks.plugin.PluginContract
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.SingleInstanceActivity
class ProfileConfigActivity : AppCompatActivity() {
companion object {
......@@ -51,6 +52,7 @@ class ProfileConfigActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
SingleInstanceActivity.register(this) ?: return
setContentView(R.layout.layout_profile_config)
setSupportActionBar(findViewById(R.id.toolbar))
supportActionBar!!.apply {
......
......@@ -23,9 +23,6 @@ package com.github.shadowsocks
import android.annotation.SuppressLint
import android.app.Activity
import android.content.*
import android.nfc.NdefMessage
import android.nfc.NdefRecord
import android.nfc.NfcAdapter
import android.os.Bundle
import android.text.format.Formatter
import android.util.LongSparseArray
......@@ -82,33 +79,13 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
arguments = bundleOf(Pair(KEY_URL, url))
}
private val url get() = arguments?.getString(KEY_URL)!!
private val nfcShareItem by lazy { url.toByteArray() }
private var adapter: NfcAdapter? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val image = ImageView(context)
image.layoutParams = LinearLayout.LayoutParams(-1, -1)
val size = resources.getDimensionPixelSize(R.dimen.qr_code_size)
image.setImageBitmap((QRCode.from(url).withSize(size, size) as QRCode).bitmap())
image.setImageBitmap((QRCode.from(arguments?.getString(KEY_URL)!!).withSize(size, size) as QRCode).bitmap())
return image
}
override fun onAttach(context: Context) {
super.onAttach(context)
val adapter = NfcAdapter.getDefaultAdapter(context)
adapter?.setNdefPushMessage(NdefMessage(arrayOf(
NdefRecord(NdefRecord.TNF_ABSOLUTE_URI, nfcShareItem, byteArrayOf(), nfcShareItem))), activity)
this.adapter = adapter
}
override fun onDetach() {
super.onDetach()
val activity = activity
if (activity != null && !activity.isFinishing && !activity.isDestroyed)
adapter?.setNdefPushMessage(null, activity)
adapter = null
}
}
inner class ProfileViewHolder(view: View) : RecyclerView.ViewHolder(view),
......@@ -202,7 +179,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
}
override fun onMenuItemClick(item: MenuItem): Boolean = when (item.itemId) {
R.id.action_qr_code_nfc -> {
R.id.action_qr_code -> {
requireFragmentManager().beginTransaction().add(QRCodeDialog(this.item.toString()), "")
.commitAllowingStateLoss()
true
......
......@@ -35,6 +35,7 @@ import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.SingleInstanceActivity
import com.github.shadowsocks.utils.resolveResourceId
class UdpFallbackProfileActivity : AppCompatActivity() {
......@@ -77,11 +78,12 @@ class UdpFallbackProfileActivity : AppCompatActivity() {
private val profilesAdapter = ProfilesAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (editingId == null) {
finish()
return
}
super.onCreate(savedInstanceState)
SingleInstanceActivity.register(this) ?: return
setContentView(R.layout.layout_udp_fallback)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
......
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/action_qr_code_nfc"
<item android:id="@+id/action_qr_code"
android:title="@string/share_qr_nfc"/>
<item android:id="@+id/action_export_clipboard"
android:title="@string/action_export"/>
......
......@@ -26,6 +26,11 @@ android {
testInstrumentationRunner "androidx.testgetRepositoryPassword().runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility javaVersion
targetCompatibility javaVersion
}
buildTypes {
release {
minifyEnabled false
......
......@@ -38,6 +38,10 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility javaVersion
targetCompatibility javaVersion
}
packagingOptions.exclude '**/*.kotlin_*'
splits {
abi {
......
......@@ -17,8 +17,7 @@
tools:replace="theme">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:launchMode="singleTask">
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
......
......@@ -22,10 +22,12 @@ package com.github.shadowsocks.tv
import android.os.Bundle
import androidx.fragment.app.FragmentActivity
import com.github.shadowsocks.utils.SingleInstanceActivity
class MainActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
SingleInstanceActivity.register(this) ?: return
setContentView(R.layout.activity_main)
}
}
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