diff --git a/app/build.gradle b/app/build.gradle index dfb906d48..bdaa46d09 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,4 +1,6 @@ apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' android { compileSdkVersion rootProject.ext.compileSdkVersion @@ -22,11 +24,15 @@ android { } // If you need to add more flavors, consider using flavor dimensions. + flavorDimensions "flavor" productFlavors { mock { + dimension "flavor" + applicationIdSuffix = ".mock" } prod { + dimension "flavor" } } @@ -52,6 +58,9 @@ android { all versions in a single place. This improves readability and helps managing project complexity. */ dependencies { + // Kotlin + compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" + // App's dependencies, including test compile "com.android.support:appcompat-v7:$rootProject.supportLibraryVersion" compile "com.android.support:cardview-v7:$rootProject.supportLibraryVersion" diff --git a/app/src/androidTestMock/java/com/example/android/testing/notes/notedetail/NoteDetailScreenTest.java b/app/src/androidTestMock/java/com/example/android/testing/notes/notedetail/NoteDetailScreenTest.java index e28d94803..450826ae3 100644 --- a/app/src/androidTestMock/java/com/example/android/testing/notes/notedetail/NoteDetailScreenTest.java +++ b/app/src/androidTestMock/java/com/example/android/testing/notes/notedetail/NoteDetailScreenTest.java @@ -92,7 +92,7 @@ public void intentWithStubbedNoteId() { // Lazily start the Activity from the ActivityTestRule this time to inject the start Intent Intent startIntent = new Intent(); - startIntent.putExtra(NoteDetailActivity.EXTRA_NOTE_ID, NOTE.getId()); + startIntent.putExtra(NoteDetailActivity.Companion.getEXTRA_NOTE_ID(), NOTE.getId()); mNoteDetailActivityTestRule.launchActivity(startIntent); registerIdlingResource(); diff --git a/app/src/main/java/com/example/android/testing/notes/addnote/AddNoteActivity.java b/app/src/main/java/com/example/android/testing/notes/addnote/AddNoteActivity.java deleted file mode 100644 index 17759a48e..000000000 --- a/app/src/main/java/com/example/android/testing/notes/addnote/AddNoteActivity.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2015, The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.android.testing.notes.addnote; - -import com.example.android.testing.notes.R; -import com.example.android.testing.notes.util.EspressoIdlingResource; - -import android.os.Bundle; -import android.support.annotation.VisibleForTesting; -import android.support.test.espresso.IdlingResource; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentTransaction; -import android.support.v7.app.ActionBar; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; - -/** - * Displays an add note screen. - */ -public class AddNoteActivity extends AppCompatActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_addnote); - - // Set up the toolbar. - Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - ActionBar actionBar = getSupportActionBar(); - actionBar.setTitle(R.string.add_note); - actionBar.setDisplayHomeAsUpEnabled(true); - actionBar.setDisplayShowHomeEnabled(true); - - if (null == savedInstanceState) { - initFragment(AddNoteFragment.newInstance()); - } - } - - @Override - public boolean onSupportNavigateUp() { - onBackPressed(); - return true; - } - - private void initFragment(Fragment detailFragment) { - // Add the AddNoteFragment to the layout - FragmentManager fragmentManager = getSupportFragmentManager(); - FragmentTransaction transaction = fragmentManager.beginTransaction(); - transaction.add(R.id.contentFrame, detailFragment); - transaction.commit(); - } - - @VisibleForTesting - public IdlingResource getCountingIdlingResource() { - return EspressoIdlingResource.getIdlingResource(); - } -} diff --git a/app/src/main/java/com/example/android/testing/notes/addnote/AddNoteActivity.kt b/app/src/main/java/com/example/android/testing/notes/addnote/AddNoteActivity.kt new file mode 100644 index 000000000..bdd2025cd --- /dev/null +++ b/app/src/main/java/com/example/android/testing/notes/addnote/AddNoteActivity.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2015, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.testing.notes.addnote + +import android.os.Bundle +import android.support.annotation.VisibleForTesting +import android.support.test.espresso.IdlingResource +import android.support.v4.app.Fragment +import android.support.v7.app.AppCompatActivity +import com.example.android.testing.notes.R +import com.example.android.testing.notes.util.EspressoIdlingResource +import kotlinx.android.synthetic.main.activity_addnote.* + +/** + * Displays an add note screen. + */ +class AddNoteActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_addnote) + + // Set up the toolbar. + setSupportActionBar(toolbar) + supportActionBar?.run { + setTitle(R.string.add_note) + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + if (null == savedInstanceState) { + initFragment(AddNoteFragment.newInstance()) + } + } + + override fun onSupportNavigateUp(): Boolean { + onBackPressed() + return true + } + + private fun initFragment(detailFragment: Fragment) { + // Add the AddNoteFragment to the layout + supportFragmentManager + .beginTransaction() + .add(R.id.contentFrame, detailFragment) + .commit() + } + + val countingIdlingResource: IdlingResource + @VisibleForTesting + get() = EspressoIdlingResource.idlingResource +} diff --git a/app/src/main/java/com/example/android/testing/notes/notedetail/NoteDetailContract.java b/app/src/main/java/com/example/android/testing/notes/addnote/AddNoteContract.kt similarity index 61% rename from app/src/main/java/com/example/android/testing/notes/notedetail/NoteDetailContract.java rename to app/src/main/java/com/example/android/testing/notes/addnote/AddNoteContract.kt index 0d09f14ce..ef7fead86 100644 --- a/app/src/main/java/com/example/android/testing/notes/notedetail/NoteDetailContract.java +++ b/app/src/main/java/com/example/android/testing/notes/addnote/AddNoteContract.kt @@ -14,36 +14,37 @@ * limitations under the License. */ -package com.example.android.testing.notes.notedetail; +package com.example.android.testing.notes.addnote -import android.support.annotation.Nullable; +import java.io.IOException /** * This specifies the contract between the view and the presenter. */ -public interface NoteDetailContract { +interface AddNoteContract { interface View { - void setProgressIndicator(boolean active); + fun showEmptyNoteError() - void showMissingNote(); + fun showNotesList() - void hideTitle(); + fun openCamera(saveTo: String) - void showTitle(String title); + fun showImagePreview(uri: String) - void showImage(String imageUrl); + fun showImageError() + } - void hideImage(); + interface UserActionsListener { - void hideDescription(); + fun saveNote(title: String, description: String) - void showDescription(String description); - } + @Throws(IOException::class) + fun takePicture() - interface UserActionsListener { + fun imageAvailable() - void openNote(@Nullable String noteId); + fun imageCaptureFailed() } } diff --git a/app/src/main/java/com/example/android/testing/notes/addnote/AddNoteFragment.java b/app/src/main/java/com/example/android/testing/notes/addnote/AddNoteFragment.java deleted file mode 100644 index 0b77927e1..000000000 --- a/app/src/main/java/com/example/android/testing/notes/addnote/AddNoteFragment.java +++ /dev/null @@ -1,195 +0,0 @@ -/* - * Copyright 2015, The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.android.testing.notes.addnote; - -import com.bumptech.glide.Glide; -import com.bumptech.glide.load.engine.DiskCacheStrategy; -import com.bumptech.glide.load.resource.drawable.GlideDrawable; -import com.bumptech.glide.request.animation.GlideAnimation; -import com.bumptech.glide.request.target.GlideDrawableImageViewTarget; -import com.example.android.testing.notes.Injection; -import com.example.android.testing.notes.R; -import com.example.android.testing.notes.util.EspressoIdlingResource; - -import android.app.Activity; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.provider.MediaStore; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.design.widget.FloatingActionButton; -import android.support.design.widget.Snackbar; -import android.support.v4.app.Fragment; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; - -import java.io.IOException; - -import static com.google.common.base.Preconditions.checkState; - -/** - * Main UI for the add note screen. Users can enter a note title and description. Images can be - * added to notes by clicking on the options menu. - */ -public class AddNoteFragment extends Fragment implements AddNoteContract.View { - - public static final int REQUEST_CODE_IMAGE_CAPTURE = 0x1001; - - private AddNoteContract.UserActionsListener mActionListener; - - private TextView mTitle; - - private TextView mDescription; - - private ImageView mImageThumbnail; - - public static AddNoteFragment newInstance() { - return new AddNoteFragment(); - } - - public AddNoteFragment() { - // Required empty public constructor - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - mActionListener = new AddNotePresenter(Injection.provideNotesRepository(), this, - Injection.provideImageFile()); - - FloatingActionButton fab = - (FloatingActionButton) getActivity().findViewById(R.id.fab_add_notes); - fab.setImageResource(R.drawable.ic_done); - fab.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - mActionListener.saveNote(mTitle.getText().toString(), - mDescription.getText().toString()); - } - }); - } - - @Nullable - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View root = inflater.inflate(R.layout.fragment_addnote, container, false); - mTitle = (TextView) root.findViewById(R.id.add_note_title); - mDescription = (TextView) root.findViewById(R.id.add_note_description); - mImageThumbnail = (ImageView) root.findViewById(R.id.add_note_image_thumbnail); - - setHasOptionsMenu(true); - setRetainInstance(true); - return root; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.take_picture: - try { - mActionListener.takePicture(); - } catch (IOException ioe) { - if (getView() != null) { - Snackbar.make(getView(), getString(R.string.take_picture_error), - Snackbar.LENGTH_LONG).show(); - } - } - return true; - } - return false; - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - inflater.inflate(R.menu.fragment_addnote_options_menu_actions, menu); - super.onCreateOptionsMenu(menu, inflater); - } - - @Override - public void showEmptyNoteError() { - Snackbar.make(mTitle, getString(R.string.empty_note_message), Snackbar.LENGTH_LONG).show(); - } - - @Override - public void showNotesList() { - getActivity().setResult(Activity.RESULT_OK); - getActivity().finish(); - } - - @Override - public void openCamera(String saveTo) { - // Open the camera to take a picture. - Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); - // Check if there is a camera app installed to handle our Intent - if (takePictureIntent.resolveActivity(getContext().getPackageManager()) != null) { - takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.parse(saveTo)); - startActivityForResult(takePictureIntent, REQUEST_CODE_IMAGE_CAPTURE); - } else { - Snackbar.make(mTitle, getString(R.string.cannot_connect_to_camera_message), - Snackbar.LENGTH_SHORT).show(); - } - } - - @Override - public void showImagePreview(@NonNull String imageUrl) { - checkState(!TextUtils.isEmpty(imageUrl), "imageUrl cannot be null or empty!"); - mImageThumbnail.setVisibility(View.VISIBLE); - - // The image is loaded in a different thread so in order to UI-test this, an idling resource - // is used to specify when the app is idle. - EspressoIdlingResource.increment(); // App is busy until further notice. - - // This app uses Glide for image loading - Glide.with(this) - .load(imageUrl) - .diskCacheStrategy(DiskCacheStrategy.ALL) - .centerCrop() - .into(new GlideDrawableImageViewTarget(mImageThumbnail) { - @Override - public void onResourceReady(GlideDrawable resource, - GlideAnimation animation) { - super.onResourceReady(resource, animation); - EspressoIdlingResource.decrement(); // Set app as idle. - } - }); - } - - @Override - public void showImageError() { - Snackbar.make(mTitle, getString(R.string.cannot_connect_to_camera_message), - Snackbar.LENGTH_SHORT).show(); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - // If an image is received, display it on the ImageView. - if (REQUEST_CODE_IMAGE_CAPTURE == requestCode && Activity.RESULT_OK == resultCode) { - mActionListener.imageAvailable(); - } else { - mActionListener.imageCaptureFailed(); - } - } -} diff --git a/app/src/main/java/com/example/android/testing/notes/addnote/AddNoteFragment.kt b/app/src/main/java/com/example/android/testing/notes/addnote/AddNoteFragment.kt new file mode 100644 index 000000000..ebf874147 --- /dev/null +++ b/app/src/main/java/com/example/android/testing/notes/addnote/AddNoteFragment.kt @@ -0,0 +1,151 @@ +/* + * Copyright 2015, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.testing.notes.addnote + +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.provider.MediaStore +import android.support.design.widget.FloatingActionButton +import android.support.design.widget.Snackbar +import android.support.v4.app.Fragment +import android.view.* +import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.load.resource.drawable.GlideDrawable +import com.bumptech.glide.request.animation.GlideAnimation +import com.bumptech.glide.request.target.GlideDrawableImageViewTarget +import com.example.android.testing.notes.Injection +import com.example.android.testing.notes.R +import com.example.android.testing.notes.util.EspressoIdlingResource +import kotlinx.android.synthetic.main.fragment_addnote.* +import java.io.IOException + +/** + * Main UI for the add note screen. Users can enter a note title and description. Images can be + * added to notes by clicking on the options menu. + */ +class AddNoteFragment : Fragment(), AddNoteContract.View { + + private lateinit var mActionListener: AddNoteContract.UserActionsListener + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + mActionListener = AddNotePresenter(Injection.provideNotesRepository(), this, Injection.provideImageFile()) + + val fab = activity.findViewById(R.id.fab_add_notes) as FloatingActionButton + fab.setImageResource(R.drawable.ic_done) + fab.setOnClickListener { + mActionListener.saveNote(add_note_title!!.text.toString(), add_note_description!!.text.toString()) + } + } + + override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val root = inflater!!.inflate(R.layout.fragment_addnote, container, false) + setHasOptionsMenu(true) + retainInstance = true + return root + } + + override fun onOptionsItemSelected(item: MenuItem?): Boolean { + when (item!!.itemId) { + R.id.take_picture -> { + try { + mActionListener.takePicture() + } catch (ioe: IOException) { + if (view != null) { + Snackbar.make(view!!, getString(R.string.take_picture_error), Snackbar.LENGTH_LONG).show() + } + } + + return true + } + } + return false + } + + override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) { + inflater!!.inflate(R.menu.fragment_addnote_options_menu_actions, menu) + super.onCreateOptionsMenu(menu, inflater) + } + + override fun showEmptyNoteError() { + Snackbar.make(add_note_title, getString(R.string.empty_note_message), Snackbar.LENGTH_LONG).show() + } + + override fun showNotesList() { + activity.setResult(Activity.RESULT_OK) + activity.finish() + } + + override fun openCamera(saveTo: String) { + // Open the camera to take a picture. + val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) + // Check if there is a camera app installed to handle our Intent + if (takePictureIntent.resolveActivity(context.packageManager) != null) { + takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.parse(saveTo)) + startActivityForResult(takePictureIntent, REQUEST_CODE_IMAGE_CAPTURE) + } + else { + Snackbar.make(add_note_title, getString(R.string.cannot_connect_to_camera_message), Snackbar.LENGTH_SHORT).show() + } + } + + override fun showImagePreview(imageUrl: String) { + add_note_image_thumbnail.visibility = View.VISIBLE + + // The image is loaded in a different thread so in order to UI-test this, an idling resource + // is used to specify when the app is idle. + EspressoIdlingResource.increment() // App is busy until further notice. + + // This app uses Glide for image loading + Glide.with(this) + .load(imageUrl) + .diskCacheStrategy(DiskCacheStrategy.ALL) + .centerCrop() + .into(object : GlideDrawableImageViewTarget(add_note_image_thumbnail!!) { + override fun onResourceReady(resource: GlideDrawable, animation: GlideAnimation?) { + super.onResourceReady(resource, animation) + EspressoIdlingResource.decrement() // Set app as idle. + } + }) + } + + override fun showImageError() { + Snackbar.make(add_note_title!!, getString(R.string.cannot_connect_to_camera_message), Snackbar.LENGTH_SHORT).show() + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + // If an image is received, display it on the ImageView. + if (REQUEST_CODE_IMAGE_CAPTURE == requestCode && Activity.RESULT_OK == resultCode) { + mActionListener.imageAvailable() + } + else { + mActionListener.imageCaptureFailed() + } + } + + companion object { + + val REQUEST_CODE_IMAGE_CAPTURE = 0x1001 + + fun newInstance(): AddNoteFragment = AddNoteFragment() + + } +} +// Required empty public constructor diff --git a/app/src/main/java/com/example/android/testing/notes/addnote/AddNotePresenter.java b/app/src/main/java/com/example/android/testing/notes/addnote/AddNotePresenter.java deleted file mode 100644 index 9e2d13bc8..000000000 --- a/app/src/main/java/com/example/android/testing/notes/addnote/AddNotePresenter.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2015, The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.android.testing.notes.addnote; - -import com.example.android.testing.notes.data.Note; -import com.example.android.testing.notes.data.NotesRepository; -import com.example.android.testing.notes.util.ImageFile; - -import android.support.annotation.NonNull; - -import java.io.IOException; -import java.text.SimpleDateFormat; -import java.util.Date; - -import static com.google.common.base.Preconditions.checkNotNull; - -/** - * Listens to user actions from the UI ({@link AddNoteFragment}), retrieves the data and updates - * the UI as required. - */ -public class AddNotePresenter implements AddNoteContract.UserActionsListener { - - @NonNull - private final NotesRepository mNotesRepository; - @NonNull - private final AddNoteContract.View mAddNoteView; - @NonNull - private final ImageFile mImageFile; - - public AddNotePresenter(@NonNull NotesRepository notesRepository, - @NonNull AddNoteContract.View addNoteView, - @NonNull ImageFile imageFile) { - mNotesRepository = checkNotNull(notesRepository); - mAddNoteView = checkNotNull(addNoteView); - mImageFile = imageFile; - } - - @Override - public void saveNote(String title, String description) { - String imageUrl = null; - if (mImageFile.exists()) { - imageUrl = mImageFile.getPath(); - } - Note newNote = new Note(title, description, imageUrl); - if (newNote.isEmpty()) { - mAddNoteView.showEmptyNoteError(); - } else { - mNotesRepository.saveNote(newNote); - mAddNoteView.showNotesList(); - } - } - - @Override - public void takePicture() throws IOException { - String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); - String imageFileName = "JPEG_" + timeStamp + "_"; - mImageFile.create(imageFileName, ".jpg"); - mAddNoteView.openCamera(mImageFile.getPath()); - } - - @Override - public void imageAvailable() { - if (mImageFile.exists()) { - mAddNoteView.showImagePreview(mImageFile.getPath()); - } else { - imageCaptureFailed(); - } - } - - @Override - public void imageCaptureFailed() { - captureFailed(); - } - - private void captureFailed() { - mImageFile.delete(); - mAddNoteView.showImageError(); - } - -} diff --git a/app/src/main/java/com/example/android/testing/notes/addnote/AddNotePresenter.kt b/app/src/main/java/com/example/android/testing/notes/addnote/AddNotePresenter.kt new file mode 100644 index 000000000..955759d4b --- /dev/null +++ b/app/src/main/java/com/example/android/testing/notes/addnote/AddNotePresenter.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2015, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.testing.notes.addnote + +import com.example.android.testing.notes.data.Note +import com.example.android.testing.notes.data.NotesRepository +import com.example.android.testing.notes.util.ImageFile +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.* + +/** + * Listens to user actions from the UI ([AddNoteFragment]), retrieves the data and updates + * the UI as required. + */ +class AddNotePresenter(val mNotesRepository: NotesRepository, + val mAddNoteView: AddNoteContract.View, + private val mImageFile: ImageFile) : AddNoteContract.UserActionsListener { + + override fun saveNote(title: String, description: String) { + var imageUrl: String? = null + if (mImageFile.exists()) { + imageUrl = mImageFile.path + } + val newNote = Note(title, description, imageUrl) + if (newNote.isEmpty) { + mAddNoteView.showEmptyNoteError() + } + else { + mNotesRepository.saveNote(newNote) + mAddNoteView.showNotesList() + } + } + + @Throws(IOException::class) + override fun takePicture() { + val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date()) + val imageFileName = "JPEG_" + timeStamp + "_" + mImageFile.create(imageFileName, ".jpg") + mAddNoteView.openCamera(mImageFile.path) + } + + override fun imageAvailable() { + if (mImageFile.exists()) { + mAddNoteView.showImagePreview(mImageFile.path) + } + else { + imageCaptureFailed() + } + } + + override fun imageCaptureFailed() { + captureFailed() + } + + private fun captureFailed() { + mImageFile.delete() + mAddNoteView.showImageError() + } + +} diff --git a/app/src/main/java/com/example/android/testing/notes/data/InMemoryNotesRepository.java b/app/src/main/java/com/example/android/testing/notes/data/InMemoryNotesRepository.java deleted file mode 100644 index f7507c89a..000000000 --- a/app/src/main/java/com/example/android/testing/notes/data/InMemoryNotesRepository.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2015, The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.android.testing.notes.data; - -import com.google.common.collect.ImmutableList; - -import android.support.annotation.NonNull; -import android.support.annotation.VisibleForTesting; - -import java.util.List; - -import static com.google.common.base.Preconditions.checkNotNull; - -/** - * Concrete implementation to load notes from the a data source. - */ -public class InMemoryNotesRepository implements NotesRepository { - - private final NotesServiceApi mNotesServiceApi; - - /** - * This method has reduced visibility for testing and is only visible to tests in the same - * package. - */ - @VisibleForTesting - List mCachedNotes; - - public InMemoryNotesRepository(@NonNull NotesServiceApi notesServiceApi) { - mNotesServiceApi = checkNotNull(notesServiceApi); - } - - @Override - public void getNotes(@NonNull final LoadNotesCallback callback) { - checkNotNull(callback); - // Load from API only if needed. - if (mCachedNotes == null) { - mNotesServiceApi.getAllNotes(new NotesServiceApi.NotesServiceCallback>() { - @Override - public void onLoaded(List notes) { - mCachedNotes = ImmutableList.copyOf(notes); - callback.onNotesLoaded(mCachedNotes); - } - }); - } else { - callback.onNotesLoaded(mCachedNotes); - } - } - - @Override - public void saveNote(@NonNull Note note) { - checkNotNull(note); - mNotesServiceApi.saveNote(note); - refreshData(); - } - - @Override - public void getNote(@NonNull final String noteId, @NonNull final GetNoteCallback callback) { - checkNotNull(noteId); - checkNotNull(callback); - // Load notes matching the id always directly from the API. - mNotesServiceApi.getNote(noteId, new NotesServiceApi.NotesServiceCallback() { - @Override - public void onLoaded(Note note) { - callback.onNoteLoaded(note); - } - }); - } - - @Override - public void refreshData() { - mCachedNotes = null; - } - -} diff --git a/app/src/main/java/com/example/android/testing/notes/data/InMemoryNotesRepository.kt b/app/src/main/java/com/example/android/testing/notes/data/InMemoryNotesRepository.kt new file mode 100644 index 000000000..18ece3f14 --- /dev/null +++ b/app/src/main/java/com/example/android/testing/notes/data/InMemoryNotesRepository.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2015, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.testing.notes.data + +import com.google.common.collect.ImmutableList +import android.support.annotation.VisibleForTesting + +import com.google.common.base.Preconditions.checkNotNull + +/** + * Concrete implementation to load notes from the a data source. + */ +class InMemoryNotesRepository(val mNotesServiceApi: NotesServiceApi) : NotesRepository { + + /** + * This method has reduced visibility for testing and is only visible to tests in the same + * package. + */ + @VisibleForTesting + internal var mCachedNotes: List? = null + /* todo: internal 키워드에 대해서 알아보기 + - byte code 를 디컴파일 해보면 private 이다 + - 같은 모듈안에서 접근이 가능한 모디파이어 + - 라이브러리 만들때 쓰면 좋은건가 ? + + */ + + override fun getNotes(callback: NotesRepository.LoadNotesCallback) { + // Load from API only if needed. + if (mCachedNotes == null) { + mNotesServiceApi.getAllNotes(object : NotesServiceApi.NotesServiceCallback> { + override fun onLoaded(notes: List) { + mCachedNotes = ImmutableList.copyOf(notes) + callback.onNotesLoaded(mCachedNotes) + } + }) + } + else { + callback.onNotesLoaded(mCachedNotes) + } + } + + override fun saveNote(note: Note) { + checkNotNull(note) + mNotesServiceApi.saveNote(note) + refreshData() + } + + override fun getNote(noteId: String, callback: NotesRepository.GetNoteCallback) { + // Load notes matching the id always directly from the API. + mNotesServiceApi.getNote(noteId, object : NotesServiceApi.NotesServiceCallback { + override fun onLoaded(note: Note?) { + callback.onNoteLoaded(note) + } + }) + } + + override fun refreshData() { + mCachedNotes = null + } + +} diff --git a/app/src/main/java/com/example/android/testing/notes/data/Note.java b/app/src/main/java/com/example/android/testing/notes/data/Note.java deleted file mode 100644 index 7696b9114..000000000 --- a/app/src/main/java/com/example/android/testing/notes/data/Note.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2015, The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.android.testing.notes.data; - -import com.google.common.base.Objects; - -import android.support.annotation.Nullable; - -import java.util.UUID; - -/** - * Immutable model class for a Note. - */ -public final class Note { - - private final String mId; - @Nullable - private final String mTitle; - @Nullable - private final String mDescription; - @Nullable - private final String mImageUrl; - - public Note(@Nullable String title, @Nullable String description) { - this(title, description, null); - } - - public Note(@Nullable String title, @Nullable String description, @Nullable String imageUrl) { - mId = UUID.randomUUID().toString(); - mTitle = title; - mDescription = description; - mImageUrl = imageUrl; - } - - public String getId() { - return mId; - } - - @Nullable - public String getTitle() { - return mTitle; - } - - @Nullable - public String getDescription() { - return mDescription; - } - - @Nullable - public String getImageUrl() { - return mImageUrl; - } - - public boolean isEmpty() { - return (mTitle == null || "".equals(mTitle)) && - (mDescription == null || "".equals(mDescription)); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Note note = (Note) o; - return Objects.equal(mId, note.mId) && - Objects.equal(mTitle, note.mTitle) && - Objects.equal(mDescription, note.mDescription) && - Objects.equal(mImageUrl, note.mImageUrl); - } - - @Override - public int hashCode() { - return Objects.hashCode(mId, mTitle, mDescription, mImageUrl); - } -} diff --git a/app/src/main/java/com/example/android/testing/notes/data/Note.kt b/app/src/main/java/com/example/android/testing/notes/data/Note.kt new file mode 100644 index 000000000..210a04124 --- /dev/null +++ b/app/src/main/java/com/example/android/testing/notes/data/Note.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2015, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.testing.notes.data + +import java.util.* +import kotlin.jvm.internal.Intrinsics + +/** + * Immutable model class for a Note. + */ +data class Note @JvmOverloads constructor( + val title: String?, + val description: String?, + val imageUrl: String? = null) { + + val id: String = UUID.randomUUID().toString() + + val isEmpty: Boolean + get() = (title == null || "" == title) && (description == null || "" == description) + + /* todo: 데이터 클래스의 equal, hashCode 동작 확실히 알기 + - equal 값이 같은지 비교, 다른 객체라도 값이 같으면 true + */ +} diff --git a/app/src/main/java/com/example/android/testing/notes/data/NoteRepositories.kt b/app/src/main/java/com/example/android/testing/notes/data/NoteRepositories.kt new file mode 100644 index 000000000..9cfc6bcf7 --- /dev/null +++ b/app/src/main/java/com/example/android/testing/notes/data/NoteRepositories.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2015, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.testing.notes.data + +object NoteRepositories { + + var repository: NotesRepository? = null + + @Synchronized + fun getInMemoryRepoInstance(notesServiceApi: NotesServiceApi): NotesRepository { + if (null == repository) + repository = InMemoryNotesRepository(notesServiceApi) + return repository!! + } + + /* todo: kotlin 에서 singleton 패턴 하는법 찾아보기 + https://blog.rahulchowdhury.co/not-so-singletons-in-kotlin/ + + - thread safe + - lazy initialization + - 자바로 디컴파일된 파일을 보면 lazy 하지 않다. + - 코틀린은 lazy 하다 + - reflection proof + */ +} \ No newline at end of file diff --git a/app/src/main/java/com/example/android/testing/notes/data/NotesRepository.java b/app/src/main/java/com/example/android/testing/notes/data/NotesRepository.kt similarity index 63% rename from app/src/main/java/com/example/android/testing/notes/data/NotesRepository.java rename to app/src/main/java/com/example/android/testing/notes/data/NotesRepository.kt index 2d06b3d7e..7aac4dbbe 100644 --- a/app/src/main/java/com/example/android/testing/notes/data/NotesRepository.java +++ b/app/src/main/java/com/example/android/testing/notes/data/NotesRepository.kt @@ -14,33 +14,29 @@ * limitations under the License. */ -package com.example.android.testing.notes.data; - -import android.support.annotation.NonNull; - -import java.util.List; +package com.example.android.testing.notes.data /** * Main entry point for accessing notes data. */ -public interface NotesRepository { +interface NotesRepository { interface LoadNotesCallback { - void onNotesLoaded(List notes); + fun onNotesLoaded(notes: List?) } interface GetNoteCallback { - void onNoteLoaded(Note note); + fun onNoteLoaded(note: Note?) } - void getNotes(@NonNull LoadNotesCallback callback); + fun getNotes(callback: LoadNotesCallback) - void getNote(@NonNull String noteId, @NonNull GetNoteCallback callback); + fun getNote(noteId: String, callback: GetNoteCallback) - void saveNote(@NonNull Note note); + fun saveNote(note: Note) - void refreshData(); + fun refreshData() } diff --git a/app/src/main/java/com/example/android/testing/notes/data/NotesServiceApi.java b/app/src/main/java/com/example/android/testing/notes/data/NotesServiceApi.kt similarity index 69% rename from app/src/main/java/com/example/android/testing/notes/data/NotesServiceApi.java rename to app/src/main/java/com/example/android/testing/notes/data/NotesServiceApi.kt index badbe159a..724ad849f 100644 --- a/app/src/main/java/com/example/android/testing/notes/data/NotesServiceApi.java +++ b/app/src/main/java/com/example/android/testing/notes/data/NotesServiceApi.kt @@ -14,24 +14,22 @@ * limitations under the License. */ -package com.example.android.testing.notes.data; - -import java.util.List; +package com.example.android.testing.notes.data /** * Defines an interface to the service API that is used by this application. All data request should * be piped through this interface. */ -public interface NotesServiceApi { +interface NotesServiceApi { - interface NotesServiceCallback { + interface NotesServiceCallback { - void onLoaded(T notes); + fun onLoaded(notes: T) } - void getAllNotes(NotesServiceCallback> callback); + fun getAllNotes(callback: NotesServiceCallback>) - void getNote(String noteId, NotesServiceCallback callback); + fun getNote(noteId: String, callback: NotesServiceCallback) - void saveNote(Note note); + fun saveNote(note: Note) } diff --git a/app/src/main/java/com/example/android/testing/notes/data/NotesServiceApiEndpoint.java b/app/src/main/java/com/example/android/testing/notes/data/NotesServiceApiEndpoint.kt similarity index 58% rename from app/src/main/java/com/example/android/testing/notes/data/NotesServiceApiEndpoint.java rename to app/src/main/java/com/example/android/testing/notes/data/NotesServiceApiEndpoint.kt index 9c3b4b4e6..4624dabdd 100644 --- a/app/src/main/java/com/example/android/testing/notes/data/NotesServiceApiEndpoint.java +++ b/app/src/main/java/com/example/android/testing/notes/data/NotesServiceApiEndpoint.kt @@ -14,33 +14,34 @@ * limitations under the License. */ -package com.example.android.testing.notes.data; +package com.example.android.testing.notes.data -import android.support.v4.util.ArrayMap; +import android.support.v4.util.ArrayMap /** * This is the endpoint for your data source. Typically, it would be a SQLite db and/or a server * API. In this example, we fake this by creating the data on the fly. */ -public final class NotesServiceApiEndpoint { +object NotesServiceApiEndpoint { - static { - DATA = new ArrayMap(2); - addNote("Oh yes!", "I demand trial by Unit testing", null); - addNote("Espresso", "UI Testing for Android", null); - } + /* todo: DATA 를 위에쓰고 init 에서 초기화를 할수있는데, init 에서 초기화 설정하는데 DATA 변수를 init 밑에서 선언하면 컴파일 에러 */ + + private val DATA: ArrayMap = ArrayMap(2) - private final static ArrayMap DATA; + init { + addNote("Oh yes!", "I demand trial by Unit testing", null) + addNote("Espresso", "UI Testing for Android", null) + } - private static void addNote(String title, String description, String imageUrl) { - Note newNote = new Note(title, description, imageUrl); - DATA.put(newNote.getId(), newNote); + private fun addNote(title: String, description: String, imageUrl: String?) { + val newNote = Note(title, description, imageUrl) + DATA.put(newNote.id, newNote) } /** * @return the Notes to show when starting the app. */ - public static ArrayMap loadPersistedNotes() { - return DATA; + fun loadPersistedNotes(): ArrayMap { + return DATA } } diff --git a/app/src/main/java/com/example/android/testing/notes/data/NotesServiceApiImpl.java b/app/src/main/java/com/example/android/testing/notes/data/NotesServiceApiImpl.java deleted file mode 100644 index 2c89b0001..000000000 --- a/app/src/main/java/com/example/android/testing/notes/data/NotesServiceApiImpl.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2015, The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.android.testing.notes.data; - -import android.os.Handler; -import android.support.v4.util.ArrayMap; - -import java.util.ArrayList; -import java.util.List; - -/** - * Implementation of the Notes Service API that adds a latency simulating network. - */ -public class NotesServiceApiImpl implements NotesServiceApi { - - private static final int SERVICE_LATENCY_IN_MILLIS = 2000; - private static final ArrayMap NOTES_SERVICE_DATA = - NotesServiceApiEndpoint.loadPersistedNotes(); - - @Override - public void getAllNotes(final NotesServiceCallback callback) { - // Simulate network by delaying the execution. - Handler handler = new Handler(); - handler.postDelayed(new Runnable() { - @Override - public void run() { - List notes = new ArrayList<>(NOTES_SERVICE_DATA.values()); - callback.onLoaded(notes); - } - }, SERVICE_LATENCY_IN_MILLIS); - } - - @Override - public void getNote(final String noteId, final NotesServiceCallback callback) { - //TODO: Add network latency here too. - Note note = NOTES_SERVICE_DATA.get(noteId); - callback.onLoaded(note); - } - - @Override - public void saveNote(Note note) { - NOTES_SERVICE_DATA.put(note.getId(), note); - } - -} diff --git a/app/src/main/java/com/example/android/testing/notes/data/NotesServiceApiImpl.kt b/app/src/main/java/com/example/android/testing/notes/data/NotesServiceApiImpl.kt new file mode 100644 index 000000000..711d00f95 --- /dev/null +++ b/app/src/main/java/com/example/android/testing/notes/data/NotesServiceApiImpl.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2015, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.testing.notes.data + +import android.os.Handler +import android.support.v4.util.ArrayMap + +import java.util.ArrayList + +/** + * Implementation of the Notes Service API that adds a latency simulating network. + */ +class NotesServiceApiImpl : NotesServiceApi { + + override fun getAllNotes(callback: NotesServiceApi.NotesServiceCallback>) { + // Simulate network by delaying the execution. + val handler = Handler() + handler.postDelayed({ + val notes = ArrayList(NOTES_SERVICE_DATA.values) + callback.onLoaded(notes) + }, SERVICE_LATENCY_IN_MILLIS.toLong()) + } + + override fun getNote(noteId: String, callback: NotesServiceApi.NotesServiceCallback) { + //TODO: Add network latency here too. + val note = NOTES_SERVICE_DATA[noteId] + callback.onLoaded(note) + } + + override fun saveNote(note: Note) { + NOTES_SERVICE_DATA.put(note.id, note) + } + + companion object { + + private val SERVICE_LATENCY_IN_MILLIS = 2000 + private val NOTES_SERVICE_DATA = NotesServiceApiEndpoint.loadPersistedNotes() + } + +} diff --git a/app/src/main/java/com/example/android/testing/notes/notedetail/NoteDetailActivity.java b/app/src/main/java/com/example/android/testing/notes/notedetail/NoteDetailActivity.java deleted file mode 100644 index b83e46e84..000000000 --- a/app/src/main/java/com/example/android/testing/notes/notedetail/NoteDetailActivity.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.android.testing.notes.notedetail; - -import com.example.android.testing.notes.R; -import com.example.android.testing.notes.util.EspressoIdlingResource; - -import android.os.Bundle; -import android.support.annotation.VisibleForTesting; -import android.support.test.espresso.IdlingResource; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentTransaction; -import android.support.v7.app.ActionBar; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; - -/** - * Displays note details screen. - */ -public class NoteDetailActivity extends AppCompatActivity { - - public static final String EXTRA_NOTE_ID = "NOTE_ID"; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.activity_detail); - - // Set up the toolbar. - Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - ActionBar ab = getSupportActionBar(); - ab.setDisplayHomeAsUpEnabled(true); - ab.setDisplayShowHomeEnabled(true); - - // Get the requested note id - String noteId = getIntent().getStringExtra(EXTRA_NOTE_ID); - - initFragment(NoteDetailFragment.newInstance(noteId)); - } - - @Override - public boolean onSupportNavigateUp() { - onBackPressed(); - return true; - } - - private void initFragment(Fragment detailFragment) { - // Add the NotesDetailFragment to the layout - FragmentManager fragmentManager = getSupportFragmentManager(); - FragmentTransaction transaction = fragmentManager.beginTransaction(); - transaction.add(R.id.contentFrame, detailFragment); - transaction.commit(); - } - - @VisibleForTesting - public IdlingResource getCountingIdlingResource() { - return EspressoIdlingResource.getIdlingResource(); - } -} diff --git a/app/src/main/java/com/example/android/testing/notes/notedetail/NoteDetailActivity.kt b/app/src/main/java/com/example/android/testing/notes/notedetail/NoteDetailActivity.kt new file mode 100644 index 000000000..0b3058ae6 --- /dev/null +++ b/app/src/main/java/com/example/android/testing/notes/notedetail/NoteDetailActivity.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.testing.notes.notedetail + +import com.example.android.testing.notes.R +import com.example.android.testing.notes.util.EspressoIdlingResource + +import android.os.Bundle +import android.support.annotation.VisibleForTesting +import android.support.test.espresso.IdlingResource +import android.support.v4.app.Fragment +import android.support.v4.app.FragmentManager +import android.support.v4.app.FragmentTransaction +import android.support.v7.app.ActionBar +import android.support.v7.app.AppCompatActivity +import android.support.v7.widget.Toolbar + +/** + * Displays note details screen. + */ +class NoteDetailActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.activity_detail) + + // Set up the toolbar. + val toolbar = findViewById(R.id.toolbar) as Toolbar + setSupportActionBar(toolbar) + val ab = supportActionBar + ab!!.setDisplayHomeAsUpEnabled(true) + ab.setDisplayShowHomeEnabled(true) + + // Get the requested note id + val noteId = intent.getStringExtra(EXTRA_NOTE_ID) + + initFragment(NoteDetailFragment.newInstance(noteId)) + } + + override fun onSupportNavigateUp(): Boolean { + onBackPressed() + return true + } + + private fun initFragment(detailFragment: Fragment) { + // Add the NotesDetailFragment to the layout + val fragmentManager = supportFragmentManager + val transaction = fragmentManager.beginTransaction() + transaction.add(R.id.contentFrame, detailFragment) + transaction.commit() + } + + val countingIdlingResource: IdlingResource + @VisibleForTesting + get() = EspressoIdlingResource.idlingResource + + companion object { + + val EXTRA_NOTE_ID = "NOTE_ID" + } +} diff --git a/app/src/main/java/com/example/android/testing/notes/notes/NotesContract.java b/app/src/main/java/com/example/android/testing/notes/notedetail/NoteDetailContract.kt similarity index 60% rename from app/src/main/java/com/example/android/testing/notes/notes/NotesContract.java rename to app/src/main/java/com/example/android/testing/notes/notedetail/NoteDetailContract.kt index 02053a324..55394bdca 100644 --- a/app/src/main/java/com/example/android/testing/notes/notes/NotesContract.java +++ b/app/src/main/java/com/example/android/testing/notes/notedetail/NoteDetailContract.kt @@ -14,36 +14,34 @@ * limitations under the License. */ -package com.example.android.testing.notes.notes; - -import android.support.annotation.NonNull; - -import com.example.android.testing.notes.data.Note; - -import java.util.List; +package com.example.android.testing.notes.notedetail /** * This specifies the contract between the view and the presenter. */ -public interface NotesContract { +interface NoteDetailContract { interface View { - void setProgressIndicator(boolean active); + fun setProgressIndicator(active: Boolean) - void showNotes(List notes); + fun showMissingNote() - void showAddNote(); + fun hideTitle() - void showNoteDetailUi(String noteId); - } + fun showTitle(title: String) - interface UserActionsListener { + fun showImage(imageUrl: String) - void loadNotes(boolean forceUpdate); + fun hideImage() - void addNewNote(); + fun hideDescription() + + fun showDescription(description: String) + } + + interface UserActionsListener { - void openNoteDetails(@NonNull Note requestedNote); + fun openNote(noteId: String?) } } diff --git a/app/src/main/java/com/example/android/testing/notes/notedetail/NoteDetailFragment.java b/app/src/main/java/com/example/android/testing/notes/notedetail/NoteDetailFragment.java deleted file mode 100644 index 8d55dc876..000000000 --- a/app/src/main/java/com/example/android/testing/notes/notedetail/NoteDetailFragment.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.android.testing.notes.notedetail; - -import com.bumptech.glide.Glide; -import com.bumptech.glide.load.engine.DiskCacheStrategy; -import com.bumptech.glide.load.resource.drawable.GlideDrawable; -import com.bumptech.glide.request.animation.GlideAnimation; -import com.bumptech.glide.request.target.GlideDrawableImageViewTarget; -import com.example.android.testing.notes.Injection; -import com.example.android.testing.notes.R; -import com.example.android.testing.notes.util.EspressoIdlingResource; - -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.ImageView; -import android.widget.TextView; - -/** - * Main UI for the note detail screen. - */ -public class NoteDetailFragment extends Fragment implements NoteDetailContract.View { - - public static final String ARGUMENT_NOTE_ID = "NOTE_ID"; - - private NoteDetailContract.UserActionsListener mActionsListener; - - private TextView mDetailTitle; - - private TextView mDetailDescription; - - private ImageView mDetailImage; - - public static NoteDetailFragment newInstance(String noteId) { - Bundle arguments = new Bundle(); - arguments.putString(ARGUMENT_NOTE_ID, noteId); - NoteDetailFragment fragment = new NoteDetailFragment(); - fragment.setArguments(arguments); - return fragment; - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - mActionsListener = new NoteDetailPresenter(Injection.provideNotesRepository(), - this); - } - - @Nullable - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View root = inflater.inflate(R.layout.fragment_detail, container, false); - mDetailTitle = (TextView) root.findViewById(R.id.note_detail_title); - mDetailDescription = (TextView) root.findViewById(R.id.note_detail_description); - mDetailImage = (ImageView) root.findViewById(R.id.note_detail_image); - return root; - } - - @Override - public void onResume() { - super.onResume(); - String noteId = getArguments().getString(ARGUMENT_NOTE_ID); - mActionsListener.openNote(noteId); - } - - @Override - public void setProgressIndicator(boolean active) { - if (active) { - mDetailTitle.setText(""); - mDetailDescription.setText(getString(R.string.loading)); - } - } - - @Override - public void hideDescription() { - mDetailDescription.setVisibility(View.GONE); - } - - @Override - public void hideTitle() { - mDetailTitle.setVisibility(View.GONE); - } - - @Override - public void showDescription(String description) { - mDetailDescription.setVisibility(View.VISIBLE); - mDetailDescription.setText(description); - } - - @Override - public void showTitle(String title) { - mDetailTitle.setVisibility(View.VISIBLE); - mDetailTitle.setText(title); - } - - @Override - public void showImage(String imageUrl) { - // The image is loaded in a different thread so in order to UI-test this, an idling resource - // is used to specify when the app is idle. - EspressoIdlingResource.increment(); // App is busy until further notice. - - mDetailImage.setVisibility(View.VISIBLE); - - // This app uses Glide for image loading - Glide.with(this) - .load(imageUrl) - .diskCacheStrategy(DiskCacheStrategy.ALL) - .centerCrop() - .into(new GlideDrawableImageViewTarget(mDetailImage) { - @Override - public void onResourceReady(GlideDrawable resource, - GlideAnimation animation) { - super.onResourceReady(resource, animation); - EspressoIdlingResource.decrement(); // App is idle. - } - }); - } - - @Override - public void hideImage() { - mDetailImage.setImageDrawable(null); - mDetailImage.setVisibility(View.GONE); - } - - @Override - public void showMissingNote() { - mDetailTitle.setText(""); - mDetailDescription.setText(getString(R.string.no_data)); - } -} diff --git a/app/src/main/java/com/example/android/testing/notes/notedetail/NoteDetailFragment.kt b/app/src/main/java/com/example/android/testing/notes/notedetail/NoteDetailFragment.kt new file mode 100644 index 000000000..4bcbe5ed6 --- /dev/null +++ b/app/src/main/java/com/example/android/testing/notes/notedetail/NoteDetailFragment.kt @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.testing.notes.notedetail + +import android.os.Bundle +import android.support.v4.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.load.resource.drawable.GlideDrawable +import com.bumptech.glide.request.animation.GlideAnimation +import com.bumptech.glide.request.target.GlideDrawableImageViewTarget +import com.example.android.testing.notes.Injection +import com.example.android.testing.notes.R +import com.example.android.testing.notes.util.EspressoIdlingResource +import kotlinx.android.synthetic.main.fragment_detail.* + +/** + * Main UI for the note detail screen. + */ +class NoteDetailFragment : Fragment(), NoteDetailContract.View { + + private lateinit var mActionsListener: NoteDetailContract.UserActionsListener + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + mActionsListener = NoteDetailPresenter(Injection.provideNotesRepository(), this) + } + + override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater!!.inflate(R.layout.fragment_detail, container, false) + } + + override fun onResume() { + super.onResume() + val noteId = arguments.getString(ARGUMENT_NOTE_ID) + mActionsListener.openNote(noteId) + } + + override fun setProgressIndicator(active: Boolean) { + if (active) { + note_detail_title.text = "" + note_detail_description.text = getString(R.string.loading) + } + } + + override fun hideDescription() { + note_detail_description.visibility = View.GONE + } + + override fun hideTitle() { + note_detail_title.visibility = View.GONE + } + + override fun showDescription(description: String) { + note_detail_description.visibility = View.VISIBLE + note_detail_description.text = description + } + + override fun showTitle(title: String) { + note_detail_title.visibility = View.VISIBLE + note_detail_title.text = title + } + + override fun showImage(imageUrl: String) { + // The image is loaded in a different thread so in order to UI-test this, an idling resource + // is used to specify when the app is idle. + EspressoIdlingResource.increment() // App is busy until further notice. + + note_detail_image.visibility = View.VISIBLE + + // This app uses Glide for image loading + Glide.with(this) + .load(imageUrl) + .diskCacheStrategy(DiskCacheStrategy.ALL) + .centerCrop() + .into(object : GlideDrawableImageViewTarget(note_detail_image) { + + override fun onResourceReady(resource: GlideDrawable, animation: GlideAnimation?) { + super.onResourceReady(resource, animation) + EspressoIdlingResource.decrement() // App is idle. + } + }) + } + + override fun hideImage() { + note_detail_image.setImageDrawable(null) + note_detail_image.visibility = View.GONE + } + + override fun showMissingNote() { + note_detail_title.text = "" + note_detail_description.text = getString(R.string.no_data) + } + + companion object { + + val ARGUMENT_NOTE_ID = "NOTE_ID" + + fun newInstance(noteId: String): NoteDetailFragment { + val arguments = Bundle() + arguments.putString(ARGUMENT_NOTE_ID, noteId) + + val fragment = NoteDetailFragment() + fragment.arguments = arguments + return fragment + } + } +} diff --git a/app/src/main/java/com/example/android/testing/notes/notedetail/NoteDetailPresenter.java b/app/src/main/java/com/example/android/testing/notes/notedetail/NoteDetailPresenter.java deleted file mode 100644 index 5e6102801..000000000 --- a/app/src/main/java/com/example/android/testing/notes/notedetail/NoteDetailPresenter.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2015, The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.android.testing.notes.notedetail; - -import com.example.android.testing.notes.data.Note; -import com.example.android.testing.notes.data.NotesRepository; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import static com.google.common.base.Preconditions.checkNotNull; - -/** - * Listens to user actions from the UI ({@link NoteDetailFragment}), retrieves the data and updates - * the UI as required. - */ -public class NoteDetailPresenter implements NoteDetailContract.UserActionsListener { - - private final NotesRepository mNotesRepository; - - private final NoteDetailContract.View mNotesDetailView; - - public NoteDetailPresenter(@NonNull NotesRepository notesRepository, - @NonNull NoteDetailContract.View noteDetailView) { - mNotesRepository = checkNotNull(notesRepository, "notesRepository cannot be null!"); - mNotesDetailView = checkNotNull(noteDetailView, "noteDetailView cannot be null!"); - } - - @Override - public void openNote(@Nullable String noteId) { - if (null == noteId || noteId.isEmpty()) { - mNotesDetailView.showMissingNote(); - return; - } - - mNotesDetailView.setProgressIndicator(true); - mNotesRepository.getNote(noteId, new NotesRepository.GetNoteCallback() { - @Override - public void onNoteLoaded(Note note) { - mNotesDetailView.setProgressIndicator(false); - if (null == note) { - mNotesDetailView.showMissingNote(); - } else { - showNote(note); - } - } - }); - } - - private void showNote(Note note) { - String title = note.getTitle(); - String description = note.getDescription(); - String imageUrl = note.getImageUrl(); - - if (title != null && title.isEmpty()) { - mNotesDetailView.hideTitle(); - } else { - mNotesDetailView.showTitle(title); - } - - if (description != null && description.isEmpty()) { - mNotesDetailView.hideDescription(); - } else { - mNotesDetailView.showDescription(description); - } - - if (imageUrl != null) { - mNotesDetailView.showImage(imageUrl); - } else { - mNotesDetailView.hideImage(); - } - - } -} diff --git a/app/src/main/java/com/example/android/testing/notes/notedetail/NoteDetailPresenter.kt b/app/src/main/java/com/example/android/testing/notes/notedetail/NoteDetailPresenter.kt new file mode 100644 index 000000000..1de464035 --- /dev/null +++ b/app/src/main/java/com/example/android/testing/notes/notedetail/NoteDetailPresenter.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2015, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.testing.notes.notedetail + +import com.example.android.testing.notes.data.Note +import com.example.android.testing.notes.data.NotesRepository + +/** + * Listens to user actions from the UI ([NoteDetailFragment]), retrieves the data and updates + * the UI as required. + */ +class NoteDetailPresenter(val mNotesRepository: NotesRepository, + val mNotesDetailView: NoteDetailContract.View) : NoteDetailContract.UserActionsListener { + + override fun openNote(noteId: String?) { + if (noteId.isNullOrEmpty()) { + mNotesDetailView.showMissingNote() + return + } + + mNotesDetailView.setProgressIndicator(true) + mNotesRepository.getNote(noteId!!, object : NotesRepository.GetNoteCallback { + override fun onNoteLoaded(note: Note?) { + mNotesDetailView.setProgressIndicator(false) + if (null == note) { + mNotesDetailView.showMissingNote() + } + else { + showNote(note) + } + } + }) + } + + private fun showNote(note: Note) { + val title = note.title + val description = note.description + val imageUrl = note.imageUrl + + if (title.isNullOrEmpty()) { + mNotesDetailView.hideTitle() + } + else { + mNotesDetailView.showTitle(title!!) + } + + if (description.isNullOrEmpty()) { + mNotesDetailView.hideDescription() + } + else { + mNotesDetailView.showDescription(description!!) + } + + if (imageUrl != null) { + mNotesDetailView.showImage(imageUrl) + } + else { + mNotesDetailView.hideImage() + } + + } +} diff --git a/app/src/main/java/com/example/android/testing/notes/notes/NotesActivity.java b/app/src/main/java/com/example/android/testing/notes/notes/NotesActivity.java deleted file mode 100644 index 4d68ba8f1..000000000 --- a/app/src/main/java/com/example/android/testing/notes/notes/NotesActivity.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.android.testing.notes.notes; - -import com.example.android.testing.notes.R; -import com.example.android.testing.notes.statistics.StatisticsActivity; -import com.example.android.testing.notes.util.EspressoIdlingResource; - -import android.content.Intent; -import android.os.Bundle; -import android.support.annotation.VisibleForTesting; -import android.support.design.widget.NavigationView; -import android.support.test.espresso.IdlingResource; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentTransaction; -import android.support.v4.view.GravityCompat; -import android.support.v4.widget.DrawerLayout; -import android.support.v7.app.ActionBar; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; -import android.view.MenuItem; - -public class NotesActivity extends AppCompatActivity { - - private DrawerLayout mDrawerLayout; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_notes); - - - // Set up the toolbar. - Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - ActionBar ab = getSupportActionBar(); - ab.setHomeAsUpIndicator(R.drawable.ic_menu); - ab.setDisplayHomeAsUpEnabled(true); - - // Set up the navigation drawer. - mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); - mDrawerLayout.setStatusBarBackground(R.color.colorPrimaryDark); - NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view); - if (navigationView != null) { - setupDrawerContent(navigationView); - } - - if (null == savedInstanceState) { - initFragment(NotesFragment.newInstance()); - } - } - - private void initFragment(Fragment notesFragment) { - // Add the NotesFragment to the layout - FragmentManager fragmentManager = getSupportFragmentManager(); - FragmentTransaction transaction = fragmentManager.beginTransaction(); - transaction.add(R.id.contentFrame, notesFragment); - transaction.commit(); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - // Open the navigation drawer when the home icon is selected from the toolbar. - mDrawerLayout.openDrawer(GravityCompat.START); - return true; - } - return super.onOptionsItemSelected(item); - } - - private void setupDrawerContent(NavigationView navigationView) { - navigationView.setNavigationItemSelectedListener( - new NavigationView.OnNavigationItemSelectedListener() { - @Override - public boolean onNavigationItemSelected(MenuItem menuItem) { - switch (menuItem.getItemId()) { - case R.id.statistics_navigation_menu_item: - startActivity(new Intent(NotesActivity.this, StatisticsActivity.class)); - break; - default: - break; - } - // Close the navigation drawer when an item is selected. - menuItem.setChecked(true); - mDrawerLayout.closeDrawers(); - return true; - } - }); - } - - @VisibleForTesting - public IdlingResource getCountingIdlingResource() { - return EspressoIdlingResource.getIdlingResource(); - } -} diff --git a/app/src/main/java/com/example/android/testing/notes/notes/NotesActivity.kt b/app/src/main/java/com/example/android/testing/notes/notes/NotesActivity.kt new file mode 100644 index 000000000..c8a17c61d --- /dev/null +++ b/app/src/main/java/com/example/android/testing/notes/notes/NotesActivity.kt @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.testing.notes.notes + +import android.content.Intent +import android.os.Bundle +import android.support.annotation.VisibleForTesting +import android.support.design.widget.NavigationView +import android.support.test.espresso.IdlingResource +import android.support.v4.app.Fragment +import android.support.v4.view.GravityCompat +import android.support.v7.app.AppCompatActivity +import android.view.MenuItem +import com.example.android.testing.notes.R +import com.example.android.testing.notes.statistics.StatisticsActivity +import com.example.android.testing.notes.util.EspressoIdlingResource +import kotlinx.android.synthetic.main.activity_notes.* + +class NotesActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_notes) + + // Set up the toolbar. + setSupportActionBar(toolbar) + supportActionBar?.run { + setHomeAsUpIndicator(R.drawable.ic_menu) + setDisplayHomeAsUpEnabled(true) + } + + // Set up the navigation drawer. + drawer_layout.setStatusBarBackground(R.color.colorPrimaryDark) + setupDrawerContent(nav_view) + + if (null == savedInstanceState) { + initFragment(NotesFragment.newInstance()) + } + } + + private fun initFragment(notesFragment: Fragment) { + // Add the NotesFragment to the layout + supportFragmentManager + .beginTransaction() + .add(R.id.contentFrame, notesFragment) + .commit() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + // Open the navigation drawer when the home icon is selected from the toolbar. + drawer_layout.openDrawer(GravityCompat.START) + return true + } + } + return super.onOptionsItemSelected(item) + } + + private fun setupDrawerContent(navigationView: NavigationView) { + navigationView.setNavigationItemSelectedListener { menuItem -> + when (menuItem.itemId) { + R.id.statistics_navigation_menu_item -> startActivity(Intent(this@NotesActivity, StatisticsActivity::class.java)) + else -> { + } + } + // Close the navigation drawer when an item is selected. + menuItem.isChecked = true + drawer_layout.closeDrawers() + true + } + } + + val countingIdlingResource: IdlingResource + @VisibleForTesting + get() = EspressoIdlingResource.idlingResource +} diff --git a/app/src/main/java/com/example/android/testing/notes/addnote/AddNoteContract.java b/app/src/main/java/com/example/android/testing/notes/notes/NotesContract.kt similarity index 59% rename from app/src/main/java/com/example/android/testing/notes/addnote/AddNoteContract.java rename to app/src/main/java/com/example/android/testing/notes/notes/NotesContract.kt index 5ab713e59..9e9e0f0ad 100644 --- a/app/src/main/java/com/example/android/testing/notes/addnote/AddNoteContract.java +++ b/app/src/main/java/com/example/android/testing/notes/notes/NotesContract.kt @@ -14,38 +14,32 @@ * limitations under the License. */ -package com.example.android.testing.notes.addnote; +package com.example.android.testing.notes.notes -import android.support.annotation.NonNull; - -import java.io.IOException; +import com.example.android.testing.notes.data.Note /** * This specifies the contract between the view and the presenter. */ -public interface AddNoteContract { +interface NotesContract { interface View { - void showEmptyNoteError(); - - void showNotesList(); + fun setProgressIndicator(active: Boolean) - void openCamera(String saveTo); + fun showNotes(notes: List) - void showImagePreview(@NonNull String uri); + fun showAddNote() - void showImageError(); + fun showNoteDetailUi(noteId: String) } interface UserActionsListener { - void saveNote(String title, String description); - - void takePicture() throws IOException; + fun loadNotes(forceUpdate: Boolean) - void imageAvailable(); + fun addNewNote() - void imageCaptureFailed(); + fun openNoteDetails(requestedNote: Note) } } diff --git a/app/src/main/java/com/example/android/testing/notes/notes/NotesFragment.java b/app/src/main/java/com/example/android/testing/notes/notes/NotesFragment.java deleted file mode 100644 index ff329e4ba..000000000 --- a/app/src/main/java/com/example/android/testing/notes/notes/NotesFragment.java +++ /dev/null @@ -1,260 +0,0 @@ -/* - * Copyright 2015, The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.android.testing.notes.notes; - -import com.example.android.testing.notes.Injection; -import com.example.android.testing.notes.addnote.AddNoteActivity; -import com.example.android.testing.notes.notedetail.NoteDetailActivity; -import com.example.android.testing.notes.R; -import com.example.android.testing.notes.data.Note; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.design.widget.FloatingActionButton; -import android.support.design.widget.Snackbar; -import android.support.v4.app.Fragment; -import android.support.v4.content.ContextCompat; -import android.support.v4.widget.SwipeRefreshLayout; -import android.support.v7.widget.GridLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import java.util.ArrayList; -import java.util.List; - -import static com.google.common.base.Preconditions.checkNotNull; - -/** - * Display a grid of {@link Note}s - */ -public class NotesFragment extends Fragment implements NotesContract.View { - - private static final int REQUEST_ADD_NOTE = 1; - - private NotesContract.UserActionsListener mActionsListener; - - private NotesAdapter mListAdapter; - - public NotesFragment() { - // Requires empty public constructor - } - - public static NotesFragment newInstance() { - return new NotesFragment(); - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mListAdapter = new NotesAdapter(new ArrayList(0), mItemListener); - mActionsListener = new NotesPresenter(Injection.provideNotesRepository(), this); - } - - @Override - public void onResume() { - super.onResume(); - mActionsListener.loadNotes(false); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - setRetainInstance(true); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - // If a note was successfully added, show snackbar - if (REQUEST_ADD_NOTE == requestCode && Activity.RESULT_OK == resultCode) { - Snackbar.make(getView(), getString(R.string.successfully_saved_note_message), - Snackbar.LENGTH_SHORT).show(); - } - } - - @Nullable - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View root = inflater.inflate(R.layout.fragment_notes, container, false); - RecyclerView recyclerView = (RecyclerView) root.findViewById(R.id.notes_list); - recyclerView.setAdapter(mListAdapter); - - int numColumns = getContext().getResources().getInteger(R.integer.num_notes_columns); - - recyclerView.setHasFixedSize(true); - recyclerView.setLayoutManager(new GridLayoutManager(getContext(), numColumns)); - - // Set up floating action button - FloatingActionButton fab = - (FloatingActionButton) getActivity().findViewById(R.id.fab_add_notes); - - fab.setImageResource(R.drawable.ic_add); - fab.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - mActionsListener.addNewNote(); - } - }); - - // Pull-to-refresh - SwipeRefreshLayout swipeRefreshLayout = - (SwipeRefreshLayout) root.findViewById(R.id.refresh_layout); - swipeRefreshLayout.setColorSchemeColors( - ContextCompat.getColor(getActivity(), R.color.colorPrimary), - ContextCompat.getColor(getActivity(), R.color.colorAccent), - ContextCompat.getColor(getActivity(), R.color.colorPrimaryDark)); - swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { - @Override - public void onRefresh() { - mActionsListener.loadNotes(true); - } - }); - return root; - } - - /** - * Listener for clicks on notes in the RecyclerView. - */ - NoteItemListener mItemListener = new NoteItemListener() { - @Override - public void onNoteClick(Note clickedNote) { - mActionsListener.openNoteDetails(clickedNote); - } - }; - - @Override - public void setProgressIndicator(final boolean active) { - - if (getView() == null) { - return; - } - final SwipeRefreshLayout srl = - (SwipeRefreshLayout) getView().findViewById(R.id.refresh_layout); - - // Make sure setRefreshing() is called after the layout is done with everything else. - srl.post(new Runnable() { - @Override - public void run() { - srl.setRefreshing(active); - } - }); - } - - @Override - public void showNotes(List notes) { - mListAdapter.replaceData(notes); - } - - @Override - public void showAddNote() { - Intent intent = new Intent(getContext(), AddNoteActivity.class); - startActivityForResult(intent, REQUEST_ADD_NOTE); - } - - @Override - public void showNoteDetailUi(String noteId) { - // in it's own Activity, since it makes more sense that way and it gives us the flexibility - // to show some Intent stubbing. - Intent intent = new Intent(getContext(), NoteDetailActivity.class); - intent.putExtra(NoteDetailActivity.EXTRA_NOTE_ID, noteId); - startActivity(intent); - } - - - private static class NotesAdapter extends RecyclerView.Adapter { - - private List mNotes; - private NoteItemListener mItemListener; - - public NotesAdapter(List notes, NoteItemListener itemListener) { - setList(notes); - mItemListener = itemListener; - } - - @Override - public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - Context context = parent.getContext(); - LayoutInflater inflater = LayoutInflater.from(context); - View noteView = inflater.inflate(R.layout.item_note, parent, false); - - return new ViewHolder(noteView, mItemListener); - } - - @Override - public void onBindViewHolder(ViewHolder viewHolder, int position) { - Note note = mNotes.get(position); - - viewHolder.title.setText(note.getTitle()); - viewHolder.description.setText(note.getDescription()); - } - - public void replaceData(List notes) { - setList(notes); - notifyDataSetChanged(); - } - - private void setList(List notes) { - mNotes = checkNotNull(notes); - } - - @Override - public int getItemCount() { - return mNotes.size(); - } - - public Note getItem(int position) { - return mNotes.get(position); - } - - public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { - - public TextView title; - - public TextView description; - private NoteItemListener mItemListener; - - public ViewHolder(View itemView, NoteItemListener listener) { - super(itemView); - mItemListener = listener; - title = (TextView) itemView.findViewById(R.id.note_detail_title); - description = (TextView) itemView.findViewById(R.id.note_detail_description); - itemView.setOnClickListener(this); - } - - @Override - public void onClick(View v) { - int position = getAdapterPosition(); - Note note = getItem(position); - mItemListener.onNoteClick(note); - - } - } - } - - public interface NoteItemListener { - - void onNoteClick(Note clickedNote); - } - -} diff --git a/app/src/main/java/com/example/android/testing/notes/notes/NotesFragment.kt b/app/src/main/java/com/example/android/testing/notes/notes/NotesFragment.kt new file mode 100644 index 000000000..5f0c07caf --- /dev/null +++ b/app/src/main/java/com/example/android/testing/notes/notes/NotesFragment.kt @@ -0,0 +1,199 @@ +/* + * Copyright 2015, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.testing.notes.notes + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.support.design.widget.FloatingActionButton +import android.support.design.widget.Snackbar +import android.support.v4.app.Fragment +import android.support.v4.content.ContextCompat +import android.support.v4.widget.SwipeRefreshLayout +import android.support.v7.widget.GridLayoutManager +import android.support.v7.widget.RecyclerView +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import com.example.android.testing.notes.Injection +import com.example.android.testing.notes.R +import com.example.android.testing.notes.addnote.AddNoteActivity +import com.example.android.testing.notes.data.Note +import com.example.android.testing.notes.notedetail.NoteDetailActivity +import com.google.common.base.Preconditions.checkNotNull +import kotlinx.android.synthetic.main.activity_notes.* +import kotlinx.android.synthetic.main.fragment_notes.* +import java.util.* + +/** + * Display a grid of [Note]s + */ +class NotesFragment : Fragment(), NotesContract.View { + + private lateinit var mActionsListener: NotesContract.UserActionsListener + private lateinit var mListAdapter: NotesAdapter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + mListAdapter = NotesAdapter(ArrayList(0), onNoteClick) + mActionsListener = NotesPresenter(Injection.provideNotesRepository(), this) + } + + override fun onResume() { + super.onResume() + mActionsListener.loadNotes(false) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + retainInstance = true + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + // If a note was successfully added, show snackbar + if (REQUEST_ADD_NOTE == requestCode && Activity.RESULT_OK == resultCode) { + Snackbar.make(view!!, getString(R.string.successfully_saved_note_message), Snackbar.LENGTH_SHORT).show() + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_notes, container, false) + } + + override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + notes_list.adapter = mListAdapter + + val numColumns = context.resources.getInteger(R.integer.num_notes_columns) + + notes_list.setHasFixedSize(true) + notes_list.layoutManager = GridLayoutManager(context, numColumns) + + // Set up floating action button + val fab = activity.findViewById(R.id.fab_add_notes) as FloatingActionButton + fab.setImageResource(R.drawable.ic_add) + fab.setOnClickListener { mActionsListener.addNewNote() } + + // Pull-to-refresh + refresh_layout.setColorSchemeColors( + ContextCompat.getColor(activity, R.color.colorPrimary), + ContextCompat.getColor(activity, R.color.colorAccent), + ContextCompat.getColor(activity, R.color.colorPrimaryDark)) + refresh_layout.setOnRefreshListener { mActionsListener.loadNotes(true) } + } + + /** + * Listener for clicks on notes in the RecyclerView. + */ + internal var onNoteClick: (clickedNote: Note) -> Unit = { clickedNote: Note -> + mActionsListener.openNoteDetails(clickedNote) + } + + override fun setProgressIndicator(active: Boolean) { + if (view == null) { + return + } + + // Make sure setRefreshing() is called after the layout is done with everything else. + refresh_layout.post { refresh_layout.isRefreshing = active } + } + + override fun showNotes(notes: List) { + mListAdapter.replaceData(notes) + } + + override fun showAddNote() { + val intent = Intent(context, AddNoteActivity::class.java) + startActivityForResult(intent, REQUEST_ADD_NOTE) + } + + override fun showNoteDetailUi(noteId: String) { + // in it's own Activity, since it makes more sense that way and it gives us the flexibility + // to show some Intent stubbing. + startActivity( + Intent(context, NoteDetailActivity::class.java).apply { + putExtra(NoteDetailActivity.EXTRA_NOTE_ID, noteId) + }) + } + + + private class NotesAdapter(notes: List, private val onNoteClick: (clickedNote: Note) -> Unit) : RecyclerView.Adapter() { + + private lateinit var mNotes: List + + init { + setList(notes) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val context = parent.context + val inflater = LayoutInflater.from(context) + val noteView = inflater.inflate(R.layout.item_note, parent, false) + + return ViewHolder(noteView, onNoteClick) + } + + override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { + val note = mNotes[position] + + viewHolder.title.text = note.title + viewHolder.description.text = note.description + } + + fun replaceData(notes: List) { + setList(notes) + notifyDataSetChanged() + } + + private fun setList(notes: List) { + mNotes = notes + } + + override fun getItemCount(): Int { + return mNotes.size + } + + fun getItem(position: Int): Note { + return mNotes[position] + } + + inner class ViewHolder(itemView: View, private val onNoteClick: (clickedNote: Note) -> Unit) : RecyclerView.ViewHolder(itemView), View.OnClickListener { + + val title: TextView by lazy { itemView.findViewById(R.id.note_detail_title) as TextView } + val description: TextView by lazy { itemView.findViewById(R.id.note_detail_description) as TextView } + + init { + itemView.setOnClickListener(this) + } + + override fun onClick(v: View) { + onNoteClick(getItem(adapterPosition)) + } + } + } + + companion object { + + private val REQUEST_ADD_NOTE = 1 + + fun newInstance(): NotesFragment = NotesFragment() + } + +} +// Requires empty public constructor diff --git a/app/src/main/java/com/example/android/testing/notes/notes/NotesPresenter.java b/app/src/main/java/com/example/android/testing/notes/notes/NotesPresenter.java deleted file mode 100644 index 3bd56029d..000000000 --- a/app/src/main/java/com/example/android/testing/notes/notes/NotesPresenter.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2015, The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.android.testing.notes.notes; - -import com.example.android.testing.notes.data.Note; -import com.example.android.testing.notes.data.NotesRepository; -import com.example.android.testing.notes.util.EspressoIdlingResource; - -import android.support.annotation.NonNull; - -import java.util.List; - -import static com.google.common.base.Preconditions.checkNotNull; - - -/** - * Listens to user actions from the UI ({@link NotesFragment}), retrieves the data and updates the - * UI as required. - */ -public class NotesPresenter implements NotesContract.UserActionsListener { - - private final NotesRepository mNotesRepository; - private final NotesContract.View mNotesView; - - public NotesPresenter( - @NonNull NotesRepository notesRepository, @NonNull NotesContract.View notesView) { - mNotesRepository = checkNotNull(notesRepository, "notesRepository cannot be null"); - mNotesView = checkNotNull(notesView, "notesView cannot be null!"); - } - - @Override - public void loadNotes(boolean forceUpdate) { - mNotesView.setProgressIndicator(true); - if (forceUpdate) { - mNotesRepository.refreshData(); - } - - // The network request might be handled in a different thread so make sure Espresso knows - // that the app is busy until the response is handled. - EspressoIdlingResource.increment(); // App is busy until further notice - - mNotesRepository.getNotes(new NotesRepository.LoadNotesCallback() { - @Override - public void onNotesLoaded(List notes) { - EspressoIdlingResource.decrement(); // Set app as idle. - mNotesView.setProgressIndicator(false); - mNotesView.showNotes(notes); - } - }); - } - - @Override - public void addNewNote() { - mNotesView.showAddNote(); - } - - @Override - public void openNoteDetails(@NonNull Note requestedNote) { - checkNotNull(requestedNote, "requestedNote cannot be null!"); - mNotesView.showNoteDetailUi(requestedNote.getId()); - } - -} diff --git a/app/src/main/java/com/example/android/testing/notes/notes/NotesPresenter.kt b/app/src/main/java/com/example/android/testing/notes/notes/NotesPresenter.kt new file mode 100644 index 000000000..d74437804 --- /dev/null +++ b/app/src/main/java/com/example/android/testing/notes/notes/NotesPresenter.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2015, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.testing.notes.notes + +import com.example.android.testing.notes.data.Note +import com.example.android.testing.notes.data.NotesRepository +import com.example.android.testing.notes.util.EspressoIdlingResource + +import com.google.common.base.Preconditions.checkNotNull + + +/** + * Listens to user actions from the UI ([NotesFragment]), retrieves the data and updates the + * UI as required. + */ +class NotesPresenter( + val mNotesRepository: NotesRepository, val mNotesView: NotesContract.View) : NotesContract.UserActionsListener { + + override fun loadNotes(forceUpdate: Boolean) { + mNotesView.setProgressIndicator(true) + if (forceUpdate) { + mNotesRepository.refreshData() + } + + // The network request might be handled in a different thread so make sure Espresso knows + // that the app is busy until the response is handled. + EspressoIdlingResource.increment() // App is busy until further notice + + mNotesRepository.getNotes(object : NotesRepository.LoadNotesCallback { + override fun onNotesLoaded(notes: List?) { + EspressoIdlingResource.decrement() // Set app as idle. + mNotesView.setProgressIndicator(false) + mNotesView.showNotes(notes ?: ArrayList()) + } + }) + } + + override fun addNewNote() { + mNotesView.showAddNote() + } + + override fun openNoteDetails(requestedNote: Note) { + checkNotNull(requestedNote, "requestedNote cannot be null!") + mNotesView.showNoteDetailUi(requestedNote.id) + } + +} diff --git a/app/src/main/java/com/example/android/testing/notes/statistics/StatisticsActivity.java b/app/src/main/java/com/example/android/testing/notes/statistics/StatisticsActivity.java deleted file mode 100644 index 6edc47290..000000000 --- a/app/src/main/java/com/example/android/testing/notes/statistics/StatisticsActivity.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2015, The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.android.testing.notes.statistics; - -import com.example.android.testing.notes.R; - -import android.os.Bundle; -import android.support.v7.app.ActionBar; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; - -/** - * Show statistics for notes. At this point this is just a dummy implementation. - */ -public class StatisticsActivity extends AppCompatActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.activity_statistics); - - // Set up the toolbar. - Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - ActionBar ab = getSupportActionBar(); - ab.setTitle(R.string.statistics_title); - ab.setDisplayHomeAsUpEnabled(true); - ab.setDisplayShowHomeEnabled(true); - } - - @Override - public boolean onSupportNavigateUp() { - onBackPressed(); - return true; - } -} diff --git a/app/src/main/java/com/example/android/testing/notes/statistics/StatisticsActivity.kt b/app/src/main/java/com/example/android/testing/notes/statistics/StatisticsActivity.kt new file mode 100644 index 000000000..5dbea8306 --- /dev/null +++ b/app/src/main/java/com/example/android/testing/notes/statistics/StatisticsActivity.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2015, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.testing.notes.statistics + +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import com.example.android.testing.notes.R +import kotlinx.android.synthetic.main.activity_statistics.* + +/** + * Show statistics for notes. At this point this is just a dummy implementation. + */ +class StatisticsActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.activity_statistics) + + // Set up the toolbar. + setSupportActionBar(toolbar) + supportActionBar?.run { + setTitle(R.string.statistics_title) + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + } + + override fun onSupportNavigateUp(): Boolean { + onBackPressed() + return true + } +} diff --git a/app/src/main/java/com/example/android/testing/notes/util/EspressoIdlingResource.java b/app/src/main/java/com/example/android/testing/notes/util/EspressoIdlingResource.java deleted file mode 100644 index 10249ba5e..000000000 --- a/app/src/main/java/com/example/android/testing/notes/util/EspressoIdlingResource.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2015, The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.android.testing.notes.util; - -import android.support.test.espresso.IdlingResource; - -/** - * Contains a static reference to {@link IdlingResource}, only available in the 'mock' build type. - */ -public class EspressoIdlingResource { - - private static final String RESOURCE = "GLOBAL"; - - private static SimpleCountingIdlingResource mCountingIdlingResource = - new SimpleCountingIdlingResource(RESOURCE); - - public static void increment() { - mCountingIdlingResource.increment(); - } - - public static void decrement() { - mCountingIdlingResource.decrement(); - } - - public static IdlingResource getIdlingResource() { - return mCountingIdlingResource; - } -} diff --git a/app/src/main/java/com/example/android/testing/notes/data/NoteRepositories.java b/app/src/main/java/com/example/android/testing/notes/util/EspressoIdlingResource.kt similarity index 51% rename from app/src/main/java/com/example/android/testing/notes/data/NoteRepositories.java rename to app/src/main/java/com/example/android/testing/notes/util/EspressoIdlingResource.kt index fd348be76..96b3a32d9 100644 --- a/app/src/main/java/com/example/android/testing/notes/data/NoteRepositories.java +++ b/app/src/main/java/com/example/android/testing/notes/util/EspressoIdlingResource.kt @@ -14,25 +14,27 @@ * limitations under the License. */ -package com.example.android.testing.notes.data; +package com.example.android.testing.notes.util -import android.support.annotation.NonNull; +import android.support.test.espresso.IdlingResource -import static com.google.common.base.Preconditions.checkNotNull; +/** + * Contains a static reference to [IdlingResource], only available in the 'mock' build type. + */ +object EspressoIdlingResource { -public class NoteRepositories { + private val RESOURCE = "GLOBAL" - private NoteRepositories() { - // no instance - } + private val mCountingIdlingResource = SimpleCountingIdlingResource(RESOURCE) - private static NotesRepository repository = null; + fun increment() { + mCountingIdlingResource.increment() + } - public synchronized static NotesRepository getInMemoryRepoInstance(@NonNull NotesServiceApi notesServiceApi) { - checkNotNull(notesServiceApi); - if (null == repository) { - repository = new InMemoryNotesRepository(notesServiceApi); - } - return repository; + fun decrement() { + mCountingIdlingResource.decrement() } -} \ No newline at end of file + + val idlingResource: IdlingResource + get() = mCountingIdlingResource +} diff --git a/app/src/main/java/com/example/android/testing/notes/util/ImageFile.java b/app/src/main/java/com/example/android/testing/notes/util/ImageFile.kt similarity index 73% rename from app/src/main/java/com/example/android/testing/notes/util/ImageFile.java rename to app/src/main/java/com/example/android/testing/notes/util/ImageFile.kt index f3feacf8f..06a51daab 100644 --- a/app/src/main/java/com/example/android/testing/notes/util/ImageFile.java +++ b/app/src/main/java/com/example/android/testing/notes/util/ImageFile.kt @@ -14,19 +14,21 @@ * limitations under the License. */ -package com.example.android.testing.notes.util; +package com.example.android.testing.notes.util -import java.io.IOException; +import java.io.IOException /** * A wrapper for handling image files. */ -public interface ImageFile { - void create(String name, String extension) throws IOException; +interface ImageFile { - boolean exists(); + @Throws(IOException::class) + fun create(name: String, extension: String) - void delete(); + fun exists(): Boolean - String getPath(); + fun delete() + + val path: String } diff --git a/app/src/main/java/com/example/android/testing/notes/util/ImageFileImpl.java b/app/src/main/java/com/example/android/testing/notes/util/ImageFileImpl.kt similarity index 51% rename from app/src/main/java/com/example/android/testing/notes/util/ImageFileImpl.java rename to app/src/main/java/com/example/android/testing/notes/util/ImageFileImpl.kt index fa64e464c..aa65e1e81 100644 --- a/app/src/main/java/com/example/android/testing/notes/util/ImageFileImpl.java +++ b/app/src/main/java/com/example/android/testing/notes/util/ImageFileImpl.kt @@ -14,49 +14,45 @@ * limitations under the License. */ -package com.example.android.testing.notes.util; +package com.example.android.testing.notes.util -import android.net.Uri; -import android.os.Environment; -import android.support.annotation.VisibleForTesting; +import android.net.Uri +import android.os.Environment +import android.support.annotation.VisibleForTesting -import java.io.File; -import java.io.IOException; +import java.io.File +import java.io.IOException /** * A thin wrapper around Android file APIs to make them more testable and allows the injection of a * fake implementation for hermetic UI tests. */ -public class ImageFileImpl implements ImageFile { +open class ImageFileImpl : ImageFile { @VisibleForTesting - File mImageFile; + var mImageFile: File? = null - @Override - public void create(String name, String extension) throws IOException { - File storageDir = Environment.getExternalStoragePublicDirectory( - Environment.DIRECTORY_PICTURES); + @Throws(IOException::class) + override fun create(name: String, extension: String) { + val storageDir = Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_PICTURES) mImageFile = File.createTempFile( - name, /* prefix */ - extension, /* suffix */ + name, /* prefix */ + extension, /* suffix */ storageDir /* directory */ - ); + ) } - @Override - public boolean exists() { - return null != mImageFile && mImageFile.exists(); + override fun exists(): Boolean { + return null != mImageFile && mImageFile!!.exists() } - @Override - public void delete() { - mImageFile = null; + override fun delete() { + mImageFile = null } - @Override - public String getPath() { - return Uri.fromFile(mImageFile).toString(); - } + override val path: String + get() = Uri.fromFile(mImageFile).toString() } diff --git a/app/src/main/java/com/example/android/testing/notes/util/SimpleCountingIdlingResource.java b/app/src/main/java/com/example/android/testing/notes/util/SimpleCountingIdlingResource.kt similarity index 55% rename from app/src/main/java/com/example/android/testing/notes/util/SimpleCountingIdlingResource.java rename to app/src/main/java/com/example/android/testing/notes/util/SimpleCountingIdlingResource.kt index b58e91b55..e828497e8 100644 --- a/app/src/main/java/com/example/android/testing/notes/util/SimpleCountingIdlingResource.java +++ b/app/src/main/java/com/example/android/testing/notes/util/SimpleCountingIdlingResource.kt @@ -14,81 +14,75 @@ * limitations under the License. */ -package com.example.android.testing.notes.util; +package com.example.android.testing.notes.util -import android.support.test.espresso.IdlingResource; +import android.support.test.espresso.IdlingResource -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicInteger -import static com.google.common.base.Preconditions.checkNotNull; +import com.google.common.base.Preconditions.checkNotNull /** - * A simple counter implementation of {@link IdlingResource} that determines idleness by + * A simple counter implementation of [IdlingResource] that determines idleness by * maintaining an internal counter. When the counter is 0 - it is considered to be idle, when it is - * non-zero it is not idle. This is very similar to the way a {@link java.util.concurrent.Semaphore} + * non-zero it is not idle. This is very similar to the way a [java.util.concurrent.Semaphore] * behaves. - *

+ * + * * This class can then be used to wrap up operations that while in progress should block tests from * accessing the UI. */ -public final class SimpleCountingIdlingResource implements IdlingResource { - - private final String mResourceName; - private final AtomicInteger counter = new AtomicInteger(0); - - // written from main thread, read from any thread. - private volatile ResourceCallback resourceCallback; +class SimpleCountingIdlingResource(val mResourceName: String) : IdlingResource { /** * Creates a SimpleCountingIdlingResource - * + * @param resourceName the resource name this resource should report to Espresso. */ - public SimpleCountingIdlingResource(String resourceName) { - mResourceName = checkNotNull(resourceName); - } - @Override - public String getName() { - return mResourceName; + private val counter = AtomicInteger(0) + + // written from main thread, read from any thread. + @Volatile private var resourceCallback: IdlingResource.ResourceCallback? = null + + override fun getName(): String { + return mResourceName } - @Override - public boolean isIdleNow() { - return counter.get() == 0; + override fun isIdleNow(): Boolean { + return counter.get() == 0 } - @Override - public void registerIdleTransitionCallback(ResourceCallback resourceCallback) { - this.resourceCallback = resourceCallback; + override fun registerIdleTransitionCallback(resourceCallback: IdlingResource.ResourceCallback) { + this.resourceCallback = resourceCallback } /** * Increments the count of in-flight transactions to the resource being monitored. */ - public void increment() { - counter.getAndIncrement(); + fun increment() { + counter.getAndIncrement() } /** * Decrements the count of in-flight transactions to the resource being monitored. - * + * If this operation results in the counter falling below 0 - an exception is raised. - * + * @throws IllegalStateException if the counter is below 0. */ - public void decrement() { - int counterVal = counter.decrementAndGet(); + fun decrement() { + val counterVal = counter.decrementAndGet() if (counterVal == 0) { // we've gone from non-zero to zero. That means we're idle now! Tell espresso. if (null != resourceCallback) { - resourceCallback.onTransitionToIdle(); + resourceCallback!!.onTransitionToIdle() } } if (counterVal < 0) { - throw new IllegalArgumentException("Counter has been corrupted!"); + throw IllegalArgumentException("Counter has been corrupted!") } } } diff --git a/app/src/main/res/layout/activity_notes.xml b/app/src/main/res/layout/activity_notes.xml index c5c326f54..1258d82f7 100644 --- a/app/src/main/res/layout/activity_notes.xml +++ b/app/src/main/res/layout/activity_notes.xml @@ -21,7 +21,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".NotesActivity" + tools:context=".notes.NotesActivity" tools:openDrawer="start"> NOTES_SERVICE_DATA = new ArrayMap(); @Override - public void getAllNotes(NotesServiceCallback> callback) { + public void getAllNotes(@NotNull NotesServiceCallback> callback) { callback.onLoaded(Lists.newArrayList(NOTES_SERVICE_DATA.values())); } - @Override - public void getNote(String noteId, NotesServiceCallback callback) { + public void getNote(@NotNull String noteId, @NotNull NotesServiceCallback callback) { Note note = NOTES_SERVICE_DATA.get(noteId); callback.onLoaded(note); } diff --git a/app/src/prod/java/com/example/android/testing/notes/Injection.java b/app/src/prod/java/com/example/android/testing/notes/Injection.kt similarity index 51% rename from app/src/prod/java/com/example/android/testing/notes/Injection.java rename to app/src/prod/java/com/example/android/testing/notes/Injection.kt index 77657e972..1ff489f59 100644 --- a/app/src/prod/java/com/example/android/testing/notes/Injection.java +++ b/app/src/prod/java/com/example/android/testing/notes/Injection.kt @@ -14,25 +14,26 @@ * limitations under the License. */ -package com.example.android.testing.notes; +package com.example.android.testing.notes -import com.example.android.testing.notes.data.NoteRepositories; -import com.example.android.testing.notes.data.NotesRepository; -import com.example.android.testing.notes.data.NotesServiceApiImpl; -import com.example.android.testing.notes.util.ImageFile; -import com.example.android.testing.notes.util.ImageFileImpl; +import com.example.android.testing.notes.data.NoteRepositories +import com.example.android.testing.notes.data.NotesRepository +import com.example.android.testing.notes.data.NotesServiceApiImpl +import com.example.android.testing.notes.util.ImageFile +import com.example.android.testing.notes.util.ImageFileImpl /** - * Enables injection of production implementations for {@link ImageFile} and - * {@link NotesRepository} at compile time. + * Enables injection of production implementations for [ImageFile] and + * [NotesRepository] at compile time. */ -public class Injection { +object Injection { - public static ImageFile provideImageFile() { - return new ImageFileImpl(); + fun provideImageFile(): ImageFile { + return ImageFileImpl() } - public static NotesRepository provideNotesRepository() { - return NoteRepositories.getInMemoryRepoInstance(new NotesServiceApiImpl()); + fun provideNotesRepository(): NotesRepository { + return NoteRepositories.getInMemoryRepoInstance(NotesServiceApiImpl()) } + } diff --git a/app/src/test/java/com/example/android/testing/notes/addnote/AddNotePresenterTest.java b/app/src/test/java/com/example/android/testing/notes/addnote/AddNotePresenterTest.java index ddb8ae8d6..27722fe9f 100644 --- a/app/src/test/java/com/example/android/testing/notes/addnote/AddNotePresenterTest.java +++ b/app/src/test/java/com/example/android/testing/notes/addnote/AddNotePresenterTest.java @@ -14,8 +14,10 @@ * limitations under the License. */ + package com.example.android.testing.notes.addnote; + import com.example.android.testing.notes.data.Note; import com.example.android.testing.notes.data.NotesRepository; import com.example.android.testing.notes.util.ImageFile; @@ -26,6 +28,9 @@ import org.mockito.MockitoAnnotations; import java.io.IOException; +import java.util.Objects; + +import kotlin.jvm.internal.Intrinsics; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; @@ -33,97 +38,98 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; + /** * Unit tests for the implementation of {@link AddNotePresenter}. */ public class AddNotePresenterTest { - @Mock - private NotesRepository mNotesRepository; - - @Mock - private ImageFile mImageFile; - - @Mock - private AddNoteContract.View mAddNoteView; - - private AddNotePresenter mAddNotesPresenter; - - @Before - public void setupAddNotePresenter() { - // Mockito has a very convenient way to inject mocks by using the @Mock annotation. To - // inject the mocks in the test the initMocks method needs to be called. - MockitoAnnotations.initMocks(this); - - // Get a reference to the class under test - mAddNotesPresenter = new AddNotePresenter(mNotesRepository, mAddNoteView, mImageFile); - } - - @Test - public void saveNoteToRepository_showsSuccessMessageUi() { - // When the presenter is asked to save a note - mAddNotesPresenter.saveNote("New Note Title", "Some Note Description"); - - // Then a note is, - verify(mNotesRepository).saveNote(any(Note.class)); // saved to the model - verify(mAddNoteView).showNotesList(); // shown in the UI - } - - @Test - public void saveNote_emptyNoteShowsErrorUi() { - // When the presenter is asked to save an empty note - mAddNotesPresenter.saveNote("", ""); - - // Then an empty not error is shown in the UI - verify(mAddNoteView).showEmptyNoteError(); - } - - @Test - public void takePicture_CreatesFileAndOpensCamera() throws IOException { - // When the presenter is asked to take an image - mAddNotesPresenter.takePicture(); - - // Then an image file is created snd camera is opened - verify(mImageFile).create(anyString(), anyString()); - verify(mImageFile).getPath(); - verify(mAddNoteView).openCamera(anyString()); - } - - @Test - public void imageAvailable_SavesImageAndUpdatesUiWithThumbnail() { - // Given an a stubbed image file - String imageUrl = "path/to/file"; - when(mImageFile.exists()).thenReturn(true); - when(mImageFile.getPath()).thenReturn(imageUrl); - - // When an image is made available to the presenter - mAddNotesPresenter.imageAvailable(); - - // Then the preview image of the stubbed image is shown in the UI - verify(mAddNoteView).showImagePreview(contains(imageUrl)); - } - - @Test - public void imageAvailable_FileDoesNotExistShowsErrorUi() { - // Given the image file does not exist - when(mImageFile.exists()).thenReturn(false); - - // When an image is made available to the presenter - mAddNotesPresenter.imageAvailable(); - - // Then an error is shown in the UI and the image file is deleted - verify(mAddNoteView).showImageError(); - verify(mImageFile).delete(); - } - - @Test - public void noImageAvailable_ShowsErrorUi() { - // When the presenter is notified that image capturing failed - mAddNotesPresenter.imageCaptureFailed(); - - // Then an error is shown in the UI and the image file is deleted - verify(mAddNoteView).showImageError(); - verify(mImageFile).delete(); - } + @Mock + private NotesRepository mNotesRepository; + + @Mock + private ImageFile mImageFile; + + @Mock + private AddNoteContract.View mAddNoteView; + + private AddNotePresenter mAddNotesPresenter; + + @Before + public void setupAddNotePresenter() { + // Mockito has a very convenient way to inject mocks by using the @Mock annotation. To + // inject the mocks in the test the initMocks method needs to be called. + MockitoAnnotations.initMocks(this); + + // Get a reference to the class under test + mAddNotesPresenter = new AddNotePresenter(mNotesRepository, mAddNoteView, mImageFile); + } + + @Test + public void saveNoteToRepository_showsSuccessMessageUi() { + // When the presenter is asked to save a note + mAddNotesPresenter.saveNote("New Note Title", "Some Note Description"); + + // Then a note is, + verify(mNotesRepository).saveNote(any(Note.class)); // saved to the model + verify(mAddNoteView).showNotesList(); // shown in the UI + } + + @Test + public void saveNote_emptyNoteShowsErrorUi() { + // When the presenter is asked to save an empty note + mAddNotesPresenter.saveNote("", ""); + + // Then an empty not error is shown in the UI + verify(mAddNoteView).showEmptyNoteError(); + } + + @Test + public void takePicture_CreatesFileAndOpensCamera() throws IOException { + // When the presenter is asked to take an image + mAddNotesPresenter.takePicture(); + + // Then an image file is created snd camera is opened + verify(mImageFile).create(anyString(), anyString()); + verify(mImageFile).getPath(); + verify(mAddNoteView).openCamera(anyString()); + } + + @Test + public void imageAvailable_SavesImageAndUpdatesUiWithThumbnail() { + // Given an a stubbed image file + String imageUrl = "path/to/file"; + when(mImageFile.exists()).thenReturn(true); + when(mImageFile.getPath()).thenReturn(imageUrl); + + // When an image is made available to the presenter + mAddNotesPresenter.imageAvailable(); + + // Then the preview image of the stubbed image is shown in the UI + verify(mAddNoteView).showImagePreview(contains(imageUrl)); + } + + @Test + public void imageAvailable_FileDoesNotExistShowsErrorUi() { + // Given the image file does not exist + when(mImageFile.exists()).thenReturn(false); + + // When an image is made available to the presenter + mAddNotesPresenter.imageAvailable(); + + // Then an error is shown in the UI and the image file is deleted + verify(mAddNoteView).showImageError(); + verify(mImageFile).delete(); + } + + @Test + public void noImageAvailable_ShowsErrorUi() { + // When the presenter is notified that image capturing failed + mAddNotesPresenter.imageCaptureFailed(); + + // Then an error is shown in the UI and the image file is deleted + verify(mAddNoteView).showImageError(); + verify(mImageFile).delete(); + } } diff --git a/app/src/test/java/com/example/android/testing/notes/data/InMemoryNotesRepositoryTest.java b/app/src/test/java/com/example/android/testing/notes/data/InMemoryNotesRepositoryTest.java deleted file mode 100644 index 5e9a6ba77..000000000 --- a/app/src/test/java/com/example/android/testing/notes/data/InMemoryNotesRepositoryTest.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright 2015, The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.android.testing.notes.data; - -import com.google.common.collect.Lists; - -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.List; - -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.Assert.assertThat; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -/** - * Unit tests for the implementation of the in-memory repository with cache. - */ -public class InMemoryNotesRepositoryTest { - - private final static String NOTE_TITLE = "title"; - - private static List NOTES = Lists.newArrayList(new Note("Title1", "Description1"), - new Note("Title2", "Description2")); - - private InMemoryNotesRepository mNotesRepository; - - @Mock - private NotesServiceApiImpl mServiceApi; - - @Mock - private NotesRepository.GetNoteCallback mGetNoteCallback; - - @Mock - private NotesRepository.LoadNotesCallback mLoadNotesCallback; - - /** - * {@link ArgumentCaptor} is a powerful Mockito API to capture argument values and use them to - * perform further actions or assertions on them. - */ - @Captor - private ArgumentCaptor mNotesServiceCallbackCaptor; - - @Before - public void setupNotesRepository() { - // Mockito has a very convenient way to inject mocks by using the @Mock annotation. To - // inject the mocks in the test the initMocks method needs to be called. - MockitoAnnotations.initMocks(this); - - // Get a reference to the class under test - mNotesRepository = new InMemoryNotesRepository(mServiceApi); - } - - @Test - public void getNotes_repositoryCachesAfterFirstApiCall() { - // Given a setup Captor to capture callbacks - // When two calls are issued to the notes repository - twoLoadCallsToRepository(mLoadNotesCallback); - - // Then notes where only requested once from Service API - verify(mServiceApi).getAllNotes(any(NotesServiceApi.NotesServiceCallback.class)); - } - - @Test - public void invalidateCache_DoesNotCallTheServiceApi() { - // Given a setup Captor to capture callbacks - twoLoadCallsToRepository(mLoadNotesCallback); - - // When data refresh is requested - mNotesRepository.refreshData(); - mNotesRepository.getNotes(mLoadNotesCallback); // Third call to API - - // The notes where requested twice from the Service API (Caching on first and third call) - verify(mServiceApi, times(2)).getAllNotes(any(NotesServiceApi.NotesServiceCallback.class)); - } - - @Test - public void getNotes_requestsAllNotesFromServiceApi() { - // When notes are requested from the notes repository - mNotesRepository.getNotes(mLoadNotesCallback); - - // Then notes are loaded from the service API - verify(mServiceApi).getAllNotes(any(NotesServiceApi.NotesServiceCallback.class)); - } - - @Test - public void saveNote_savesNoteToServiceAPIAndInvalidatesCache() { - // Given a stub note with title and description - Note newNote = new Note(NOTE_TITLE, "Some Note Description"); - - // When a note is saved to the notes repository - mNotesRepository.saveNote(newNote); - - // Then the notes cache is cleared - assertThat(mNotesRepository.mCachedNotes, is(nullValue())); - } - - @Test - public void getNote_requestsSingleNoteFromServiceApi() { - // When a note is requested from the notes repository - mNotesRepository.getNote(NOTE_TITLE, mGetNoteCallback); - - // Then the note is loaded from the service API - verify(mServiceApi).getNote(eq(NOTE_TITLE), any(NotesServiceApi.NotesServiceCallback.class)); - } - - /** - * Convenience method that issues two calls to the notes repository - */ - private void twoLoadCallsToRepository(NotesRepository.LoadNotesCallback callback) { - // When notes are requested from repository - mNotesRepository.getNotes(callback); // First call to API - - // Use the Mockito Captor to capture the callback - verify(mServiceApi).getAllNotes(mNotesServiceCallbackCaptor.capture()); - - // Trigger callback so notes are cached - mNotesServiceCallbackCaptor.getValue().onLoaded(NOTES); - - mNotesRepository.getNotes(callback); // Second call to API - } - -} \ No newline at end of file diff --git a/app/src/test/java/com/example/android/testing/notes/data/InMemoryNotesRepositoryTest.kt b/app/src/test/java/com/example/android/testing/notes/data/InMemoryNotesRepositoryTest.kt new file mode 100644 index 000000000..5497c1584 --- /dev/null +++ b/app/src/test/java/com/example/android/testing/notes/data/InMemoryNotesRepositoryTest.kt @@ -0,0 +1,147 @@ +/* + * Copyright 2015, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.example.android.testing.notes.data + + +import com.google.common.collect.Lists +import org.hamcrest.Matchers.`is` +import org.hamcrest.Matchers.nullValue +import org.junit.Assert.assertThat +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Matchers.any +import org.mockito.Matchers.eq +import org.mockito.Mock +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + + +/** + * Unit tests for the implementation of the in-memory repository with cache. + */ +class InMemoryNotesRepositoryTest { + + private var mNotesRepository: InMemoryNotesRepository? = null + + @Mock + private lateinit var mServiceApi: NotesServiceApiImpl + + @Mock + private lateinit var mGetNoteCallback: NotesRepository.GetNoteCallback + + @Mock + private lateinit var mLoadNotesCallback: NotesRepository.LoadNotesCallback + + /** + * [ArgumentCaptor] is a powerful Mockito API to capture argument values and use them to + * perform further actions or assertions on them. + */ + @Captor + private val mNotesServiceCallbackCaptor: ArgumentCaptor>? = null + + @Before + fun setupNotesRepository() { + // Mockito has a very convenient way to inject mocks by using the @Mock annotation. To + // inject the mocks in the test the initMocks method needs to be called. + MockitoAnnotations.initMocks(this) + + // Get a reference to the class under test + mNotesRepository = InMemoryNotesRepository(mServiceApi) + } + + @Test + fun getNotes_repositoryCachesAfterFirstApiCall() { + // Given a setup Captor to capture callbacks + // When two calls are issued to the notes repository + twoLoadCallsToRepository(mLoadNotesCallback) + + // Then notes where only requested once from Service API + verify(mServiceApi).getAllNotes(any(NotesServiceApi.NotesServiceCallback<*>::class.java)) + } + + @Test + fun invalidateCache_DoesNotCallTheServiceApi() { + // Given a setup Captor to capture callbacks + twoLoadCallsToRepository(mLoadNotesCallback) + + // When data refresh is requested + mNotesRepository!!.refreshData() + mNotesRepository!!.getNotes(mLoadNotesCallback) // Third call to API + + // The notes where requested twice from the Service API (Caching on first and third call) + verify(mServiceApi, times(2)).getAllNotes(any(NotesServiceApi.NotesServiceCallback<*>::class.java)) + } + + @Test + fun getNotes_requestsAllNotesFromServiceApi() { + // When notes are requested from the notes repository + mNotesRepository!!.getNotes(mLoadNotesCallback) + + // Then notes are loaded from the service API + verify(mServiceApi).getAllNotes(any(NotesServiceApi.NotesServiceCallback<*>::class.java)) + } + + @Test + fun saveNote_savesNoteToServiceAPIAndInvalidatesCache() { + // Given a stub note with title and description + val newNote = Note(NOTE_TITLE, "Some Note Description") + + // When a note is saved to the notes repository + mNotesRepository!!.saveNote(newNote) + + // Then the notes cache is cleared + assertThat>(mNotesRepository!!.mCachedNotes, `is`(nullValue())) + } + + @Test + fun getNote_requestsSingleNoteFromServiceApi() { + // When a note is requested from the notes repository + mNotesRepository!!.getNote(NOTE_TITLE, mGetNoteCallback) + + // Then the note is loaded from the service API + verify(mServiceApi).getNote(eq(NOTE_TITLE), any(NotesServiceApi.NotesServiceCallback<*>::class.java)) + } + + /** + * Convenience method that issues two calls to the notes repository + */ + private fun twoLoadCallsToRepository(callback: NotesRepository.LoadNotesCallback) { + // When notes are requested from repository + mNotesRepository!!.getNotes(callback) // First call to API + + // Use the Mockito Captor to capture the callback + verify(mServiceApi).getAllNotes(mNotesServiceCallbackCaptor!!.capture()) + + // Trigger callback so notes are cached + mNotesServiceCallbackCaptor.value.onLoaded(NOTES) + + mNotesRepository!!.getNotes(callback) // Second call to API + } + + companion object { + + private val NOTE_TITLE = "title" + + private val NOTES = Lists.newArrayList(Note("Title1", "Description1"), + Note("Title2", "Description2")) + } + +} \ No newline at end of file diff --git a/app/src/test/java/com/example/android/testing/notes/notes/NotesPresenterTest.java b/app/src/test/java/com/example/android/testing/notes/notes/NotesPresenterTest.java deleted file mode 100644 index a8047d1b1..000000000 --- a/app/src/test/java/com/example/android/testing/notes/notes/NotesPresenterTest.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2015, The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.android.testing.notes.notes; - -import com.google.common.collect.Lists; - -import com.example.android.testing.notes.data.Note; -import com.example.android.testing.notes.data.NotesRepository; -import com.example.android.testing.notes.data.NotesRepository.LoadNotesCallback; - -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.InOrder; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -import java.util.ArrayList; -import java.util.List; - -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.verify; - -/** - * Unit tests for the implementation of {@link NotesPresenter} - */ -public class NotesPresenterTest { - - private static List NOTES = Lists.newArrayList(new Note("Title1", "Description1"), - new Note("Title2", "Description2")); - - private static List EMPTY_NOTES = new ArrayList<>(0); - - @Mock - private NotesRepository mNotesRepository; - - @Mock - private NotesContract.View mNotesView; - - /** - * {@link ArgumentCaptor} is a powerful Mockito API to capture argument values and use them to - * perform further actions or assertions on them. - */ - @Captor - private ArgumentCaptor mLoadNotesCallbackCaptor; - - private NotesPresenter mNotesPresenter; - - @Before - public void setupNotesPresenter() { - // Mockito has a very convenient way to inject mocks by using the @Mock annotation. To - // inject the mocks in the test the initMocks method needs to be called. - MockitoAnnotations.initMocks(this); - - // Get a reference to the class under test - mNotesPresenter = new NotesPresenter(mNotesRepository, mNotesView); - } - - @Test - public void loadNotesFromRepositoryAndLoadIntoView() { - // Given an initialized NotesPresenter with initialized notes - // When loading of Notes is requested - mNotesPresenter.loadNotes(true); - - // Callback is captured and invoked with stubbed notes - verify(mNotesRepository).getNotes(mLoadNotesCallbackCaptor.capture()); - mLoadNotesCallbackCaptor.getValue().onNotesLoaded(NOTES); - - // Then progress indicator is hidden and notes are shown in UI - InOrder inOrder = Mockito.inOrder(mNotesView); - inOrder.verify(mNotesView).setProgressIndicator(true); - inOrder.verify(mNotesView).setProgressIndicator(false); - verify(mNotesView).showNotes(NOTES); - } - - @Test - public void clickOnFab_ShowsAddsNoteUi() { - // When adding a new note - mNotesPresenter.addNewNote(); - - // Then add note UI is shown - verify(mNotesView).showAddNote(); - } - - @Test - public void clickOnNote_ShowsDetailUi() { - // Given a stubbed note - Note requestedNote = new Note("Details Requested", "For this note"); - - // When open note details is requested - mNotesPresenter.openNoteDetails(requestedNote); - - // Then note detail UI is shown - verify(mNotesView).showNoteDetailUi(any(String.class)); - } -} diff --git a/app/src/test/java/com/example/android/testing/notes/notes/NotesPresenterTest.kt b/app/src/test/java/com/example/android/testing/notes/notes/NotesPresenterTest.kt new file mode 100644 index 000000000..d14bafb9b --- /dev/null +++ b/app/src/test/java/com/example/android/testing/notes/notes/NotesPresenterTest.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2015, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.testing.notes.notes + +import com.example.android.testing.notes.data.Note +import com.example.android.testing.notes.data.NotesRepository +import com.example.android.testing.notes.data.NotesRepository.LoadNotesCallback +import com.example.android.testing.notes.notes.NotesContract.View +import com.google.common.collect.Lists +import org.junit.Before +import org.junit.Test +import org.mockito.* +import org.mockito.Matchers.any +import org.mockito.Mockito.verify +import java.util.* + +/** + * Unit tests for the implementation of [NotesPresenter] + */ +class NotesPresenterTest { + + @Mock + private val mNotesRepository: NotesRepository? = null + + @Mock + private val mNotesView: NotesContract.View? = null + + /** + * [ArgumentCaptor] is a powerful Mockito API to capture argument values and use them to + * perform further actions or assertions on them. + */ + @Captor + private val mLoadNotesCallbackCaptor: ArgumentCaptor? = null + + private var mNotesPresenter: NotesPresenter? = null + + @Before + fun setupNotesPresenter() { + // Mockito has a very convenient way to inject mocks by using the @Mock annotation. To + // inject the mocks in the test the initMocks method needs to be called. + MockitoAnnotations.initMocks(this) + + // Get a reference to the class under test + mNotesPresenter = NotesPresenter(mNotesRepository!!, mNotesView!!) + } + + @Test + fun loadNotesFromRepositoryAndLoadIntoView() { + // Given an initialized NotesPresenter with initialized notes + // When loading of Notes is requested + mNotesPresenter!!.loadNotes(true) + + // Callback is captured and invoked with stubbed notes + verify(mNotesRepository).getNotes(mLoadNotesCallbackCaptor!!.capture()) + mLoadNotesCallbackCaptor.value.onNotesLoaded(NOTES) + + // Then progress indicator is hidden and notes are shown in UI + val inOrder = Mockito.inOrder(mNotesView) + inOrder.verify(mNotesView).setProgressIndicator(true) + inOrder.verify(mNotesView).setProgressIndicator(false) + verify(mNotesView).showNotes(NOTES) + } + + @Test + fun clickOnFab_ShowsAddsNoteUi() { + // When adding a new note + mNotesPresenter!!.addNewNote() + + // Then add note UI is shown + verify(mNotesView).showAddNote() + } + + @Test + fun clickOnNote_ShowsDetailUi() { + // Given a stubbed note + val requestedNote = Note("Details Requested", "For this note") + + // When open note details is requested + mNotesPresenter!!.openNoteDetails(requestedNote) + + // Then note detail UI is shown + verify(mNotesView).showNoteDetailUi(any(String::class.java)) + } + + companion object { + + private val NOTES = Lists.newArrayList(Note("Title1", "Description1"), + Note("Title2", "Description2")) + + private val EMPTY_NOTES = ArrayList(0) + } +} diff --git a/app/src/test/java/com/example/android/testing/notes/util/ImageFileTest.java b/app/src/test/java/com/example/android/testing/notes/util/ImageFileTest.kt similarity index 54% rename from app/src/test/java/com/example/android/testing/notes/util/ImageFileTest.java rename to app/src/test/java/com/example/android/testing/notes/util/ImageFileTest.kt index 979e1a26c..46b9a1d71 100644 --- a/app/src/test/java/com/example/android/testing/notes/util/ImageFileTest.java +++ b/app/src/test/java/com/example/android/testing/notes/util/ImageFileTest.kt @@ -14,95 +14,104 @@ * limitations under the License. */ -package com.example.android.testing.notes.util; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; +package com.example.android.testing.notes.util -import android.os.Environment; -import java.io.File; -import java.io.IOException; +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.powermock.core.classloader.annotations.PrepareForTest +import org.powermock.modules.junit4.PowerMockRunner + +import android.os.Environment + +import java.io.File +import java.io.IOException + +import org.hamcrest.CoreMatchers.`is` +import org.hamcrest.CoreMatchers.notNullValue +import org.hamcrest.CoreMatchers.nullValue +import org.junit.Assert.assertThat +import org.mockito.Matchers.anyString +import org.mockito.Matchers.eq +import org.powermock.api.mockito.PowerMockito.mockStatic +import org.powermock.api.mockito.PowerMockito.`when` -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.junit.Assert.assertThat; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.eq; -import static org.powermock.api.mockito.PowerMockito.mockStatic; -import static org.powermock.api.mockito.PowerMockito.when; /** - * Unit tests for the implementation of {@link ImageFileImpl}. - *

+ * Unit tests for the implementation of [ImageFileImpl]. + * + * * The current Android tools support for writing unit tests is limited and requires mocking of all * Android dependencies in unit tests. That's why unit tests ideally should not have any * dependencies into android.jar, but sometimes they are inevitable. Usually using a wrapper class * or using a mocking framework like Mockito works fine, but there are situations where these * frameworks fall short, for instance when working with static util classes in the android.jar. * - *

+ * + * + * * To work around that limitation this test uses Powermockito, a library which adds support for * mocking static methods to Mockito. Powermockito should be used with care since it is normally a * sign of a bad code design. Nevertheless it can be handy while working with third party * dependencies, like the android.jar. */ -@RunWith(PowerMockRunner.class) -@PrepareForTest({Environment.class, File.class}) // Prepare the static classes for mocking -public class ImageFileTest { +@RunWith(PowerMockRunner::class) +@PrepareForTest(Environment::class, File::class) // Prepare the static classes for mocking +class ImageFileTest { @Mock - private File mDirectory; + private val mDirectory: File? = null @Mock - private File mImageFile; + private val mImageFile: File? = null - private ImageFileImpl mFileHelper; + private var mFileHelper: ImageFileImpl? = null @Before - public void createImageFile() throws IOException { + @Throws(IOException::class) + fun createImageFile() { // Get a reference to the class under test - mFileHelper = new ImageFileImpl(); + mFileHelper = ImageFileImpl() // Setup required static mocking - withStaticallyMockedEnvironmentAndFileApis(); + withStaticallyMockedEnvironmentAndFileApis() } @Test - public void create_SetsImageFile() throws IOException { + @Throws(IOException::class) + fun create_SetsImageFile() { // When file helper is asked to create a file - mFileHelper.create("Name", "Extension"); + mFileHelper!!.create("Name", "Extension") // Then the created file is stored inside the image file. - assertThat(mFileHelper.mImageFile, is(notNullValue())); + assertThat(mFileHelper!!.mImageFile, `is`(notNullValue())) } @Test - public void deleteImageFile() { + fun deleteImageFile() { // When file should be deleted - mFileHelper.delete(); + mFileHelper!!.delete() // Then stored file is deleted - assertThat(mFileHelper.mImageFile, is(nullValue())); + assertThat(mFileHelper!!.mImageFile, `is`(nullValue())) } /** * Mock static methods in android.jar */ - private void withStaticallyMockedEnvironmentAndFileApis() throws IOException { + @Throws(IOException::class) + private fun withStaticallyMockedEnvironmentAndFileApis() { // Setup mocking for Environment and File classes - mockStatic(Environment.class, File.class); + mockStatic(Environment::class.java, File::class.java) // Make the Environment class return a mocked external storage directory - when(Environment.getExternalStorageDirectory()) - .thenReturn(mDirectory); + `when`(Environment.getExternalStorageDirectory()) + .thenReturn(mDirectory) // Make the File class return a mocked image file - when(File.createTempFile(anyString(), anyString(), eq(mDirectory))).thenReturn(mImageFile); + `when`(File.createTempFile(anyString(), anyString(), eq(mDirectory))).thenReturn(mImageFile) } } diff --git a/build.gradle b/build.gradle index 7f26d06e9..7afdf8645 100644 --- a/build.gradle +++ b/build.gradle @@ -1,9 +1,12 @@ buildscript { + ext.kotlin_version = '1.1.3-2' + repositories { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.3.0' + classpath 'com.android.tools.build:gradle:3.0.0-alpha3' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f41ea62b4..5ffac5e50 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed May 03 13:10:53 CEST 2017 +#Sun Jul 23 00:26:44 KST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.0-milestone-1-all.zip