diff --git a/app/build.gradle b/app/build.gradle
index 96cc465..a63949c 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -25,10 +25,12 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
- implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'com.google.android.material:material:1.0.0'
+ implementation 'androidx.legacy:legacy-support-v4:1.0.0'
+ implementation 'androidx.recyclerview:recyclerview:1.0.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e4c0ecf..2332e1e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -14,6 +14,24 @@
android:supportsRtl="true"
android:theme="@style/AppTheme">
+
+
+
+
+
+
@@ -24,4 +42,5 @@
+
\ No newline at end of file
diff --git a/app/src/main/java/fr/plnech/dunbar/BuddyDetailActivity.kt b/app/src/main/java/fr/plnech/dunbar/BuddyDetailActivity.kt
new file mode 100644
index 0000000..f2b9603
--- /dev/null
+++ b/app/src/main/java/fr/plnech/dunbar/BuddyDetailActivity.kt
@@ -0,0 +1,72 @@
+package fr.plnech.dunbar
+
+import android.content.Intent
+import android.os.Bundle
+import com.google.android.material.snackbar.Snackbar
+import androidx.appcompat.app.AppCompatActivity
+import android.view.MenuItem
+import kotlinx.android.synthetic.main.activity_buddy_detail.*
+
+/**
+ * An activity representing a single Buddy detail screen. This
+ * activity is only used on narrow width devices. On tablet-size devices,
+ * item details are presented side-by-side with a list of items
+ * in a [BuddyListActivity].
+ */
+class BuddyDetailActivity : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_buddy_detail)
+ setSupportActionBar(detail_toolbar)
+
+ fab.setOnClickListener { view ->
+ Snackbar.make(view, "Replace with your own detail action", Snackbar.LENGTH_LONG)
+ .setAction("Action", null).show()
+ }
+
+ // Show the Up button in the action bar.
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+
+ // savedInstanceState is non-null when there is fragment state
+ // saved from previous configurations of this activity
+ // (e.g. when rotating the screen from portrait to landscape).
+ // In this case, the fragment will automatically be re-added
+ // to its container so we don't need to manually add it.
+ // For more information, see the Fragments API guide at:
+ //
+ // http://developer.android.com/guide/components/fragments.html
+ //
+ if (savedInstanceState == null) {
+ // Create the detail fragment and add it to the activity
+ // using a fragment transaction.
+ val fragment = BuddyDetailFragment().apply {
+ arguments = Bundle().apply {
+ putString(
+ BuddyDetailFragment.ARG_ITEM_ID,
+ intent.getStringExtra(BuddyDetailFragment.ARG_ITEM_ID)
+ )
+ }
+ }
+
+ supportFragmentManager.beginTransaction()
+ .add(R.id.buddy_detail_container, fragment)
+ .commit()
+ }
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem) =
+ when (item.itemId) {
+ android.R.id.home -> {
+ // This ID represents the Home or Up button. In the case of this
+ // activity, the Up button is shown. For
+ // more details, see the Navigation pattern on Android Design:
+ //
+ // http://developer.android.com/design/patterns/navigation.html#up-vs-back
+
+ navigateUpTo(Intent(this, BuddyListActivity::class.java))
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+}
diff --git a/app/src/main/java/fr/plnech/dunbar/BuddyDetailFragment.kt b/app/src/main/java/fr/plnech/dunbar/BuddyDetailFragment.kt
new file mode 100644
index 0000000..d7144c4
--- /dev/null
+++ b/app/src/main/java/fr/plnech/dunbar/BuddyDetailFragment.kt
@@ -0,0 +1,62 @@
+package fr.plnech.dunbar
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import fr.plnech.dunbar.dummy.DummyContent
+import fr.plnech.dunbar.model.Friend
+import kotlinx.android.synthetic.main.activity_buddy_detail.*
+import kotlinx.android.synthetic.main.buddy_detail.view.*
+
+/**
+ * A fragment representing a single Buddy detail screen.
+ * This fragment is either contained in a [BuddyListActivity]
+ * in two-pane mode (on tablets) or a [BuddyDetailActivity]
+ * on handsets.
+ */
+class BuddyDetailFragment : Fragment() {
+
+ /**
+ * The dummy content this fragment is presenting.
+ */
+ private var item: Friend? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ arguments?.let {
+ if (it.containsKey(ARG_ITEM_ID)) {
+ // Load the dummy content specified by the fragment
+ // arguments. In a real-world scenario, use a Loader
+ // to load content from a content provider.
+ item = DummyContent.ITEM_MAP[it.getInt(ARG_ITEM_ID)]
+ activity?.toolbar_layout?.title = item?.name
+ }
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ val rootView = inflater.inflate(R.layout.buddy_detail, container, false)
+
+ // Show the dummy content as text in a TextView.
+ item?.let {
+ rootView.buddy_detail.text = it.mapString()
+ activity?.title = it.name
+ }
+
+ return rootView
+ }
+
+ companion object {
+ /**
+ * The fragment argument representing the item ID that this fragment
+ * represents.
+ */
+ const val ARG_ITEM_ID = "item_id"
+ }
+}
diff --git a/app/src/main/java/fr/plnech/dunbar/BuddyListActivity.kt b/app/src/main/java/fr/plnech/dunbar/BuddyListActivity.kt
new file mode 100644
index 0000000..975d13f
--- /dev/null
+++ b/app/src/main/java/fr/plnech/dunbar/BuddyListActivity.kt
@@ -0,0 +1,121 @@
+package fr.plnech.dunbar
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.app.NavUtils
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.snackbar.Snackbar
+import fr.plnech.dunbar.dummy.DummyContent
+import fr.plnech.dunbar.model.Friend
+import kotlinx.android.synthetic.main.activity_buddy_list.*
+import kotlinx.android.synthetic.main.buddy_list.*
+import kotlinx.android.synthetic.main.buddy_list_content.view.*
+
+class BuddyListActivity : AppCompatActivity() {
+
+ /**
+ * Whether or not the activity is in two-pane mode, i.e. running on a tablet
+ * device.
+ */
+ private var twoPane: Boolean = false
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_buddy_list)
+
+ setSupportActionBar(toolbar)
+ toolbar.title = title
+
+ fab.setOnClickListener { view ->
+ Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
+ .setAction("Action", null).show()
+ }
+ // Show the Up button in the action bar.
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+
+ if (buddy_detail_container != null) {
+ // The detail container view will be present only in the
+ // large-screen layouts (res/values-w900dp).
+ // If this view is present, then the
+ // activity should be in two-pane mode.
+ twoPane = true
+ }
+
+ setupRecyclerView(buddy_list)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem) =
+ when (item.itemId) {
+ android.R.id.home -> {
+ NavUtils.navigateUpFromSameTask(this)
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+
+ private fun setupRecyclerView(recyclerView: RecyclerView) {
+ recyclerView.adapter = SimpleItemRecyclerViewAdapter(this, DummyContent.ITEMS, twoPane)
+ }
+
+ class SimpleItemRecyclerViewAdapter(
+ private val parentActivity: BuddyListActivity,
+ private val values: List,
+ private val twoPane: Boolean
+ ) :
+ RecyclerView.Adapter() {
+
+ private val onClickListener: View.OnClickListener
+
+ init {
+ onClickListener = View.OnClickListener { v ->
+ val item = v.tag as Friend
+ if (twoPane) {
+ val fragment = BuddyDetailFragment().apply {
+ arguments = Bundle().apply {
+ putString(BuddyDetailFragment.ARG_ITEM_ID, "friend_${item.id}")
+ }
+ }
+ parentActivity.supportFragmentManager
+ .beginTransaction()
+ .replace(R.id.buddy_detail_container, fragment)
+ .commit()
+ } else {
+ val intent = Intent(v.context, BuddyDetailActivity::class.java).apply {
+ putExtra(BuddyDetailFragment.ARG_ITEM_ID, "friend_${item.id}")
+ }
+ v.context.startActivity(intent)
+ }
+ }
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.buddy_list_content, parent, false)
+ return ViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val item = values[position]
+ holder.idView.text = item.name
+ holder.contentView.text = item.mapString()
+
+ with(holder.itemView) {
+ tag = item
+ setOnClickListener(onClickListener)
+ }
+ }
+
+ override fun getItemCount() = values.size
+
+ inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+ val idView: TextView = view.id_text
+ val contentView: TextView = view.content
+ }
+ }
+}
diff --git a/app/src/main/java/fr/plnech/dunbar/Extensions.kt b/app/src/main/java/fr/plnech/dunbar/Extensions.kt
index 0d3f6b5..35ae3eb 100644
--- a/app/src/main/java/fr/plnech/dunbar/Extensions.kt
+++ b/app/src/main/java/fr/plnech/dunbar/Extensions.kt
@@ -15,7 +15,8 @@ fun String.plural(count: Int = 1): String {
}
}
-fun Context.fetchFriends(): List {
+fun Context.fetchFriends(includeNot: Boolean = true): List {
+ // TODO: https://developer.android.com/training/permissions/requesting
val friends = mutableListOf()
//TODO: More efficient query using non-null parameters
@@ -42,7 +43,10 @@ fun Context.fetchFriends(): List {
it.close()
}
- return friends.sortedByDescending { it.timesContacted }
+ println("Fetched ${friends.size} friends.")
+
+ return (if (includeNot) friends else friends.filter { it.lastDate != null })
+ .sortedByDescending { it.timesContacted }
}
@@ -66,6 +70,5 @@ private fun Context.getFriendPhoto(id: Long): Bitmap? {
} catch (e: IOException) {
e.printStackTrace()
}
- println("Photo for $id: $photo")
return photo
}
\ No newline at end of file
diff --git a/app/src/main/java/fr/plnech/dunbar/dummy/DummyContent.kt b/app/src/main/java/fr/plnech/dunbar/dummy/DummyContent.kt
new file mode 100644
index 0000000..72503c6
--- /dev/null
+++ b/app/src/main/java/fr/plnech/dunbar/dummy/DummyContent.kt
@@ -0,0 +1,49 @@
+package fr.plnech.dunbar.dummy
+
+import android.provider.ContactsContract
+import fr.plnech.dunbar.model.Friend
+import java.util.*
+
+/**
+ * Helper class for providing sample content for user interfaces created by
+ * Android template wizards.
+ *
+ * TODO: Replace all uses of this class before publishing your app.
+ */
+object DummyContent {
+
+ /**
+ * An array of sample (dummy) items.
+ */
+ val ITEMS: MutableList = ArrayList()
+
+ /**
+ * A map of sample (dummy) items, by ID.
+ */
+ val ITEM_MAP: MutableMap = HashMap()
+
+ private val COUNT = 25
+
+ init {
+ // Add some sample items.
+ for (i in 1..COUNT) {
+ addItem(createFriend(i))
+ }
+ }
+
+ private fun addItem(item: Friend) {
+ ITEMS.add(item)
+ ITEM_MAP[item.id] = item
+ }
+
+ private fun createFriend(position: Int): Friend {
+ return Friend(
+ mapOf(
+ ContactsContract.Contacts._ID to "$position",
+ ContactsContract.Contacts.DISPLAY_NAME to "Friend $position",
+ "data1" to "01 23 45 67 89",
+ "details" to "$position: this friend has a lot of details about them. What do you think?"
+ )
+ )
+ }
+}
diff --git a/app/src/main/java/fr/plnech/dunbar/model/Friend.kt b/app/src/main/java/fr/plnech/dunbar/model/Friend.kt
index 4adabf4..21d9389 100644
--- a/app/src/main/java/fr/plnech/dunbar/model/Friend.kt
+++ b/app/src/main/java/fr/plnech/dunbar/model/Friend.kt
@@ -6,7 +6,7 @@ import android.net.Uri
import android.provider.ContactsContract.Contacts
import java.util.*
-data class Friend(val map: MutableMap, val photo: Bitmap?) {
+data class Friend(val map: Map = mutableMapOf(), val photo: Bitmap? = null) {
override fun toString(): String = "$name"
fun mapString(): String = map.entries.filter { !it.value.isNullOrEmpty() }.toString()
@@ -23,11 +23,12 @@ data class Friend(val map: MutableMap, val photo: Bitmap?) {
val phone: String?
get() = if (map[Contacts.HAS_PHONE_NUMBER] == "1") map["data1"] else null
- val lastTimeStamp = map[Contacts.LAST_TIME_CONTACTED]!!.toLong()
+ val lastTimeStamp = map[Contacts.LAST_TIME_CONTACTED]?.toLong() ?: 0
val lastDate: Date?
get() = if (lastTimeStamp > 0) Date(lastTimeStamp) else null
+ //FIXME: Broken on AndroidQ. Use messages / phone log
val timesContacted: Int
get() = map[Contacts.TIMES_CONTACTED]?.toInt() ?: 0
@@ -51,7 +52,5 @@ data class Friend(val map: MutableMap, val photo: Bitmap?) {
Intent(Intent.ACTION_DIAL, Uri.parse("tel:${it}"))
}
}
-
-
}
diff --git a/app/src/main/java/fr/plnech/dunbar/ui/FriendsActivity.kt b/app/src/main/java/fr/plnech/dunbar/ui/FriendsActivity.kt
index a4b1f70..4717ba7 100644
--- a/app/src/main/java/fr/plnech/dunbar/ui/FriendsActivity.kt
+++ b/app/src/main/java/fr/plnech/dunbar/ui/FriendsActivity.kt
@@ -1,5 +1,6 @@
package fr.plnech.dunbar.ui
+import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
@@ -7,6 +8,7 @@ import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NotificationManagerCompat
import androidx.recyclerview.widget.LinearLayoutManager
+import fr.plnech.dunbar.BuddyListActivity
import fr.plnech.dunbar.R
import fr.plnech.dunbar.data.Messages
import fr.plnech.dunbar.fetchFriends
@@ -74,6 +76,10 @@ class FriendsActivity : AppCompatActivity() {
// as you specify a parent activity in AndroidManifest.xml.
return when (item.itemId) {
R.id.action_settings -> true
+ R.id.action_buddies -> {
+ startActivity(Intent(this, BuddyListActivity::class.java))
+ true
+ }
else -> super.onOptionsItemSelected(item)
}
}
diff --git a/app/src/main/java/fr/plnech/dunbar/ui/FriendsAdapter.kt b/app/src/main/java/fr/plnech/dunbar/ui/FriendsAdapter.kt
index 456bd9e..b78dd63 100644
--- a/app/src/main/java/fr/plnech/dunbar/ui/FriendsAdapter.kt
+++ b/app/src/main/java/fr/plnech/dunbar/ui/FriendsAdapter.kt
@@ -69,9 +69,7 @@ class FriendsViewHolder(private val view: View) : RecyclerView.ViewHolder(view)
}
private fun bindPic(friend: Friend) {
- friend.photo?.let {
- pic.setImageBitmap(it)
- }
+ pic.setImageBitmap(friend.photo)
}
private fun bindPhone(friend: Friend) {
diff --git a/app/src/main/res/drawable/dunbar.jpg b/app/src/main/res/drawable/dunbar.jpg
new file mode 100644
index 0000000..3bfab2c
Binary files /dev/null and b/app/src/main/res/drawable/dunbar.jpg differ
diff --git a/app/src/main/res/layout-w900dp/buddy_list.xml b/app/src/main/res/layout-w900dp/buddy_list.xml
new file mode 100644
index 0000000..1a453ac
--- /dev/null
+++ b/app/src/main/res/layout-w900dp/buddy_list.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_buddy_detail.xml b/app/src/main/res/layout/activity_buddy_detail.xml
new file mode 100644
index 0000000..4cdf42d
--- /dev/null
+++ b/app/src/main/res/layout/activity_buddy_detail.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_buddy_list.xml b/app/src/main/res/layout/activity_buddy_list.xml
new file mode 100644
index 0000000..43cf072
--- /dev/null
+++ b/app/src/main/res/layout/activity_buddy_list.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/buddy_detail.xml b/app/src/main/res/layout/buddy_detail.xml
new file mode 100644
index 0000000..ef9a0c2
--- /dev/null
+++ b/app/src/main/res/layout/buddy_detail.xml
@@ -0,0 +1,10 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/buddy_list.xml b/app/src/main/res/layout/buddy_list.xml
new file mode 100644
index 0000000..1a28090
--- /dev/null
+++ b/app/src/main/res/layout/buddy_list.xml
@@ -0,0 +1,13 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/buddy_list_content.xml b/app/src/main/res/layout/buddy_list_content.xml
new file mode 100644
index 0000000..395ec3e
--- /dev/null
+++ b/app/src/main/res/layout/buddy_list_content.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/content_friends.xml b/app/src/main/res/layout/content_friends.xml
index 93351a0..ada0014 100644
--- a/app/src/main/res/layout/content_friends.xml
+++ b/app/src/main/res/layout/content_friends.xml
@@ -26,6 +26,7 @@
android:id="@+id/friendsList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:scrollbars="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml
index 3ca9b6b..8ef05c0 100644
--- a/app/src/main/res/menu/menu_main.xml
+++ b/app/src/main/res/menu/menu_main.xml
@@ -3,6 +3,11 @@
xmlns:tools="http://schemas.android.com/tools"
tools:context="fr.plnech.dunbar.ui.FriendsActivity">
+ -
16dp
+ 200dp
+ 200dp
+ 16dp
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 932fcfb..398d808 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -3,4 +3,7 @@
Dunbar
Updates about friends you don\'t want to forget
Settings
+ Buddies
+ Buddies
+ Buddy Detail