/******************************************************************************* * * * Copyright (C) 2017 by Max Lv * * Copyright (C) 2017 by Mygod Studio * * * * 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 . * * * *******************************************************************************/ 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") public 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() 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(android.R.id.text1) private val text2 = itemView.findViewById(android.R.id.text2) private val traffic = itemView.findViewById(R.id.traffic) private val edit = itemView.findViewById(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(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() 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(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() { 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>) { for ((index, item) in actions) { profiles.add(index, item) notifyItemInserted(index) } } fun commit(actions: List>) { 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 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(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() } }