375 lines
15 KiB
Scala
375 lines
15 KiB
Scala
/*******************************************************************************/
|
|
/* */
|
|
/* 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 java.util.GregorianCalendar
|
|
|
|
import android.app.Activity
|
|
import android.content._
|
|
import android.net.Uri
|
|
import android.os.Bundle
|
|
import android.support.v7.widget.RecyclerView.ViewHolder
|
|
import android.support.v7.widget._
|
|
import android.support.v7.widget.helper.ItemTouchHelper
|
|
import android.support.v7.widget.helper.ItemTouchHelper.SimpleCallback
|
|
import android.text.TextUtils
|
|
import android.view.View.OnLongClickListener
|
|
import android.view._
|
|
import android.widget.{LinearLayout, PopupMenu, TextView, Toast}
|
|
import com.github.shadowsocks.ShadowsocksApplication.app
|
|
import com.github.shadowsocks.database.Profile
|
|
import com.github.shadowsocks.utils._
|
|
import com.github.shadowsocks.widget.UndoSnackbarManager
|
|
import com.google.android.gms.ads.{AdRequest, AdSize, NativeExpressAdView}
|
|
|
|
import scala.collection.mutable.ArrayBuffer
|
|
import scala.util.Random
|
|
|
|
object ProfilesFragment {
|
|
var instance: ProfilesFragment = _ // used for callback from ProfileManager and stateChanged from MainActivity
|
|
|
|
private final val REQUEST_SCAN_QR_CODE = 0
|
|
}
|
|
|
|
final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickListener {
|
|
import ProfilesFragment._
|
|
|
|
private val cardButtonLongClickListener: OnLongClickListener = view => {
|
|
Utils.positionToast(Toast.makeText(getActivity, view.getContentDescription, Toast.LENGTH_SHORT), view,
|
|
getActivity.getWindow, 0, Utils.dpToPx(getActivity, 8)).show()
|
|
true
|
|
}
|
|
|
|
/**
|
|
* Is ProfilesFragment editable at all.
|
|
*/
|
|
private def isEnabled = getActivity.asInstanceOf[MainActivity].state match {
|
|
case State.CONNECTED | State.STOPPED => true
|
|
case _ => false
|
|
}
|
|
private def isProfileEditable(id: => Int) = getActivity.asInstanceOf[MainActivity].state match {
|
|
case State.CONNECTED => id != app.profileId
|
|
case State.STOPPED => true
|
|
case _ => false
|
|
}
|
|
|
|
final class ProfileViewHolder(view: View) extends RecyclerView.ViewHolder(view)
|
|
with View.OnClickListener with PopupMenu.OnMenuItemClickListener {
|
|
|
|
var item: Profile = _
|
|
|
|
private val title = itemView.findViewById(R.id.title).asInstanceOf[TextView]
|
|
private val address = itemView.findViewById(R.id.address).asInstanceOf[TextView]
|
|
private val traffic = itemView.findViewById(R.id.traffic).asInstanceOf[TextView]
|
|
private val edit = itemView.findViewById(R.id.edit)
|
|
edit.setOnClickListener(_ => startConfig(item.id))
|
|
edit.setOnLongClickListener(cardButtonLongClickListener)
|
|
itemView.setOnClickListener(this)
|
|
// it will not take effect unless set in code
|
|
itemView.findViewById(R.id.indicator).setBackgroundResource(R.drawable.background_profile)
|
|
|
|
private var adView: NativeExpressAdView = _
|
|
|
|
{
|
|
val share = itemView.findViewById(R.id.share)
|
|
share.setOnClickListener(_ => {
|
|
val popup = new PopupMenu(getActivity, share)
|
|
popup.getMenuInflater.inflate(R.menu.profile_share_popup, popup.getMenu)
|
|
popup.setOnMenuItemClickListener(this)
|
|
popup.show()
|
|
})
|
|
share.setOnLongClickListener(cardButtonLongClickListener)
|
|
}
|
|
|
|
def bind(item: Profile) {
|
|
this.item = item
|
|
val editable = isProfileEditable(item.id)
|
|
edit.setEnabled(editable)
|
|
edit.setAlpha(if (editable) 1 else 0.5F)
|
|
var tx = item.tx
|
|
var rx = item.rx
|
|
if (item.id == bandwidthProfile) {
|
|
tx += txTotal
|
|
rx += rxTotal
|
|
}
|
|
title.setText(item.getName)
|
|
address.setText(if (item.nameIsEmpty) "" else item.formattedAddress)
|
|
traffic.setText(getString(R.string.stat_profiles,
|
|
TrafficMonitor.formatTraffic(tx), TrafficMonitor.formatTraffic(rx)))
|
|
|
|
if (item.id == app.profileId) {
|
|
itemView.setSelected(true)
|
|
selectedItem = this
|
|
} else {
|
|
itemView.setSelected(false)
|
|
if (selectedItem eq this) selectedItem = null
|
|
}
|
|
|
|
if (item.host == "198.199.101.152") {
|
|
if (adView == null) {
|
|
val params =
|
|
new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
|
params.gravity = Gravity.CENTER_HORIZONTAL
|
|
params.setMargins(0, Utils.dpToPx(getActivity, 8), 0, 0)
|
|
adView = new NativeExpressAdView(getActivity)
|
|
adView.setLayoutParams(params)
|
|
adView.setAdUnitId("ca-app-pub-9097031975646651/5224027521")
|
|
adView.setAdSize(new AdSize(328, 132))
|
|
itemView.findViewById(R.id.content).asInstanceOf[LinearLayout].addView(adView)
|
|
|
|
// Demographics
|
|
val random = new Random()
|
|
val adBuilder = new AdRequest.Builder()
|
|
adBuilder.setGender(AdRequest.GENDER_MALE)
|
|
val year = 1975 + random.nextInt(40)
|
|
val month = 1 + random.nextInt(12)
|
|
val day = random.nextInt(28)
|
|
adBuilder.setBirthday(new GregorianCalendar(year, month, day).getTime)
|
|
|
|
// Load Ad
|
|
adView.loadAd(adBuilder.build())
|
|
} else adView.setVisibility(View.VISIBLE)
|
|
} else if (adView != null) adView.setVisibility(View.GONE)
|
|
}
|
|
|
|
def onClick(v: View): Unit = if (isEnabled) {
|
|
val activity = getActivity.asInstanceOf[MainActivity]
|
|
val old = app.profileId
|
|
app.switchProfile(item.id)
|
|
profilesAdapter.refreshId(old)
|
|
itemView.setSelected(true)
|
|
if (activity.state == State.CONNECTED) activity.bgService.use(item.id) // reconnect to new profile
|
|
}
|
|
|
|
override def onMenuItemClick(menu: MenuItem): Boolean = menu.getItemId match {
|
|
case R.id.action_qr_code_nfc =>
|
|
getFragmentManager.beginTransaction().add(new QRCodeDialog(item.toString), "").commitAllowingStateLoss()
|
|
true
|
|
case R.id.action_export =>
|
|
clipboard.setPrimaryClip(ClipData.newPlainText(null, item.toString))
|
|
true
|
|
case _ => false
|
|
}
|
|
}
|
|
|
|
final class ProfilesAdapter extends RecyclerView.Adapter[ProfileViewHolder] {
|
|
var profiles = new ArrayBuffer[Profile]
|
|
profiles ++= app.profileManager.getAllProfiles.getOrElse(List.empty[Profile])
|
|
|
|
def getItemCount: Int = profiles.length
|
|
|
|
def onBindViewHolder(vh: ProfileViewHolder, i: Int): Unit = vh.bind(profiles(i))
|
|
|
|
def onCreateViewHolder(vg: ViewGroup, i: Int) =
|
|
new ProfileViewHolder(LayoutInflater.from(vg.getContext).inflate(R.layout.layout_profile, vg, false))
|
|
|
|
setHasStableIds(true) // Reason: http://stackoverflow.com/a/32488059/2245107
|
|
override def getItemId(position: Int): Long = profiles(position).id
|
|
|
|
def add(item: Profile) {
|
|
undoManager.flush()
|
|
val pos = getItemCount
|
|
profiles += item
|
|
notifyItemInserted(pos)
|
|
}
|
|
|
|
def move(from: Int, to: Int) {
|
|
undoManager.flush()
|
|
val step = if (from < to) 1 else -1
|
|
val first = profiles(from)
|
|
var previousOrder = profiles(from).userOrder
|
|
for (i <- from until to by step) {
|
|
val next = profiles(i + step)
|
|
val order = next.userOrder
|
|
next.userOrder = previousOrder
|
|
previousOrder = order
|
|
profiles(i) = next
|
|
app.profileManager.updateProfile(next)
|
|
}
|
|
first.userOrder = previousOrder
|
|
profiles(to) = first
|
|
app.profileManager.updateProfile(first)
|
|
notifyItemMoved(from, to)
|
|
}
|
|
|
|
def remove(pos: Int) {
|
|
profiles.remove(pos)
|
|
notifyItemRemoved(pos)
|
|
}
|
|
def undo(actions: Iterator[(Int, Profile)]): Unit = for ((index, item) <- actions) {
|
|
profiles.insert(index, item)
|
|
notifyItemInserted(index)
|
|
}
|
|
def commit(actions: Iterator[(Int, Profile)]): Unit =
|
|
for ((_, item) <- actions) app.profileManager.delProfile(item.id)
|
|
|
|
def refreshId(id: Int) {
|
|
val index = profiles.indexWhere(_.id == id)
|
|
if (index >= 0) notifyItemChanged(index)
|
|
}
|
|
def deepRefreshId(id: Int) {
|
|
val index = profiles.indexWhere(_.id == id)
|
|
if (index >= 0) {
|
|
profiles(index) = app.profileManager.getProfile(id).get
|
|
notifyItemChanged(index)
|
|
}
|
|
}
|
|
def removeId(id: Int) {
|
|
val index = profiles.indexWhere(_.id == id)
|
|
if (index >= 0) {
|
|
profiles.remove(index)
|
|
notifyItemRemoved(index)
|
|
if (id == app.profileId) app.profileId(0) // switch to null profile
|
|
}
|
|
}
|
|
}
|
|
|
|
private var selectedItem: ProfileViewHolder = _
|
|
|
|
lazy val profilesAdapter = new ProfilesAdapter
|
|
private var undoManager: UndoSnackbarManager[Profile] = _
|
|
private var bandwidthProfile: Int = _
|
|
private var txTotal: Long = _
|
|
private var rxTotal: Long = _
|
|
|
|
private lazy val clipboard = getActivity.getSystemService(Context.CLIPBOARD_SERVICE).asInstanceOf[ClipboardManager]
|
|
|
|
private def startConfig(id: Int) = startActivity(new Intent(getActivity, classOf[ProfileConfigActivity])
|
|
.putExtra(Action.EXTRA_PROFILE_ID, id))
|
|
|
|
override def onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle): View =
|
|
inflater.inflate(R.layout.layout_list, container, false)
|
|
|
|
override def 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 (app.profileManager.getFirstProfile.isEmpty) app.profileId(app.profileManager.createProfile().id)
|
|
val profilesList = view.findViewById(R.id.list).asInstanceOf[RecyclerView]
|
|
val layoutManager = new LinearLayoutManager(getActivity, LinearLayoutManager.VERTICAL, false)
|
|
profilesList.setLayoutManager(layoutManager)
|
|
layoutManager.scrollToPosition(profilesAdapter.profiles.zipWithIndex.collectFirst {
|
|
case (profile, i) if profile.id == app.profileId => i
|
|
}.getOrElse(-1))
|
|
val animator = new DefaultItemAnimator()
|
|
animator.setSupportsChangeAnimations(false) // prevent fading-in/out when rebinding
|
|
profilesList.setItemAnimator(animator)
|
|
profilesList.setAdapter(profilesAdapter)
|
|
instance = this
|
|
undoManager = new UndoSnackbarManager[Profile](getActivity.findViewById(R.id.snackbar),
|
|
profilesAdapter.undo, profilesAdapter.commit)
|
|
new ItemTouchHelper(new SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
|
|
ItemTouchHelper.START | ItemTouchHelper.END) {
|
|
override def getSwipeDirs(recyclerView: RecyclerView, viewHolder: ViewHolder): Int =
|
|
if (isProfileEditable(viewHolder.asInstanceOf[ProfileViewHolder].item.id))
|
|
super.getSwipeDirs(recyclerView, viewHolder) else 0
|
|
override def getDragDirs(recyclerView: RecyclerView, viewHolder: ViewHolder): Int =
|
|
if (isEnabled) super.getDragDirs(recyclerView, viewHolder) else 0
|
|
|
|
def onSwiped(viewHolder: ViewHolder, direction: Int) {
|
|
val index = viewHolder.getAdapterPosition
|
|
profilesAdapter.remove(index)
|
|
undoManager.remove((index, viewHolder.asInstanceOf[ProfileViewHolder].item))
|
|
}
|
|
def onMove(recyclerView: RecyclerView, viewHolder: ViewHolder, target: ViewHolder): Boolean = {
|
|
profilesAdapter.move(viewHolder.getAdapterPosition, target.getAdapterPosition)
|
|
true
|
|
}
|
|
}).attachToRecyclerView(profilesList)
|
|
}
|
|
|
|
override def onTrafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long): Unit =
|
|
if (profileId != -1) { // ignore resets from MainActivity
|
|
if (bandwidthProfile != profileId) {
|
|
onTrafficPersisted(bandwidthProfile)
|
|
bandwidthProfile = profileId
|
|
}
|
|
this.txTotal = txTotal
|
|
this.rxTotal = rxTotal
|
|
profilesAdapter.refreshId(profileId)
|
|
}
|
|
def onTrafficPersisted(profileId: Int) {
|
|
txTotal = 0
|
|
rxTotal = 0
|
|
if (bandwidthProfile != profileId) {
|
|
onTrafficPersisted(bandwidthProfile)
|
|
bandwidthProfile = profileId
|
|
}
|
|
profilesAdapter.deepRefreshId(profileId)
|
|
}
|
|
|
|
override def onDetach() {
|
|
undoManager.flush()
|
|
super.onDetach()
|
|
}
|
|
|
|
override def onDestroy() {
|
|
instance = null
|
|
super.onDestroy()
|
|
}
|
|
|
|
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent): Unit = requestCode match {
|
|
case REQUEST_SCAN_QR_CODE => if (resultCode == Activity.RESULT_OK) {
|
|
val contents = data.getStringExtra("SCAN_RESULT")
|
|
if (!TextUtils.isEmpty(contents)) Parser.findAll(contents).foreach(app.profileManager.createProfile)
|
|
}
|
|
case _ => super.onActivityResult(resultCode, resultCode, data)
|
|
}
|
|
|
|
def onMenuItemClick(item: MenuItem): Boolean = item.getItemId match {
|
|
case R.id.action_scan_qr_code =>
|
|
try startActivityForResult(new Intent("com.google.zxing.client.android.SCAN")
|
|
.addCategory(Intent.CATEGORY_DEFAULT)
|
|
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_DOCUMENT),
|
|
REQUEST_SCAN_QR_CODE) catch {
|
|
case _: ActivityNotFoundException =>
|
|
Toast.makeText(getActivity, R.string.add_profile_scanner_not_installed, Toast.LENGTH_LONG).show()
|
|
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=tw.com.quickmark")))
|
|
}
|
|
true
|
|
case R.id.action_import =>
|
|
if (clipboard.hasPrimaryClip) {
|
|
val profiles = Parser.findAll(clipboard.getPrimaryClip.getItemAt(0).getText)
|
|
if (profiles.nonEmpty) {
|
|
profiles.foreach(app.profileManager.createProfile)
|
|
Toast.makeText(getActivity, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
|
|
return true
|
|
}
|
|
}
|
|
Toast.makeText(getActivity, R.string.action_import_err, Toast.LENGTH_SHORT).show()
|
|
true
|
|
case R.id.action_manual_settings =>
|
|
startConfig(app.profileManager.createProfile().id)
|
|
true
|
|
case R.id.action_export =>
|
|
app.profileManager.getAllProfiles match {
|
|
case Some(profiles) =>
|
|
clipboard.setPrimaryClip(ClipData.newPlainText(null, profiles.mkString("\n")))
|
|
Toast.makeText(getActivity, R.string.action_export_msg, Toast.LENGTH_SHORT).show()
|
|
case _ => Toast.makeText(getActivity, R.string.action_export_err, Toast.LENGTH_SHORT).show()
|
|
}
|
|
true
|
|
case _ => false
|
|
}
|
|
}
|