diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c6cbe56 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures diff --git a/README.md b/README.md deleted file mode 100644 index 0df3f1e..0000000 --- a/README.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: Build an Email Client Lab -type: lab -duration: "9:00" -creator: James Davis (NYC) ---- - -# ![](https://ga-dash.s3.amazonaws.com/production/assets/logo-9f88ae6c9c3871690e33280fcf557f33.png) Build an Email Client Lab - -## Exercise - -In this lab, you will be using [Google's Gmail API](https://developers.google.com/gmail/api/) to create an email app. - -The app should have an inbox, and a way of viewing the email's contents. The app should also account for tablet devices. - -You should take a look at the [Android Quickstart Guide](https://developers.google.com/gmail/api/quickstart/android) to get started. - -The requirement is to "make an email client app," but the design and functionality of the app is yours to choose! For instance, [Google's Inbox](https://www.google.com/inbox/) app is an email client, but added one-click functionality that makes it easy to empty the inbox of unnecessary emails. As long as you can view a list of a user's emails, and view the contents of the emails, then you can add whatever you would like. - -Note: We suggest using Google's Gmail API. However, points will not be taken away if you use another API to access a user's email. So, if you find another API that you find easier to use, or more straight forward, or more difficult if you are a masochist, then go ahead! - -#### Requirements - -Your email clients must show/do the following: - -* A list of emails in a user's inbox -* A screen for viewing the contents of an email - * ... which is seen when clicking an item in the list of emails -* On tablets, in landscape, both the master list of emails in the inbox and the details of a selected email should be visible. - * You should use a master/detail layout! -* Be able to compose and send emails -* Must use a class, like _Email.java_, that defines the email objects - -#### Bonuses - -Though not required, try to aim for the following goals: - -* Use material design :) -* Add a way to save drafts of emails -* Add a way to search emails -* Add functionality where you can select different accounts and view their emails - -If you add anything else you find to be spectacular and bonus-worthy, add it below and we'll evaluate it: - -* *Extra bonus 1* -* *Extra bonus 2* -* *Extra bonus 3* -* *etc.* - -#### Deliverable - -An Android Studio application that meets the above requirements. - -When making your pull requests, make sure the title contains your name. It makes it easier to search for us. - -##### Screenshots - -A few screenshots of the Gmail app: - - - - - - - -#### Resources - -* [Google's Gmail API - https://developers.google.com/gmail/api](https://developers.google.com/gmail/api/) diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..ffd6dc5 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,37 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.2" + + defaultConfig { + applicationId "com.boloutaredoubeni.emailapp" + minSdkVersion 16 + targetSdkVersion 23 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + dataBinding { + enabled = true + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + testCompile 'junit:junit:4.12' + compile 'com.android.support:appcompat-v7:23.2.0' + compile 'com.android.support:design:23.2.0' + compile 'com.google.android.gms:play-services-identity:8.4.0' + compile('com.google.api-client:google-api-client-android:1.20.0') { + exclude group: 'org.apache.httpcomponents' + } + compile('com.google.apis:google-api-services-gmail:v1-rev29-1.20.0') { + exclude group: 'org.apache.httpcomponents' + } +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..a965f92 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /usr/local/opt/android-sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/app/src/androidTest/java/com/boloutaredoubeni/emailapp/ApplicationTest.java b/app/src/androidTest/java/com/boloutaredoubeni/emailapp/ApplicationTest.java new file mode 100644 index 0000000..c66466f --- /dev/null +++ b/app/src/androidTest/java/com/boloutaredoubeni/emailapp/ApplicationTest.java @@ -0,0 +1,12 @@ +package com.boloutaredoubeni.emailapp; + +import android.app.Application; +import android.test.ApplicationTestCase; + +/** + * Testing + * Fundamentals + */ +public class ApplicationTest extends ApplicationTestCase { + public ApplicationTest() { super(Application.class); } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8c0f52e --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/boloutaredoubeni/emailapp/EmailApplication.java b/app/src/main/java/com/boloutaredoubeni/emailapp/EmailApplication.java new file mode 100644 index 0000000..744fc65 --- /dev/null +++ b/app/src/main/java/com/boloutaredoubeni/emailapp/EmailApplication.java @@ -0,0 +1,15 @@ +package com.boloutaredoubeni.emailapp; + +import android.app.Application; + +import com.google.api.services.gmail.GmailScopes; + +/** + * Copyright 2016 Boloutare Doubeni + */ +public class EmailApplication extends Application { + public static final String PREF_ACCOUNT_NAME = "accountName"; + private static final String[] SCOPES = {GmailScopes.GMAIL_COMPOSE, GmailScopes.GMAIL_READONLY}; + + public static String[] getScopes() { return SCOPES; } +} diff --git a/app/src/main/java/com/boloutaredoubeni/emailapp/activities/ComposeEmailActivity.java b/app/src/main/java/com/boloutaredoubeni/emailapp/activities/ComposeEmailActivity.java new file mode 100644 index 0000000..419b2f2 --- /dev/null +++ b/app/src/main/java/com/boloutaredoubeni/emailapp/activities/ComposeEmailActivity.java @@ -0,0 +1,117 @@ +package com.boloutaredoubeni.emailapp.activities; + +import android.content.Context; +import android.content.SharedPreferences; +import android.databinding.DataBindingUtil; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.view.View; +import android.widget.Toast; + +import com.boloutaredoubeni.emailapp.EmailApplication; +import com.boloutaredoubeni.emailapp.R; +import com.boloutaredoubeni.emailapp.databinding.ActivityComposeEmailBinding; +import com.boloutaredoubeni.emailapp.models.Email; +import com.boloutaredoubeni.emailapp.viewmodels.EmailViewModel; +import com.google.api.client.extensions.android.http.AndroidHttp; +import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.client.util.ExponentialBackOff; +import com.google.api.services.gmail.model.Message; + +import java.io.IOException; +import java.util.Arrays; + +/** + * An activity for composing emails + * This activity should navigable from the main activity and from a reply action + */ +public class ComposeEmailActivity extends AppCompatActivity { + + private GoogleAccountCredential mCredential; + private EmailViewModel mViewModel; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + SharedPreferences settings = getPreferences(Context.MODE_PRIVATE); + mCredential = + GoogleAccountCredential.usingOAuth2( + getApplicationContext(), + Arrays.asList(EmailApplication.getScopes())) + .setBackOff(new ExponentialBackOff()) + .setSelectedAccountName( + settings.getString(EmailApplication.PREF_ACCOUNT_NAME, null)); + + ActivityComposeEmailBinding binding = + DataBindingUtil.setContentView(this, R.layout.activity_compose_email); + mViewModel = new EmailViewModel(); + binding.setVm(mViewModel); + binding.emailEdit.addTextChangedListener(mViewModel.toFieldWatcher); + binding.subjectEdit.addTextChangedListener(mViewModel.subjectWatcher); + binding.bodyEdit.addTextChangedListener(mViewModel.bodyWatcher); + } + + public void onSendEmail(View v) { + Email email = mViewModel.emitEmail(); + if (email.isValid()) { + new SendEmailTask(mCredential).execute(email); + return; + } + Toast.makeText(this, "Something ain't right", Toast.LENGTH_SHORT).show(); + } + + /** + * Send a user email to the correct recipient, for now It should only support + * starting a new email thread + */ + public class SendEmailTask extends AsyncTask { + private com.google.api.services.gmail.Gmail mService = null; + private Exception mLastError = null; + + public SendEmailTask(GoogleAccountCredential credential) { + HttpTransport transport = AndroidHttp.newCompatibleTransport(); + JsonFactory jsonFactory = JacksonFactory.getDefaultInstance(); + mService = new com.google.api.services.gmail.Gmail.Builder(transport, + jsonFactory, + credential) + .setApplicationName("Email App") + .build(); + } + + @Override + protected Void doInBackground(Email... params) { + try { + sendEmail(params[0]); + } catch (IOException e) { + mLastError = e; + cancel(true); + mLastError.printStackTrace(); + } finally { + return null; + } + } + + @Override + protected void onPostExecute(Void aVoid) { + super.onPostExecute(aVoid); + Toast.makeText(ComposeEmailActivity.this, "Email sent!!!", + Toast.LENGTH_SHORT) + .show(); + } + + @Override + protected void onCancelled() { + // TODO: implement me + } + + private void sendEmail(Email email) throws IOException { + String user = "me"; + Message message = Email.createMessageFrom(email); + message = mService.users().messages().send(user, message).execute(); + } + } +} diff --git a/app/src/main/java/com/boloutaredoubeni/emailapp/activities/EmailDetailActivity.java b/app/src/main/java/com/boloutaredoubeni/emailapp/activities/EmailDetailActivity.java new file mode 100644 index 0000000..99514dd --- /dev/null +++ b/app/src/main/java/com/boloutaredoubeni/emailapp/activities/EmailDetailActivity.java @@ -0,0 +1,30 @@ +package com.boloutaredoubeni.emailapp.activities; + +import android.content.res.Configuration; +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; + +import com.boloutaredoubeni.emailapp.fragments.EmailDetailFragment; + +public class EmailDetailActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (getResources().getConfiguration().orientation == + Configuration.ORIENTATION_LANDSCAPE) { + finish(); + return; + } + + if (savedInstanceState == null) { + EmailDetailFragment fragment = new EmailDetailFragment(); + fragment.setArguments(getIntent().getExtras()); + getSupportFragmentManager() + .beginTransaction() + .add(android.R.id.content, fragment) + .commit(); + } + } +} diff --git a/app/src/main/java/com/boloutaredoubeni/emailapp/activities/MainActivity.java b/app/src/main/java/com/boloutaredoubeni/emailapp/activities/MainActivity.java new file mode 100644 index 0000000..fbf7012 --- /dev/null +++ b/app/src/main/java/com/boloutaredoubeni/emailapp/activities/MainActivity.java @@ -0,0 +1,213 @@ +package com.boloutaredoubeni.emailapp.activities; + +import android.accounts.AccountManager; +import android.app.Dialog; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.Bundle; +import android.support.design.widget.CoordinatorLayout; +import android.support.design.widget.FloatingActionButton; +import android.support.design.widget.Snackbar; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; +import android.support.v7.app.AppCompatActivity; +import android.util.Log; +import android.view.View; + +import com.boloutaredoubeni.emailapp.EmailApplication; +import com.boloutaredoubeni.emailapp.R; +import com.boloutaredoubeni.emailapp.fragments.EmailDetailFragment; +import com.boloutaredoubeni.emailapp.fragments.InboxFragment; +import com.boloutaredoubeni.emailapp.models.Email; +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.GooglePlayServicesUtil; +import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential; +import com.google.api.client.util.ExponentialBackOff; + +import java.util.Arrays; + +/** + * Copyright 2016 Boloutare Doubeni + * + * The main activity for our email client + */ +public class MainActivity + extends AppCompatActivity implements InboxFragment.OnEmailClickListener { + + public static final int REQUEST_ACCOUNT_PICKER = 1000; + public static final int REQUEST_AUTHORIZATION = 1001; + public static final int REQUEST_GOOGLE_PLAY_SERVICES = 1002; + + private GoogleAccountCredential mCredential; + + public GoogleAccountCredential getCredential() { return mCredential; } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + SharedPreferences settings = getPreferences(Context.MODE_PRIVATE); + mCredential = + GoogleAccountCredential.usingOAuth2( + getApplicationContext(), + Arrays.asList(EmailApplication.getScopes())) + .setBackOff(new ExponentialBackOff()) + .setSelectedAccountName( + settings.getString(EmailApplication.PREF_ACCOUNT_NAME, null)); + + setContentView(R.layout.activity_main); + + FloatingActionButton fab = (FloatingActionButton)findViewById(R.id.fab); + fab.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + // TODO: Start A create Email Activity + Intent intent = + new Intent(MainActivity.this, ComposeEmailActivity.class); + startActivity(intent); + } + }); + + FragmentManager manager = getSupportFragmentManager(); + FragmentTransaction transaction = manager.beginTransaction(); + InboxFragment inboxFragment = new InboxFragment(); + transaction.add(R.id.inbox_container, inboxFragment); + transaction.commit(); + } + + @Override + protected void onResume() { + super.onResume(); + if (isGooglePlayServicesAvailable()) { + refreshResults(); + } else { + CoordinatorLayout layout = + (CoordinatorLayout)findViewById(R.id.snackbar_layout); + Snackbar.make(layout, + "Google Play Services required: " + + "after installing, close and relaunch this app.", + Snackbar.LENGTH_LONG) + .show(); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, + Intent data) { + super.onActivityResult(requestCode, resultCode, data); + switch (requestCode) { + case REQUEST_GOOGLE_PLAY_SERVICES: + if (resultCode != RESULT_OK) { + isGooglePlayServicesAvailable(); + } + break; + case REQUEST_ACCOUNT_PICKER: + if (resultCode == RESULT_OK && data != null && data.getExtras() != null) { + String accountName = + data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME); + if (accountName != null) { + mCredential.setSelectedAccountName(accountName); + SharedPreferences settings = getPreferences(Context.MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(EmailApplication.PREF_ACCOUNT_NAME, accountName); + editor.apply(); + } + } else if (resultCode == RESULT_CANCELED) { + Log.d("Main Activity", "Account unspecified."); + } + break; + case REQUEST_AUTHORIZATION: + if (resultCode != RESULT_OK) { + chooseAccount(); + } + break; + } + + super.onActivityResult(requestCode, resultCode, data); + } + + @Override + public void onEmailSelected(Email email) { + // TODO: start the next fragment with the email data; + // if the screen is dual paned show the activity side by side + + Bundle args = new Bundle(); + args.putSerializable("Email", email); + if (isDualPaned()) { + // TODO: make sure the recycler highlights the proper item + FragmentManager fm = getSupportFragmentManager(); + FragmentTransaction ft = fm.beginTransaction(); + EmailDetailFragment detailFragment = new EmailDetailFragment(); + detailFragment.setArguments(args); + ft.replace(R.id.email_container, detailFragment); + ft.commit(); + } else { + Intent intent = new Intent(this, EmailDetailActivity.class); + intent.putExtras(args); + startActivity(intent); + } + } + + private boolean isGooglePlayServicesAvailable() { + final int connectionStatusCode = + GooglePlayServicesUtil.isGooglePlayServicesAvailable(this); + if (GooglePlayServicesUtil.isUserRecoverableError(connectionStatusCode)) { + showGooglePlayServicesAvailabilityErrorDialog(connectionStatusCode); + return false; + } else if (connectionStatusCode != ConnectionResult.SUCCESS) { + return false; + } + return true; + } + + public void showGooglePlayServicesAvailabilityErrorDialog( + final int connectionStatusCode) { + Dialog dialog = GooglePlayServicesUtil.getErrorDialog( + connectionStatusCode, this, MainActivity.REQUEST_GOOGLE_PLAY_SERVICES); + dialog.show(); + } + + /** + * Attempt to get a set of data from the Gmail API to display. If the + * email address isn't known yet, then call chooseAccount() method so the + * user can pick an account. + * + * Prompt the fragment to load the data + */ + private void refreshResults() { + if (mCredential.getSelectedAccountName() == null) { + chooseAccount(); + } + } + + /** + * Checks whether the device currently has a network connection. + * @return true if the device has a network connection, false otherwise. + */ + public boolean isDeviceOnline() { + ConnectivityManager connMgr = + (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = connMgr.getActiveNetworkInfo(); + return (networkInfo != null && networkInfo.isConnected()); + } + + /** + * Starts an activity in Google Play Services so the user can pick an + * account. + */ + private void chooseAccount() { + startActivityForResult(mCredential.newChooseAccountIntent(), + REQUEST_ACCOUNT_PICKER); + } + + /** + * There is a separate layout for large landscape devices where the the email + * container is present in the view by default + */ + public boolean isDualPaned() { + return findViewById(R.id.email_container) != null; + } +} diff --git a/app/src/main/java/com/boloutaredoubeni/emailapp/fragments/EmailDetailFragment.java b/app/src/main/java/com/boloutaredoubeni/emailapp/fragments/EmailDetailFragment.java new file mode 100644 index 0000000..435e203 --- /dev/null +++ b/app/src/main/java/com/boloutaredoubeni/emailapp/fragments/EmailDetailFragment.java @@ -0,0 +1,53 @@ +package com.boloutaredoubeni.emailapp.fragments; + +import android.databinding.DataBindingUtil; +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 com.boloutaredoubeni.emailapp.R; +import com.boloutaredoubeni.emailapp.databinding.FragmentEmailDetailBinding; +import com.boloutaredoubeni.emailapp.models.Email; + +/** + * Copyright 2016 Boloutare Doubeni + * + * A fragment that shows the detail of the email to the user + */ +public class EmailDetailFragment extends Fragment { + + private Email mEmail; + + public static EmailDetailFragment newInstance(int index) { + EmailDetailFragment fragment = new EmailDetailFragment(); + Bundle args = new Bundle(); + args.putInt(InboxFragment.EMAIL_POSITION, index); + fragment.setArguments(args); + + return fragment; + } + + public EmailDetailFragment() {} + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mEmail = (Email)getArguments().getSerializable("Email"); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + FragmentEmailDetailBinding binding = + FragmentEmailDetailBinding.inflate(inflater, container, false); + + if (mEmail != null) { + binding.setEmail(mEmail); + } + return binding.getRoot(); + } +} diff --git a/app/src/main/java/com/boloutaredoubeni/emailapp/fragments/InboxFragment.java b/app/src/main/java/com/boloutaredoubeni/emailapp/fragments/InboxFragment.java new file mode 100644 index 0000000..b8c18d1 --- /dev/null +++ b/app/src/main/java/com/boloutaredoubeni/emailapp/fragments/InboxFragment.java @@ -0,0 +1,222 @@ +package com.boloutaredoubeni.emailapp.fragments; + +import android.content.Context; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ProgressBar; +import android.widget.Toast; + +import com.boloutaredoubeni.emailapp.R; +import com.boloutaredoubeni.emailapp.activities.MainActivity; +import com.boloutaredoubeni.emailapp.models.Email; +import com.boloutaredoubeni.emailapp.views.adapters.InboxAdapter; +import com.boloutaredoubeni.emailapp.views.listeners.InboxItemClickedListener; +import com.google.api.client.extensions.android.http.AndroidHttp; +import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential; +import com.google.api.client.googleapis.extensions.android.gms.auth.GooglePlayServicesAvailabilityIOException; +import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.services.gmail.model.ListMessagesResponse; +import com.google.api.services.gmail.model.Message; + +import java.io.IOException; +import java.util.ArrayList; + +/** + * Copyright 2016 Boloutare Doubeni + * + * A fragment that shows the aggregation of recent emails + */ +public class InboxFragment extends Fragment { + + private OnEmailClickListener mListener; + private int mCurrentEmailPosition = 0; + static final String EMAIL_POSITION = "3m41l"; + + private RecyclerView mRecyclerView; + private InboxAdapter mAdapter; + private ProgressBar mProgressBar; + + public InboxFragment() {} + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_inbox, container, false); + mProgressBar = (ProgressBar)view.findViewById(R.id.progress_bar); + mRecyclerView = (RecyclerView)view.findViewById(R.id.inbox_recycler); + mRecyclerView.addOnItemTouchListener(new InboxItemClickedListener( + getContext(), new InboxItemClickedListener.OnItemClickListener() { + @Override + public void onItemClick(View view, int position) { + // TODO: Move to next fragment + Toast.makeText(getContext(), "Clicked on Item", Toast.LENGTH_SHORT) + .show(); + + mListener.onEmailSelected(mAdapter.getEmailAt(position)); + } + })); + return view; + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + mAdapter = new InboxAdapter(new ArrayList(), getContext()); + LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity()); + mRecyclerView.setAdapter(mAdapter); + mRecyclerView.setLayoutManager(layoutManager); + + if (((MainActivity)getActivity()).isDualPaned()) { + // TODO: indicate the selected item + // Something like listView.setChoiceMode() + // TODO: update the UI to show the last selected Email or the first if + // there is none or noe if there are no emails + } else { + } + } + + @Override + public void onResume() { + super.onResume(); + if (((MainActivity)getActivity()) + .getCredential() + .getSelectedAccountName() != null) { + if (((MainActivity)getActivity()).isDeviceOnline()) { + new MessageTask(((MainActivity)getActivity()).getCredential()) + .execute(); + } else { + Log.e("InboxFragment", "No network connection available."); + } + } + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + try { + mListener = (OnEmailClickListener)context; + } catch (ClassCastException ex) { + throw new ClassCastException(context.toString() + " must implement " + + OnEmailClickListener.class.getName()); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt(EMAIL_POSITION, mCurrentEmailPosition); + } + + @Override + public void onDetach() { + super.onDetach(); + mListener = null; + } + + /** + * FIXME: Retain emails in a database some how and check that first + * + * The activity that implements this interface must send the data to the + * detail activity + */ + public interface OnEmailClickListener { void onEmailSelected(Email email); } + + /** + * An AsyncTask that retrieves the data from the GMail API + */ + private class MessageTask extends AsyncTask> { + private com.google.api.services.gmail.Gmail mService = null; + private Exception mLastError = null; + + public MessageTask(GoogleAccountCredential credential) { + HttpTransport transport = AndroidHttp.newCompatibleTransport(); + JsonFactory jsonFactory = JacksonFactory.getDefaultInstance(); + mService = new com.google.api.services.gmail.Gmail.Builder(transport, + jsonFactory, + credential) + .setApplicationName("Email App") + .build(); + } + + @Override + protected ArrayList doInBackground(Void... params) { + try { + return getInboxMessages(); + } catch (IOException e) { + mLastError = e; + cancel(true); + return null; + } + } + + @Override + protected void onCancelled() { + // TODO: update the UI to reflect a cancelled request + if (mLastError != null) { + if (mLastError instanceof GooglePlayServicesAvailabilityIOException) { + ((MainActivity)getActivity()) + .showGooglePlayServicesAvailabilityErrorDialog( + ((GooglePlayServicesAvailabilityIOException)mLastError) + .getConnectionStatusCode()); + } else if (mLastError instanceof UserRecoverableAuthIOException) { + startActivityForResult( + ((UserRecoverableAuthIOException)mLastError).getIntent(), + MainActivity.REQUEST_AUTHORIZATION); + } else { + mLastError.printStackTrace(); + } + } else { + Log.e("InboxFragment", "getting inbox data cancelled"); + } + } + + @Override + protected void onPostExecute(ArrayList messages) { + super.onPostExecute(messages); + mProgressBar.setVisibility(View.GONE); + mAdapter.addMessages(messages); + } + + /** + * Get the email messages for the user's account + * + * @throws IOException + */ + private ArrayList getInboxMessages() throws IOException { + String user = "me"; + ArrayList messages = new ArrayList<>(); + ArrayList emails = new ArrayList<>(); + + // TODO: get emails with specific labels + // https://developers.google.com/gmail/api/v1/reference/users/messages/list + ListMessagesResponse response = mService.users() + .messages() + .list(user) + .setIncludeSpamTrash(false) + .execute(); + messages.addAll(response.getMessages()); + + for (final Message message : messages) { + Message msg = + mService.users().messages().get(user, message.getId()).execute(); + + Email email = Email.createFrom(msg); + emails.add(email); + } + + return emails; + } + } +} diff --git a/app/src/main/java/com/boloutaredoubeni/emailapp/models/Email.java b/app/src/main/java/com/boloutaredoubeni/emailapp/models/Email.java new file mode 100644 index 0000000..266b307 --- /dev/null +++ b/app/src/main/java/com/boloutaredoubeni/emailapp/models/Email.java @@ -0,0 +1,143 @@ +package com.boloutaredoubeni.emailapp.models; + +import android.databinding.BaseObservable; +import android.databinding.Bindable; + +import com.boloutaredoubeni.emailapp.BR; +import com.google.api.client.repackaged.org.apache.commons.codec.binary.Base64; +import com.google.api.services.gmail.model.Message; +import com.google.api.services.gmail.model.MessagePart; +import com.google.api.services.gmail.model.MessagePartBody; +import com.google.api.services.gmail.model.MessagePartHeader; + +import java.io.Serializable; +import java.util.List; + +/** + * Copyright 2016 Boloutare Doubeni + * + * A Thread of emails represented by a DoublyLinkedList + */ +public class Email extends BaseObservable implements Serializable { + + private static final long serialVersionUID = -6099312954099962806L; + + final private String mId; + private String mSnippet; + private String mFrom; + private String mDate; + private Email mNextEmailInThread; + private String mSubject; + private String mBody; + private Email mPreviousEmailInThread; + private boolean mValid; + + public Email(String id, String from, String date, Email previousEmail, + Email nextEmail, String subject, String body, String snippet) { + mId = id; + mFrom = from; + mDate = date; + mPreviousEmailInThread = previousEmail; + mNextEmailInThread = nextEmail; + mSubject = subject; + mBody = body; + mSnippet = snippet; + } + + /** + * Parse the email from GMail as an Email Object + * + * @param message A message object returned from the API call + * @return My representation of an Email message for the app + */ + public static Email createFrom(Message message) { + String id = message.getId(); + String snippet = message.getSnippet(); + MessagePart messagePart = message.getPayload(); + MessagePartBody messagePartBody = messagePart.getBody(); + String body = messagePartBody.getData(); + List messagePartHeaders = messagePart.getHeaders(); + String from = "", subject = "", date = ""; + for (MessagePartHeader header : messagePartHeaders) { + String name = header.getName().toLowerCase(); + switch (name) { + case "from": + from = header.getValue(); + break; + case "subject": + subject = header.getValue(); + break; + case "date": + date = header.getValue(); + break; + default: + continue; + } + } + + return new Email(id, from, date, null, null, subject, body, snippet); + } + + public static Message createMessageFrom(Email email) { + Message message = new Message(); + byte[] buffer = email.mBody.getBytes(); + String body = Base64.encodeBase64URLSafeString(buffer); + message.setRaw(body); + return message; + } + + public static Email defaultEmail() { + return new Email("", "", "", null, null, "", "", null); + } + + public String getID() { return mId; } + + @Bindable + public String getFrom() { + return mFrom; + } + + public void setFrom(String to) { + mFrom = to; + notifyPropertyChanged(BR.from); + } + + @Bindable + public String getDate() { + return mDate; + } + + public Email getNextEmail() { return mNextEmailInThread; } + + public Email getPrevEmail() { return mPreviousEmailInThread; } + + @Bindable + public String getSubject() { + return mSubject; + } + + public void setSubject(String subject) { + mSubject = subject; + notifyPropertyChanged(BR.subject); + } + + @Bindable + public String getBody() { + return mBody; + } + + public void setBody(String body) { + mBody = body; + notifyPropertyChanged(BR.body); + } + + @Bindable + public String getSnippet() { + return mSnippet; + } + + public boolean isValid() { + mValid = mFrom == null || !mFrom.isEmpty() ; + return mValid; + } +} diff --git a/app/src/main/java/com/boloutaredoubeni/emailapp/viewmodels/EmailViewModel.java b/app/src/main/java/com/boloutaredoubeni/emailapp/viewmodels/EmailViewModel.java new file mode 100644 index 0000000..7739fa9 --- /dev/null +++ b/app/src/main/java/com/boloutaredoubeni/emailapp/viewmodels/EmailViewModel.java @@ -0,0 +1,88 @@ +package com.boloutaredoubeni.emailapp.viewmodels; + +import android.databinding.ObservableField; +import android.text.Editable; +import android.text.TextWatcher; + +import com.boloutaredoubeni.emailapp.models.Email; + +/** + * Copyright 2016 Boloutare Doubeni + */ +public class EmailViewModel { + + public EmailViewModel() { + to = new ObservableField<>(); + to.set(""); + + subject = new ObservableField<>(); + subject.set(""); + + body = new ObservableField<>(); + body.set(""); + } + + public ObservableField to; + public TextWatcher toFieldWatcher = new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + + } + + @Override + public void afterTextChanged(Editable s) { + if (!to.get().equals(s.toString())) { + to.set(s.toString()); + } + } + }; + + public ObservableField subject; + public TextWatcher subjectWatcher = new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + + } + + @Override + public void afterTextChanged(Editable s) { + if (!subject.get().equals(s.toString())) { + subject.set(s.toString()); + } + } + }; + + public ObservableField body; + public TextWatcher bodyWatcher = new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + + } + + @Override + public void afterTextChanged(Editable s) { + if (!body.get().equals(s.toString())) { + body.set(s.toString()); + } + } + }; + + public Email emitEmail() { + return new Email(null, to.get(), null, null, null, subject.get(), body.get(), null); + } +} diff --git a/app/src/main/java/com/boloutaredoubeni/emailapp/views/adapters/InboxAdapter.java b/app/src/main/java/com/boloutaredoubeni/emailapp/views/adapters/InboxAdapter.java new file mode 100644 index 0000000..f9cf979 --- /dev/null +++ b/app/src/main/java/com/boloutaredoubeni/emailapp/views/adapters/InboxAdapter.java @@ -0,0 +1,66 @@ +package com.boloutaredoubeni.emailapp.views.adapters; + +import android.content.Context; +import android.databinding.DataBindingUtil; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.boloutaredoubeni.emailapp.R; +import com.boloutaredoubeni.emailapp.databinding.MessageItemBinding; +import com.boloutaredoubeni.emailapp.models.Email; + +import java.util.ArrayList; + +/** + * Copyright 2016 Boloutare Doubeni + */ +public class InboxAdapter + extends RecyclerView.Adapter { + + private Context mContext; + private ArrayList mEmails; + + public InboxAdapter(ArrayList messageList, Context context) { + mContext = context; + mEmails = messageList; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + View itemView = inflater.inflate(R.layout.message_item, null); + return new ViewHolder(itemView); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + Email email = mEmails.get(position); + holder.getBinding().setEmail(email); + holder.getBinding().executePendingBindings(); + } + + @Override + public int getItemCount() { + return mEmails.size(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + private MessageItemBinding mBinding; + + public ViewHolder(View view) { + super(view); + mBinding = DataBindingUtil.bind(view); + } + + public MessageItemBinding getBinding() { return mBinding; } + } + + public void addMessages(ArrayList messages) { + mEmails.addAll(messages); + notifyDataSetChanged(); + } + + public Email getEmailAt(int position) { return mEmails.get(position); } +} diff --git a/app/src/main/java/com/boloutaredoubeni/emailapp/views/listeners/InboxItemClickedListener.java b/app/src/main/java/com/boloutaredoubeni/emailapp/views/listeners/InboxItemClickedListener.java new file mode 100644 index 0000000..cb568d6 --- /dev/null +++ b/app/src/main/java/com/boloutaredoubeni/emailapp/views/listeners/InboxItemClickedListener.java @@ -0,0 +1,71 @@ +package com.boloutaredoubeni.emailapp.views.listeners; + +import android.content.Context; +import android.support.v7.widget.RecyclerView; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; + +/** + * Copyright 2016 Boloutare Doubeni + */ +public class InboxItemClickedListener + implements RecyclerView.OnItemTouchListener { + + private OnItemClickListener mListener; + private GestureDetector mDetector; + + public InboxItemClickedListener(Context context, + OnItemClickListener listener) { + mListener = listener; + mDetector = + new GestureDetector(context, new GestureDetector.OnGestureListener() { + @Override + public boolean onDown(MotionEvent e) { + return false; + } + + @Override + public void onShowPress(MotionEvent e) {} + + @Override + public boolean onSingleTapUp(MotionEvent e) { + return true; + } + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, + float distanceX, float distanceY) { + return false; + } + + @Override + public void onLongPress(MotionEvent e) {} + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, + float velocityX, float velocityY) { + return false; + } + }); + } + + @Override + public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { + View childView = rv.findChildViewUnder(e.getX(), e.getY()); + if (childView != null && mListener != null && mDetector.onTouchEvent(e)) { + mListener.onItemClick(childView, rv.getChildAdapterPosition(childView)); + } + return false; + } + + @Override + public void onTouchEvent(RecyclerView rv, MotionEvent e) {} + + @Override + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {} + + public interface OnItemClickListener { + void onItemClick(View view, int position); + } +} diff --git a/app/src/main/res/layout-land/content_main.xml b/app/src/main/res/layout-land/content_main.xml new file mode 100644 index 0000000..026a281 --- /dev/null +++ b/app/src/main/res/layout-land/content_main.xml @@ -0,0 +1,25 @@ + + + + + + diff --git a/app/src/main/res/layout/activity_compose_email.xml b/app/src/main/res/layout/activity_compose_email.xml new file mode 100644 index 0000000..b848d95 --- /dev/null +++ b/app/src/main/res/layout/activity_compose_email.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + +