diff options
15 files changed, 395 insertions, 0 deletions
diff --git a/android/build.gradle b/android/build.gradle index 43bfdb53c9..5be82b79d7 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -31,6 +31,7 @@ repositories { dependencies { implementation 'com.android.support:appcompat-v7:28.0.0' + implementation 'com.android.support:recyclerview-v7:28.0.0' implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.3.21' } diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/SelectLocationFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/SelectLocationFragment.kt index 54c1b1b2e7..71f118410d 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/SelectLocationFragment.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/SelectLocationFragment.kt @@ -2,11 +2,16 @@ package net.mullvad.mullvadvpn import android.os.Bundle import android.support.v4.app.Fragment +import android.support.v7.widget.LinearLayoutManager +import android.support.v7.widget.RecyclerView import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageButton +import net.mullvad.mullvadvpn.relaylist.RelayItemDividerDecoration +import net.mullvad.mullvadvpn.relaylist.RelayListAdapter + class SelectLocationFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, @@ -19,6 +24,13 @@ class SelectLocationFragment : Fragment() { activity?.onBackPressed() } + view.findViewById<RecyclerView>(R.id.relay_list).apply { + layoutManager = LinearLayoutManager(context!!) + adapter = RelayListAdapter() + + addItemDecoration(RelayItemDividerDecoration(context!!)) + } + return view } } diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/GetItemResult.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/GetItemResult.kt new file mode 100644 index 0000000000..d443d30cfe --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/GetItemResult.kt @@ -0,0 +1,6 @@ +package net.mullvad.mullvadvpn.relaylist + +sealed class GetItemResult { + data class Item(val item: RelayItem) : GetItemResult() + data class Count(val count: Int) : GetItemResult() +} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/Relay.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/Relay.kt new file mode 100644 index 0000000000..47a17c69d3 --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/Relay.kt @@ -0,0 +1,3 @@ +package net.mullvad.mullvadvpn.relaylist + +data class Relay(val hostname: String) diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayCity.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayCity.kt new file mode 100644 index 0000000000..5ccd78bde9 --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayCity.kt @@ -0,0 +1,30 @@ +package net.mullvad.mullvadvpn.relaylist + +class RelayCity(val city: String, val relays: List<Relay>, var expanded: Boolean) { + fun getItem(position: Int): GetItemResult { + if (position == 0) { + return GetItemResult.Item(RelayItem(RelayItemType.City, city)) + } + + if (!expanded) { + return GetItemResult.Count(1) + } + + val offset = position - 1 + val relayCount = relays.size + + if (offset >= relayCount) { + return GetItemResult.Count(1 + relayCount) + } else { + return GetItemResult.Item(RelayItem(RelayItemType.Relay, relays[offset].hostname)) + } + } + + fun getItemCount(): Int { + if (expanded) { + return 1 + relays.size + } else { + return 1 + } + } +} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayCountry.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayCountry.kt new file mode 100644 index 0000000000..6f54f50518 --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayCountry.kt @@ -0,0 +1,36 @@ +package net.mullvad.mullvadvpn.relaylist + +class RelayCountry(val country: String, val cities: List<RelayCity>, var expanded: Boolean) { + fun getItem(position: Int): GetItemResult { + if (position == 0) { + return GetItemResult.Item(RelayItem(RelayItemType.Country, country)) + } + + var itemCount = 1 + var remaining = position - 1 + + if (expanded) { + for (city in cities) { + val itemOrCount = city.getItem(remaining) + + when (itemOrCount) { + is GetItemResult.Item -> return itemOrCount + is GetItemResult.Count -> { + remaining -= itemOrCount.count + itemCount += itemOrCount.count + } + } + } + } + + return GetItemResult.Count(itemCount) + } + + fun getItemCount(): Int { + if (expanded) { + return 1 + cities.map { city -> city.getItemCount() }.sum() + } else { + return 1 + } + } +} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItem.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItem.kt new file mode 100644 index 0000000000..4814df43db --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItem.kt @@ -0,0 +1,3 @@ +package net.mullvad.mullvadvpn.relaylist + +data class RelayItem(val type: RelayItemType, val name: String) diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemDividerDecoration.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemDividerDecoration.kt new file mode 100644 index 0000000000..01e66e63ab --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemDividerDecoration.kt @@ -0,0 +1,23 @@ +package net.mullvad.mullvadvpn.relaylist + +import android.content.Context +import android.graphics.Rect +import android.support.v7.widget.RecyclerView +import android.support.v7.widget.RecyclerView.ItemDecoration +import android.support.v7.widget.RecyclerView.State +import android.view.View + +import net.mullvad.mullvadvpn.R + +class RelayItemDividerDecoration(private val context: Context) : ItemDecoration() { + private val dividerHeight = context.resources.getDimensionPixelSize(R.dimen.relay_list_divider) + + override fun getItemOffsets(offsets: Rect, view: View, parent: RecyclerView, state: State) { + val position = parent.getChildAdapterPosition(view) + val lastItem = parent.adapter!!.itemCount - 1 + + if (position != lastItem) { + offsets.bottom = dividerHeight + } + } +} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemHolder.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemHolder.kt new file mode 100644 index 0000000000..4934b55c4f --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemHolder.kt @@ -0,0 +1,53 @@ +package net.mullvad.mullvadvpn.relaylist + +import android.support.v7.widget.RecyclerView.ViewHolder +import android.view.View +import android.widget.TextView + +import net.mullvad.mullvadvpn.R + +class RelayItemHolder(private val view: View) : ViewHolder(view) { + private val name: TextView = view.findViewById(R.id.name) + + private val countryColor = view.context.getColor(R.color.blue) + private val cityColor = view.context.getColor(R.color.blue40) + private val relayColor = view.context.getColor(R.color.blue20) + + private val countryPadding = view.resources.getDimensionPixelSize(R.dimen.country_row_padding) + private val cityPadding = view.resources.getDimensionPixelSize(R.dimen.city_row_padding) + private val relayPadding = view.resources.getDimensionPixelSize(R.dimen.relay_row_padding) + + var item: RelayItem? = null + set(value) { + field = value + updateView() + } + + private fun updateView() { + val item = this.item + + if (item != null) { + name.text = item.name + + when (item.type) { + RelayItemType.Country -> setViewStyle(countryColor, countryPadding) + RelayItemType.City -> setViewStyle(cityColor, cityPadding) + RelayItemType.Relay -> setViewStyle(relayColor, relayPadding) + } + } else { + name.text = "" + } + } + + private fun setViewStyle(backgroundColor: Int, padding: Int) { + val paddingLeft = padding + val paddingTop = view.paddingTop + val paddingRight = view.paddingRight + val paddingBottom = view.paddingBottom + + view.apply { + setBackgroundColor(backgroundColor) + setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom) + } + } +} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemType.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemType.kt new file mode 100644 index 0000000000..cdbd58b291 --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemType.kt @@ -0,0 +1,7 @@ +package net.mullvad.mullvadvpn.relaylist + +enum class RelayItemType { + Country, + City, + Relay, +} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayListAdapter.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayListAdapter.kt new file mode 100644 index 0000000000..5efa8f5990 --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayListAdapter.kt @@ -0,0 +1,168 @@ +package net.mullvad.mullvadvpn.relaylist + +import android.support.v7.widget.RecyclerView.Adapter +import android.view.LayoutInflater +import android.view.ViewGroup + +import net.mullvad.mullvadvpn.R + +class RelayListAdapter : Adapter<RelayItemHolder>() { + private val relayList = fakeRelayList + + override fun onCreateViewHolder(parentView: ViewGroup, type: Int): RelayItemHolder { + val inflater = LayoutInflater.from(parentView.context) + val view = inflater.inflate(R.layout.relay_list_item, parentView, false) + + return RelayItemHolder(view) + } + + override fun onBindViewHolder(holder: RelayItemHolder, position: Int) { + var remaining = position + + for (country in relayList) { + val itemOrCount = country.getItem(remaining) + + when (itemOrCount) { + is GetItemResult.Item -> { + holder.item = itemOrCount.item + return + } + is GetItemResult.Count -> remaining -= itemOrCount.count + } + } + } + + override fun getItemCount(): Int { + return relayList.map { country -> country.getItemCount() }.sum() + } +} + +val fakeRelayList = listOf( + RelayCountry( + "Australia", + listOf( + RelayCity( + "Brisbane", + listOf(Relay("au-bne-001")), + false + ), + RelayCity( + "Melbourne", + listOf(Relay("au-mel-002"), Relay("au-mel-003"), Relay("au-mel-004")), + false + ), + RelayCity( + "Perth", + listOf(Relay("au-per-001")), + false + ), + RelayCity( + "Sydney", + listOf( + Relay("au1-wireguard"), + Relay("au-syd-001"), + Relay("au-syd-002"), + Relay("au-mel-003") + ), + false + ) + ), + false + ), + RelayCountry( + "South Africa", + listOf( + RelayCity( + "Johannesburg", + listOf(Relay("za-jnb-001")), + false + ) + ), + false + ), + RelayCountry( + "Sweden", + listOf( + RelayCity( + "Gothenburg", + listOf( + Relay("se3-wireguard"), + Relay("se5-wireguard"), + Relay("se-got-001"), + Relay("se-got-002"), + Relay("se-got-003"), + Relay("se-got-004"), + Relay("se-got-005"), + Relay("se-got-006"), + Relay("se-got-007") + ), + false + ), + RelayCity( + "Helsingborg", + listOf( + Relay("se-hel-001"), + Relay("se-hel-002"), + Relay("se-hel-003"), + Relay("se-hel-004"), + Relay("se-hel-007"), + Relay("se-hel-008") + ), + false + ), + RelayCity( + "Malmö", + listOf( + Relay("se4-wireguard"), + Relay("se-mma-001"), + Relay("se-mma-002"), + Relay("se-mma-003"), + Relay("se-mma-004"), + Relay("se-mma-005"), + Relay("se-mma-006"), + Relay("se-mma-007"), + Relay("se-mma-008"), + Relay("se-mma-009"), + Relay("se-mma-010") + ), + false + ), + RelayCity( + "Stockholm", + listOf( + Relay("se2-wireguard"), + Relay("se6-wireguard"), + Relay("se7-wireguard"), + Relay("se8-wireguard"), + Relay("se-sto-001"), + Relay("se-sto-002"), + Relay("se-sto-003"), + Relay("se-sto-004"), + Relay("se-sto-005"), + Relay("se-sto-006"), + Relay("se-sto-007"), + Relay("se-sto-008"), + Relay("se-sto-009"), + Relay("se-sto-010"), + Relay("se-sto-011"), + Relay("se-sto-012"), + Relay("se-sto-013"), + Relay("se-sto-014"), + Relay("se-sto-015"), + Relay("se-sto-016"), + Relay("se-sto-017"), + Relay("se-sto-018"), + Relay("se-sto-019"), + Relay("se-sto-020"), + Relay("se-sto-021"), + Relay("se-sto-022"), + Relay("se-sto-023"), + Relay("se-sto-024"), + Relay("se-sto-025") + ), + false + ) + ), + false + ) +) diff --git a/android/src/main/res/drawable/icon_relay_active.xml b/android/src/main/res/drawable/icon_relay_active.xml new file mode 100644 index 0000000000..f6e70521f8 --- /dev/null +++ b/android/src/main/res/drawable/icon_relay_active.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape + xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="oval" + > + <solid android:color="@color/green"/> + <size android:width="16dp" android:height="16dp"/> +</shape> diff --git a/android/src/main/res/layout/relay_list_item.xml b/android/src/main/res/layout/relay_list_item.xml new file mode 100644 index 0000000000..16561ca0ec --- /dev/null +++ b/android/src/main/res/layout/relay_list_item.xml @@ -0,0 +1,29 @@ +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/main_fragment" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingLeft="@dimen/country_row_padding" + android:paddingRight="24dp" + android:paddingVertical="16dp" + android:background="@color/blue" + android:orientation="horizontal" + android:gravity="center" + > + <ImageView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="0" + android:src="@drawable/icon_relay_active" + /> + <TextView android:id="@+id/name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:layout_marginHorizontal="8dp" + android:textColor="@color/white" + android:textSize="20sp" + android:textStyle="bold" + android:text="" + /> +</LinearLayout> diff --git a/android/src/main/res/layout/select_location.xml b/android/src/main/res/layout/select_location.xml index 0835c36dc3..a372b32a9f 100644 --- a/android/src/main/res/layout/select_location.xml +++ b/android/src/main/res/layout/select_location.xml @@ -11,6 +11,7 @@ <ImageButton android:id="@+id/close" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_weight="0" android:layout_margin="12dp" android:background="?android:attr/selectableItemBackground" android:src="@drawable/icon_close" @@ -18,6 +19,7 @@ <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_weight="0" android:layout_marginVertical="4dp" android:layout_marginHorizontal="24dp" android:textColor="@color/white" @@ -28,9 +30,17 @@ <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_weight="0" android:layout_marginHorizontal="24dp" + android:layout_marginBottom="24dp" android:textColor="@color/white60" android:textSize="13sp" android:text="@string/select_location_description" /> + <android.support.v7.widget.RecyclerView android:id="@+id/relay_list" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" + android:scrollbars="vertical" + /> </LinearLayout> diff --git a/android/src/main/res/values/dimensions.xml b/android/src/main/res/values/dimensions.xml new file mode 100644 index 0000000000..379843b2bf --- /dev/null +++ b/android/src/main/res/values/dimensions.xml @@ -0,0 +1,6 @@ +<resources> + <dimen name="country_row_padding">20dp</dimen> + <dimen name="city_row_padding">40dp</dimen> + <dimen name="relay_row_padding">60dp</dimen> + <dimen name="relay_list_divider">1dp</dimen> +</resources> |
