Files
shadowsocks-android/mobile/src/main/java/com/github/shadowsocks/ProfilesFragment.kt
T

421 lines
18 KiB
Kotlin

/*******************************************************************************
* *
* 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.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")
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() {
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),
View.OnClickListener, 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) }
TooltipCompat.setTooltipText(edit, edit.contentDescription)
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()
}
TooltipCompat.setTooltipText(share, share.contentDescription)
}
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.isNullOrEmpty()) 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
traffic.text = getString(R.string.traffic,
TrafficMonitor.formatTraffic(tx), 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
val context = context!!
adView = AdView(context)
adView.layoutParams = params
adView.adUnitId = "ca-app-pub-9097031975646651/7760346322"
adView.adSize = AdSize.FLUID
val padding = context.resources.getDimensionPixelOffset(R.dimen.profile_padding)
adView.setPadding(padding, 0, 0, padding)
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 onMenuItemClick(item: MenuItem): Boolean = when (item.itemId) {
R.id.action_qr_code_nfc -> {
fragmentManager!!.beginTransaction().add(QRCodeDialog(this.item.toString()), "")
.commitAllowingStateLoss()
true
}
R.id.action_export -> {
clipboard.primaryClip = ClipData.newPlainText(null, this.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 first = profiles[from]
var previousOrder = first.userOrder
val (step, range) = if (from < to) Pair(1, from until to) else Pair(-1, to + 1 downTo from)
for (i in range) {
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 by lazy { 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()
}
}