diff --git a/Confirmation of Submission.txt b/Confirmation of Submission.txt new file mode 100644 index 0000000..23e878d --- /dev/null +++ b/Confirmation of Submission.txt @@ -0,0 +1,21 @@ +Dear Sales-i + +The following project contains my submitted code for the requirements specified within your github page +(https://github.com/sales-i/android-coding-test). + +I just wanted to affirm and let you know that I implemented as many of the requirements as I could +within the two hour time limit, unfortunately I couldn't implement them all. + +I didn't get a chance to implement tasks iV and V. + +If I did have enough time I would have done the following: + +*Implement and injected the RuntimePermissions class +*Added the necessary permissions within the manifest +*Checked that the device running the app could make phone calls and had an network connection +*Check if they had permission granted, and if not request them. +*Store the granted permission within the onActivityResult callback + +Looking forward to hearing from you. + +James \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6ed188b..f367e34 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/java/com/salesi/coding/DetailsActivity.java b/app/src/main/java/com/salesi/coding/DetailsActivity.java new file mode 100644 index 0000000..4b1c962 --- /dev/null +++ b/app/src/main/java/com/salesi/coding/DetailsActivity.java @@ -0,0 +1,46 @@ +package com.salesi.coding; + +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; + +import com.salesi.coding.entity.ContactEntity; +import com.salesi.coding.ui.Fragments.DetailFragment; +import com.salesi.coding.ui.screens.FContacts; + +import butterknife.Bind; +import butterknife.ButterKnife; + +/** + * Created by buxtonj on 05/11/2017. + */ + +public class DetailsActivity extends AppCompatActivity { + @Bind(R.id.toolbar) protected Toolbar mToolbar; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_detail); + + ButterKnife.bind(this); + + setSupportActionBar(mToolbar); + + if (savedInstanceState == null) { + // Create the detail fragment and add it to the activity + // using a fragment transaction. + + ContactEntity fContacts = (ContactEntity) getIntent().getSerializableExtra("contact"); + + DetailFragment fragment = new DetailFragment(); + fragment.setData(fContacts); + + getSupportFragmentManager().beginTransaction() + .add(R.id.contact_detail_container, fragment, MainActivity.DETAILFRAGMENT_TAG) + .commit(); + } + } +} diff --git a/app/src/main/java/com/salesi/coding/MainActivity.java b/app/src/main/java/com/salesi/coding/MainActivity.java index d091985..e5ec476 100644 --- a/app/src/main/java/com/salesi/coding/MainActivity.java +++ b/app/src/main/java/com/salesi/coding/MainActivity.java @@ -5,7 +5,10 @@ import android.support.v4.view.ViewPager; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; +import android.view.Menu; +import android.view.MenuItem; +import com.salesi.coding.ui.Fragments.DetailFragment; import com.salesi.coding.ui.adapter.TabsAdapter; import butterknife.Bind; @@ -16,6 +19,9 @@ public class MainActivity extends AppCompatActivity { @Bind(R.id.view_pager) protected ViewPager mViewPager; @Bind(R.id.toolbar) protected Toolbar mToolbar; + public static final String DETAILFRAGMENT_TAG = "DFTAG"; + public static final String CONTACTFRAGMENT_TAG = "CFTAG"; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -26,8 +32,34 @@ protected void onCreate(Bundle savedInstanceState) { setSupportActionBar(mToolbar); getSupportActionBar().setLogo(R.mipmap.ic_launcher); + if (findViewById(R.id.contact_detail_container) != null) { + if (savedInstanceState == null) { + getSupportFragmentManager().beginTransaction() + .replace(R.id.contact_detail_container, new DetailFragment(), DETAILFRAGMENT_TAG) + .commit(); + } + } + TabsAdapter adapter = new TabsAdapter(getSupportFragmentManager(), getApplicationContext()); mViewPager.setAdapter(adapter); mTabLayout.setupWithViewPager(mViewPager); } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.main_activity, menu); + + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + + if (id == R.id.close_app_option) { + finish(); + } + + return super.onOptionsItemSelected(item); + } } diff --git a/app/src/main/java/com/salesi/coding/entity/ContactEntity.java b/app/src/main/java/com/salesi/coding/entity/ContactEntity.java index 6334c88..51b8404 100644 --- a/app/src/main/java/com/salesi/coding/entity/ContactEntity.java +++ b/app/src/main/java/com/salesi/coding/entity/ContactEntity.java @@ -2,7 +2,10 @@ import com.google.gson.annotations.Expose; +import java.io.Serializable; +import java.security.spec.PSSParameterSpec; import java.util.List; +import java.util.Locale; /** * Contact Entity @@ -11,9 +14,58 @@ * Copyright © 2017 sales­i */ -public class ContactEntity { +public class ContactEntity implements Serializable{ @Expose public Integer ContactID; @Expose public String Title; - @Expose public String FirstName; + @Expose public String FirstNane; @Expose public String LastName; + @Expose public String JobTitle; + @Expose public String PhoneNumber; + @Expose public String Email; + + /** + * Address : {"Address1":"Marine Drive","Address2":"","Address3":"Clove Lane","Address4":"","Town":"Solihull","County":"","Postcode":"","Country":"CCV23A"} + * Hobbies : ["table-tennis","badminton"] + */ + + @Expose + public AddressBean Address; + @Expose + public List Hobbies; + + public static class AddressBean implements Serializable { + /** + * Address1 : Marine Drive + * Address2 : + * Address3 : Clove Lane + * Address4 : + * Town : Solihull + * County : + * Postcode : + * Country : CCV23A + */ + + @Expose + public String Address1; + @Expose + public String Address2; + @Expose + public String Address3; + @Expose + public String Address4; + @Expose + public String Town; + @Expose + public String County; + @Expose + public String Postcode; + @Expose + public String Country; + + @Override + public String toString() { + return String.format(Locale.UK, "%s, %s, %s, %s, %s, %s, %s, %s", + Address1, Address2, Address3, Address4, Town, County,Postcode, Country); + } + } } diff --git a/app/src/main/java/com/salesi/coding/ui/Fragments/DetailFragment.java b/app/src/main/java/com/salesi/coding/ui/Fragments/DetailFragment.java new file mode 100644 index 0000000..363255b --- /dev/null +++ b/app/src/main/java/com/salesi/coding/ui/Fragments/DetailFragment.java @@ -0,0 +1,78 @@ +package com.salesi.coding.ui.Fragments; + +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.salesi.coding.R; +import com.salesi.coding.entity.ContactEntity; +import com.salesi.coding.ui.screens.FContacts; + +import java.util.Locale; + +import static com.salesi.coding.MainActivity.CONTACTFRAGMENT_TAG; + +/** + * Created by buxtonj on 05/11/2017. + */ + +public class DetailFragment extends Fragment { + + TextView contactName, jobTitle, phoneNumber, email, address, hobbies; + private ContactEntity contact; + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + + + View rootView = inflater.inflate(R.layout.fragment_detail, container, false); + contactName = (TextView) rootView.findViewById(R.id.contact_name); + jobTitle = (TextView) rootView.findViewById(R.id.jobTitle); + phoneNumber = (TextView) rootView.findViewById(R.id.phoneNumber); + email = (TextView) rootView.findViewById(R.id.email); + address = (TextView) rootView.findViewById(R.id.adress); + hobbies = (TextView) rootView.findViewById(R.id.hobbies); + + if (contact != null && rootView.findViewById(R.id.contacts_frag_container) != null) { + if (savedInstanceState == null) { + getActivity().getSupportFragmentManager().beginTransaction() + .replace(R.id.contacts_frag_container, FContacts.instance(), CONTACTFRAGMENT_TAG) + .commit(); + } + } + + return rootView; + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + if (contact != null) { + contactName.setText(String.format(Locale.UK, "%s %s %s", contact.Title, contact.FirstNane, contact.LastName)); + jobTitle.setText(contact.JobTitle); + phoneNumber.setText(contact.PhoneNumber); + email.setText(contact.Email); + + + if (contact.Address != null) + address.setText(contact.Address.toString()); + + if (contact.Hobbies != null) + hobbies.setText(contact.Hobbies.toString()); + } + } + + public void setData(ContactEntity contacts) { + this.contact = contacts; + } + + public ContactEntity getContact() { + return contact; + } +} diff --git a/app/src/main/java/com/salesi/coding/ui/adapter/ContactsAdapter.java b/app/src/main/java/com/salesi/coding/ui/adapter/ContactsAdapter.java index c458668..93a5e64 100644 --- a/app/src/main/java/com/salesi/coding/ui/adapter/ContactsAdapter.java +++ b/app/src/main/java/com/salesi/coding/ui/adapter/ContactsAdapter.java @@ -5,17 +5,22 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.AdapterView; import android.widget.TextView; +import android.widget.Toast; +import com.salesi.coding.MainApp; import com.salesi.coding.R; import com.salesi.coding.entity.ContactEntity; import java.util.List; +import java.util.Locale; import javax.inject.Inject; import butterknife.Bind; import butterknife.ButterKnife; +import butterknife.OnClick; /** * Contacts view adapter @@ -24,12 +29,20 @@ */ public class ContactsAdapter extends RecyclerView.Adapter { private List mContacts; + private OnItemClickListener listener; + + public interface OnItemClickListener { + void onItemClick(ContactEntity item); + } + @Inject - public ContactsAdapter() {} + public ContactsAdapter() { + } - public void setData(List contacts) { - mContacts = contacts; + public void setData(List contacts, OnItemClickListener listener) { + this.mContacts = contacts; + this.listener = listener; } @Override @@ -41,7 +54,7 @@ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { @Override public void onBindViewHolder(ViewHolder holder, int position) { - holder.bind(mContacts.get(position)); + holder.bind(mContacts.get(position), listener); } @Override @@ -53,14 +66,32 @@ class ViewHolder extends RecyclerView.ViewHolder { @Nullable @Bind(R.id.contact_id) protected TextView mId; @Nullable @Bind(R.id.contact_name) protected TextView mName; + @OnClick(R.id.phone) + void callContact() { + //TODO Implement Call feature + Toast.makeText(itemView.getContext(), "Calling contact...", Toast.LENGTH_SHORT).show(); + } + + @OnClick(R.id.email) + void emailContact() { + //TODO Implement Email feature + Toast.makeText(itemView.getContext(), "Emailing contact...", Toast.LENGTH_SHORT).show(); + } + public ViewHolder(View itemView) { super(itemView); ButterKnife.bind(this, itemView); } - public void bind(ContactEntity entity) { - mId.setText(entity.ContactID); - mName.setText(entity.FirstName+" "+entity.LastName); + public void bind(final ContactEntity entity, final OnItemClickListener listener) { + mId.setText(String.valueOf(entity.ContactID)); + mName.setText(String.format(Locale.UK, "%1$s %2$s", entity.FirstNane, entity.LastName)); + itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + listener.onItemClick(entity); + } + }); } } } diff --git a/app/src/main/java/com/salesi/coding/ui/adapter/TabsAdapter.java b/app/src/main/java/com/salesi/coding/ui/adapter/TabsAdapter.java index 4bbb33e..e01b323 100644 --- a/app/src/main/java/com/salesi/coding/ui/adapter/TabsAdapter.java +++ b/app/src/main/java/com/salesi/coding/ui/adapter/TabsAdapter.java @@ -24,9 +24,8 @@ public TabsAdapter(final FragmentManager fm, final Context context) { @Override public Fragment getItem(int position) { switch(position) { - case 0 : { + case 0 : return FContacts.instance(); - } } return null; } @@ -34,9 +33,8 @@ public Fragment getItem(int position) { @Override public CharSequence getPageTitle(int position) { switch(position) { - case 0 : { + case 0 : return mContext.getString(R.string.title_tab_question_1); - } } return null; } diff --git a/app/src/main/java/com/salesi/coding/ui/screens/FContacts.java b/app/src/main/java/com/salesi/coding/ui/screens/FContacts.java index 066c80e..ba5cd0d 100644 --- a/app/src/main/java/com/salesi/coding/ui/screens/FContacts.java +++ b/app/src/main/java/com/salesi/coding/ui/screens/FContacts.java @@ -1,7 +1,9 @@ package com.salesi.coding.ui.screens; import android.content.Context; +import android.content.Intent; import android.os.Bundle; +import android.os.CpuUsageInfo; import android.os.StrictMode; import android.support.v4.app.Fragment; import android.support.v7.widget.DefaultItemAnimator; @@ -11,13 +13,20 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.Toast; +import com.salesi.coding.DetailsActivity; +import com.salesi.coding.MainActivity; import com.salesi.coding.MainApp; import com.salesi.coding.R; import com.salesi.coding.entity.ContactEntity; import com.salesi.coding.service.IContactService; +import com.salesi.coding.ui.Fragments.DetailFragment; import com.salesi.coding.ui.adapter.ContactsAdapter; +import com.salesi.coding.ui.adapter.ContactsAdapter.OnItemClickListener; +import com.salesi.coding.utils.Utils; +import java.util.ArrayList; import java.util.List; import javax.inject.Inject; @@ -28,15 +37,22 @@ /** * Fragment matching first tab - * + *

* Copyright © 2017 sales­i */ public class FContacts extends Fragment { - @Inject protected Lazy mContactService; - @Inject protected Lazy mAdapter; + @Inject + protected Lazy mContactService; + @Inject + protected Lazy mAdapter; - @Bind(R.id.list_contacts) protected RecyclerView mRecycler; + @Bind(R.id.list_contacts) + protected RecyclerView mRecycler; + + private ContactEntity selectedContact; + + DetailFragment detailsFrag; public static FContacts instance() { return new FContacts(); @@ -54,19 +70,28 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa ButterKnife.bind(this, view); StrictMode.ThreadPolicy strictPolicy = new StrictMode.ThreadPolicy.Builder() - .permitAll().build(); + .permitAll().build(); StrictMode.setThreadPolicy(strictPolicy); getActivity().runOnUiThread(new Runnable() { @Override public void run() { - List contacts = mContactService.get().fetchContacts(); + + detailsFrag = ((DetailFragment) getActivity().getSupportFragmentManager().findFragmentByTag(MainActivity.DETAILFRAGMENT_TAG)); + + if (detailsFrag != null) { + selectedContact = detailsFrag.getContact(); + } + + List contacts = (selectedContact != null) + ? filterContactData(selectedContact, mContactService.get().fetchContacts()) + : mContactService.get().fetchContacts(); mRecycler.setLayoutManager(new LinearLayoutManager(getActivity())); mRecycler.addItemDecoration(new DividerItemDecoration(getActivity(), LinearLayoutManager.VERTICAL)); mRecycler.setItemAnimator(new DefaultItemAnimator()); - mAdapter.get().setData(contacts); + mAdapter.get().setData(contacts, onItemClickListener); mRecycler.setAdapter(mAdapter.get()); } }); @@ -74,6 +99,63 @@ public void run() { return view; } + private List filterContactData(ContactEntity selectedContact, List contactEntities) { + List filteredDataList = new ArrayList<>(); + + if (selectedContact != null && (selectedContact.Hobbies != null && selectedContact.Hobbies.size() > 0)) { + for (ContactEntity contactEntity : contactEntities) { + + // DOn't add selected contact or contact we've already got + if (contactEntity.ContactID.equals(selectedContact.ContactID) || filteredDataList.contains(contactEntity)) { + continue; + } + + if (contactEntity.Hobbies != null) { + for (String contactHobby : contactEntity.Hobbies) { + if (Utils.containsIgnoreCase(selectedContact.Hobbies, contactHobby)) { + filteredDataList.add(contactEntity); + break; + } + + } + } + } + } + + return filteredDataList; + } + + + + public OnItemClickListener onItemClickListener = new OnItemClickListener() { + @Override + public void onItemClick(ContactEntity item) { + selectedContact = item; + + // If I had more time, I would instead save all the data into local storage, pass across the id, + // and then retrieve the data from the database using a DAO + // Because this way could cause a transaction too large exception + displayDetails(item); + } + }; + + private void displayDetails(ContactEntity contactEntity) { + if (getActivity().findViewById(R.id.contact_detail_container) != null) { + + DetailFragment fragment = new DetailFragment(); + fragment.setData(contactEntity); + + getActivity().getSupportFragmentManager().beginTransaction() + .replace(R.id.contact_detail_container, fragment, MainActivity.DETAILFRAGMENT_TAG) + .commit(); + + } else { + Intent intent = new Intent(getActivity(), DetailsActivity.class); + intent.putExtra("contact", contactEntity); + startActivity(intent); + } + } + @Override public void onDestroyView() { super.onDestroyView(); diff --git a/app/src/main/java/com/salesi/coding/utils/Utils.java b/app/src/main/java/com/salesi/coding/utils/Utils.java new file mode 100644 index 0000000..97020f4 --- /dev/null +++ b/app/src/main/java/com/salesi/coding/utils/Utils.java @@ -0,0 +1,19 @@ +package com.salesi.coding.utils; + +import java.util.List; + +/** + * Created by buxtonj on 05/11/2017. + */ + +public class Utils { + public static boolean containsIgnoreCase(List array, String item) { + for(int i = 0; i < array.size(); i++) { + if (array.get(i).toString().equalsIgnoreCase(item)) { + return true; + } + } + + return false; + } +} diff --git a/app/src/main/res/layout-large/activity_main.xml b/app/src/main/res/layout-large/activity_main.xml new file mode 100644 index 0000000..db75f2c --- /dev/null +++ b/app/src/main/res/layout-large/activity_main.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_detail.xml b/app/src/main/res/layout/activity_detail.xml new file mode 100644 index 0000000..f3da6c8 --- /dev/null +++ b/app/src/main/res/layout/activity_detail.xml @@ -0,0 +1,31 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_detail.xml b/app/src/main/res/layout/fragment_detail.xml new file mode 100644 index 0000000..b94ea5e --- /dev/null +++ b/app/src/main/res/layout/fragment_detail.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_contact_row_item.xml b/app/src/main/res/layout/layout_contact_row_item.xml index a3118bf..bde8ab5 100644 --- a/app/src/main/res/layout/layout_contact_row_item.xml +++ b/app/src/main/res/layout/layout_contact_row_item.xml @@ -2,70 +2,70 @@ + android:textStyle="normal|bold" + tools:targetApi="ICE_CREAM_SANDWICH" /> + app:srcCompat="@android:drawable/sym_action_email" + tools:targetApi="ICE_CREAM_SANDWICH" /> \ No newline at end of file diff --git a/app/src/main/res/menu/main_activity.xml b/app/src/main/res/menu/main_activity.xml new file mode 100644 index 0000000..dea54d2 --- /dev/null +++ b/app/src/main/res/menu/main_activity.xml @@ -0,0 +1,9 @@ + +

+ + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e30960f..f6b2fcc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,4 +2,10 @@ CodingTest http://www.mocky.io/ Contacts + + + 885 + Arriva PLC + + Close App