Initial draft for porting this project to Gradle + Kotlin
This is one of my stupidest idea ever.
This commit is contained in:
@@ -0,0 +1,421 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 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.annotation.SuppressLint
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.nfc.NdefMessage
|
||||
import android.nfc.NdefRecord
|
||||
import android.nfc.NfcAdapter
|
||||
import android.os.Bundle
|
||||
import android.support.design.widget.Snackbar
|
||||
import android.support.v4.app.DialogFragment
|
||||
import android.support.v7.widget.*
|
||||
import android.support.v7.widget.helper.ItemTouchHelper
|
||||
import android.view.*
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.bg.BaseService
|
||||
import com.github.shadowsocks.bg.TrafficMonitor
|
||||
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.Action
|
||||
import com.github.shadowsocks.utils.position
|
||||
import com.github.shadowsocks.widget.UndoSnackbarManager
|
||||
import com.google.android.gms.ads.AdRequest
|
||||
import com.google.android.gms.ads.AdSize
|
||||
import com.google.android.gms.ads.AdView
|
||||
import net.glxn.qrgen.android.QRCode
|
||||
|
||||
class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
|
||||
companion object {
|
||||
/**
|
||||
* used for callback from ProfileManager and stateChanged from MainActivity
|
||||
*/
|
||||
var instance: ProfilesFragment? = null
|
||||
|
||||
private const val KEY_URL = "com.github.shadowsocks.QRCodeDialog.KEY_URL"
|
||||
}
|
||||
|
||||
/**
|
||||
* Is ProfilesFragment editable at all.
|
||||
*/
|
||||
private val isEnabled get() = when ((activity as MainActivity).state) {
|
||||
BaseService.CONNECTED, BaseService.STOPPED -> true
|
||||
else -> false
|
||||
}
|
||||
private fun isProfileEditable(id: Int) = when ((activity as MainActivity).state) {
|
||||
BaseService.CONNECTED -> id != DataStore.profileId
|
||||
BaseService.STOPPED -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
@SuppressLint("ValidFragment")
|
||||
private class QRCodeDialog() : DialogFragment() {
|
||||
constructor(url: String) : this() {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(KEY_URL, url)
|
||||
arguments = bundle
|
||||
}
|
||||
|
||||
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())
|
||||
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() {
|
||||
adapter?.setNdefPushMessage(null, activity)
|
||||
adapter = null
|
||||
super.onDetach()
|
||||
}
|
||||
}
|
||||
|
||||
inner class ProfileViewHolder(view: View) : RecyclerView.ViewHolder(view),
|
||||
View.OnClickListener, View.OnLongClickListener, PopupMenu.OnMenuItemClickListener {
|
||||
internal lateinit var item: Profile
|
||||
|
||||
private val text1 = itemView.findViewById<TextView>(android.R.id.text1)
|
||||
private val text2 = itemView.findViewById<TextView>(android.R.id.text2)
|
||||
private val traffic = itemView.findViewById<TextView>(R.id.traffic)
|
||||
private val edit = itemView.findViewById<View>(R.id.edit)
|
||||
private var adView: AdView? = null
|
||||
|
||||
init {
|
||||
edit.setOnClickListener { startConfig(item.id) }
|
||||
edit.setOnLongClickListener(this)
|
||||
itemView.setOnClickListener(this)
|
||||
val share = itemView.findViewById<View>(R.id.share)
|
||||
share.setOnClickListener {
|
||||
val popup = PopupMenu(activity!!, share)
|
||||
popup.menuInflater.inflate(R.menu.profile_share_popup, popup.menu)
|
||||
popup.setOnMenuItemClickListener(this)
|
||||
popup.show()
|
||||
}
|
||||
share.setOnLongClickListener(this)
|
||||
}
|
||||
|
||||
fun bind(item: Profile) {
|
||||
this.item = item
|
||||
val editable = isProfileEditable(item.id)
|
||||
edit.isEnabled = editable
|
||||
edit.alpha = if (editable) 1F else .5F
|
||||
var tx = item.tx
|
||||
var rx = item.rx
|
||||
if (item.id == bandwidthProfile) {
|
||||
tx += txTotal
|
||||
rx += rxTotal
|
||||
}
|
||||
text1.text = item.formattedName
|
||||
val t2 = ArrayList<String>()
|
||||
if (item.name.isNotEmpty()) t2 += item.formattedAddress
|
||||
val id = PluginConfiguration(item.plugin ?: "").selected
|
||||
if (id.isNotEmpty()) t2 += app.getString(R.string.profile_plugin, id)
|
||||
if (t2.isEmpty()) text2.visibility = View.GONE else {
|
||||
text2.visibility = View.VISIBLE
|
||||
text2.text = t2.joinToString("\n")
|
||||
}
|
||||
if (tx <= 0 && rx <= 0) traffic.visibility = View.GONE else {
|
||||
traffic.visibility = View.VISIBLE
|
||||
@SuppressLint("SetTextI18n")
|
||||
traffic.text = "${TrafficMonitor.formatTraffic(tx)}↑\t${TrafficMonitor.formatTraffic(rx)}↓"
|
||||
}
|
||||
|
||||
if (item.id == DataStore.profileId) {
|
||||
itemView.isSelected = true
|
||||
selectedItem = this
|
||||
} else {
|
||||
itemView.isSelected = false
|
||||
if (selectedItem === this) selectedItem = null
|
||||
}
|
||||
|
||||
var adView = adView
|
||||
if (item.host == "198.199.101.152") {
|
||||
if (adView == null) {
|
||||
val params =
|
||||
LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
params.gravity = Gravity.CENTER_HORIZONTAL
|
||||
adView = AdView(activity)
|
||||
adView.layoutParams = params
|
||||
adView.adUnitId = "ca-app-pub-9097031975646651/7760346322"
|
||||
adView.adSize = AdSize.FLUID
|
||||
|
||||
itemView.findViewById<LinearLayout>(R.id.content).addView(adView)
|
||||
|
||||
// Load Ad
|
||||
val adBuilder = AdRequest.Builder()
|
||||
adBuilder.addTestDevice("B08FC1764A7B250E91EA9D0D5EBEB208")
|
||||
adView.loadAd(adBuilder.build())
|
||||
this.adView = adView
|
||||
} else adView.visibility = View.VISIBLE
|
||||
} else if (adView != null) adView.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun onClick(v: View?) {
|
||||
if (isEnabled) {
|
||||
val activity = activity as MainActivity
|
||||
val old = DataStore.profileId
|
||||
app.switchProfile(item.id)
|
||||
profilesAdapter.refreshId(old)
|
||||
itemView.isSelected = true
|
||||
if (activity.state == BaseService.CONNECTED) app.reloadService()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLongClick(view: View): Boolean {
|
||||
Toast.makeText(activity, view.contentDescription, Toast.LENGTH_SHORT).position(view, activity!!.window,
|
||||
0, resources.getDimensionPixelOffset(R.dimen.margin_small)).show()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean = when (item.itemId) {
|
||||
R.id.action_qr_code_nfc -> {
|
||||
fragmentManager!!.beginTransaction().add(QRCodeDialog(item.toString()), "")
|
||||
.commitAllowingStateLoss()
|
||||
true
|
||||
}
|
||||
R.id.action_export -> {
|
||||
clipboard.primaryClip = ClipData.newPlainText(null, item.toString())
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
inner class ProfilesAdapter : RecyclerView.Adapter<ProfileViewHolder>() {
|
||||
internal val profiles = ProfileManager.getAllProfiles()?.toMutableList() ?: mutableListOf()
|
||||
|
||||
init {
|
||||
setHasStableIds(true) // see: http://stackoverflow.com/a/32488059/2245107
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ProfileViewHolder, position: Int) = holder.bind(profiles[position])
|
||||
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ProfileViewHolder = ProfileViewHolder(
|
||||
LayoutInflater.from(parent!!.context).inflate(R.layout.layout_profile, parent, false))
|
||||
override fun getItemCount(): Int = profiles.size
|
||||
override fun getItemId(position: Int): Long = profiles[position].id.toLong()
|
||||
|
||||
fun add(item: Profile) {
|
||||
undoManager.flush()
|
||||
val pos = itemCount
|
||||
profiles += item
|
||||
notifyItemInserted(pos)
|
||||
}
|
||||
|
||||
fun move(from: Int, to: Int) {
|
||||
undoManager.flush()
|
||||
val step = if (from < to) 1 else -1
|
||||
val first = profiles[from]
|
||||
var previousOrder = first.userOrder
|
||||
for (i in if (from < to) from until to else from..to + 1 step -1) {
|
||||
val next = profiles[i + step]
|
||||
val order = next.userOrder
|
||||
next.userOrder = previousOrder
|
||||
previousOrder = order
|
||||
profiles[i] = next
|
||||
ProfileManager.updateProfile(next)
|
||||
}
|
||||
first.userOrder = previousOrder
|
||||
profiles[to] = first
|
||||
ProfileManager.updateProfile(first)
|
||||
notifyItemMoved(from, to)
|
||||
}
|
||||
|
||||
fun remove(pos: Int) {
|
||||
profiles.removeAt(pos)
|
||||
notifyItemRemoved(pos)
|
||||
}
|
||||
fun undo(actions: List<Pair<Int, Profile>>) {
|
||||
for ((index, item) in actions) {
|
||||
profiles.add(index, item)
|
||||
notifyItemInserted(index)
|
||||
}
|
||||
}
|
||||
fun commit(actions: List<Pair<Int, Profile>>) {
|
||||
for ((_, item) in actions) ProfileManager.delProfile(item.id)
|
||||
}
|
||||
|
||||
fun refreshId(id: Int) {
|
||||
val index = profiles.indexOfFirst { it.id == id }
|
||||
if (index >= 0) notifyItemChanged(index)
|
||||
}
|
||||
fun deepRefreshId(id: Int) {
|
||||
val index = profiles.indexOfFirst { it.id == id }
|
||||
if (index < 0) return
|
||||
profiles[index] = ProfileManager.getProfile(id)!!
|
||||
notifyItemChanged(index)
|
||||
}
|
||||
fun removeId(id: Int) {
|
||||
val index = profiles.indexOfFirst { it.id == id }
|
||||
if (index < 0) return
|
||||
profiles.removeAt(index)
|
||||
notifyItemRemoved(index)
|
||||
if (id == DataStore.profileId) DataStore.profileId = 0 // switch to null profile
|
||||
}
|
||||
}
|
||||
|
||||
private var selectedItem: ProfileViewHolder? = null
|
||||
|
||||
val profilesAdapter = ProfilesAdapter()
|
||||
private lateinit var undoManager: UndoSnackbarManager<Profile>
|
||||
private var bandwidthProfile: Int = 0
|
||||
private var txTotal: Long = 0L
|
||||
private var rxTotal: Long = 0L
|
||||
|
||||
private val clipboard by lazy { activity!!.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager }
|
||||
|
||||
private fun startConfig(id: Int) = startActivity(Intent(context, ProfileConfigActivity::class.java)
|
||||
.putExtra(Action.EXTRA_PROFILE_ID, id))
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
|
||||
inflater.inflate(R.layout.layout_list, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
toolbar.setTitle(R.string.profiles)
|
||||
toolbar.inflateMenu(R.menu.profile_manager_menu)
|
||||
toolbar.setOnMenuItemClickListener(this)
|
||||
|
||||
if (ProfileManager.getFirstProfile() == null)
|
||||
DataStore.profileId = ProfileManager.createProfile().id
|
||||
val profilesList = view.findViewById<RecyclerView>(R.id.list)
|
||||
val layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||
profilesList.layoutManager = layoutManager
|
||||
profilesList.addItemDecoration(DividerItemDecoration(context, layoutManager.orientation))
|
||||
layoutManager.scrollToPosition(profilesAdapter.profiles.indexOfFirst { it.id == DataStore.profileId })
|
||||
val animator = DefaultItemAnimator()
|
||||
animator.supportsChangeAnimations = false // prevent fading-in/out when rebinding
|
||||
profilesList.itemAnimator = animator
|
||||
profilesList.adapter = profilesAdapter
|
||||
instance = this
|
||||
undoManager = UndoSnackbarManager(activity!!.findViewById(R.id.snackbar),
|
||||
profilesAdapter::undo, profilesAdapter::commit)
|
||||
ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN,
|
||||
ItemTouchHelper.START or ItemTouchHelper.END) {
|
||||
override fun getSwipeDirs(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int =
|
||||
if (isProfileEditable((viewHolder as ProfileViewHolder).item.id))
|
||||
super.getSwipeDirs(recyclerView, viewHolder) else 0
|
||||
override fun getDragDirs(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int =
|
||||
if (isEnabled) super.getDragDirs(recyclerView, viewHolder) else 0
|
||||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
val index = viewHolder.adapterPosition
|
||||
profilesAdapter.remove(index)
|
||||
undoManager.remove(Pair(index, (viewHolder as ProfileViewHolder).item))
|
||||
}
|
||||
override fun onMove(recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
|
||||
profilesAdapter.move(viewHolder.adapterPosition, target.adapterPosition)
|
||||
return true
|
||||
}
|
||||
}).attachToRecyclerView(profilesList)
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.action_scan_qr_code -> {
|
||||
startActivity(Intent(context, ScannerActivity::class.java))
|
||||
true
|
||||
}
|
||||
R.id.action_import -> {
|
||||
try {
|
||||
val profiles = Profile.findAll(clipboard.primaryClip.getItemAt(0).text).toList()
|
||||
if (profiles.isNotEmpty()) {
|
||||
profiles.forEach { ProfileManager.createProfile(it) }
|
||||
Toast.makeText(activity, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
|
||||
return true
|
||||
}
|
||||
} catch (exc: Exception) {
|
||||
app.track(exc)
|
||||
}
|
||||
Snackbar.make(activity!!.findViewById(R.id.snackbar), R.string.action_import_err, Snackbar.LENGTH_LONG)
|
||||
.show()
|
||||
true
|
||||
}
|
||||
R.id.action_manual_settings -> {
|
||||
startConfig(ProfileManager.createProfile().id)
|
||||
true
|
||||
}
|
||||
R.id.action_export -> {
|
||||
val profiles = ProfileManager.getAllProfiles()
|
||||
if (profiles != null) {
|
||||
clipboard.primaryClip = ClipData.newPlainText(null, profiles.joinToString("\n"))
|
||||
Toast.makeText(activity, R.string.action_export_msg, Toast.LENGTH_SHORT).show()
|
||||
} else Toast.makeText(activity, R.string.action_export_err, Toast.LENGTH_SHORT).show()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTrafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
|
||||
if (profileId != -1) { // ignore resets from MainActivity
|
||||
if (bandwidthProfile != profileId) {
|
||||
onTrafficPersisted(bandwidthProfile)
|
||||
bandwidthProfile = profileId
|
||||
}
|
||||
this.txTotal = txTotal
|
||||
this.rxTotal = rxTotal
|
||||
profilesAdapter.refreshId(profileId)
|
||||
}
|
||||
}
|
||||
fun onTrafficPersisted(profileId: Int) {
|
||||
txTotal = 0
|
||||
rxTotal = 0
|
||||
if (bandwidthProfile != profileId) {
|
||||
onTrafficPersisted(bandwidthProfile)
|
||||
bandwidthProfile = profileId
|
||||
}
|
||||
profilesAdapter.deepRefreshId(profileId)
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
undoManager.flush()
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
instance = null
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user