├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── techyourchance │ │ └── android_mvc_tutorial │ │ ├── MvcTutorialApplication.java │ │ ├── common │ │ ├── BaseObservable.java │ │ ├── dependencyinjection │ │ │ ├── ActivityCompositionRoot.java │ │ │ └── ApplicationCompositionRoot.java │ │ └── permissions │ │ │ ├── MyPermission.java │ │ │ └── PermissionsHelper.java │ │ ├── screens │ │ ├── ScreensNavigator.java │ │ ├── common │ │ │ ├── BaseFragment.java │ │ │ ├── BaseViewMvc.java │ │ │ ├── ViewMvc.java │ │ │ ├── ViewMvcFactory.java │ │ │ └── toolbar │ │ │ │ └── MyToolbar.java │ │ ├── main │ │ │ └── MainActivity.java │ │ ├── smsall │ │ │ ├── SmsAllFragment.java │ │ │ ├── SmsAllListAdapter.java │ │ │ ├── SmsAllViewMvc.java │ │ │ ├── SmsAllViewMvcImpl.java │ │ │ └── smslistitem │ │ │ │ ├── SmsListItemViewMvc.java │ │ │ │ └── SmsListItemViewMvcImpl.java │ │ └── smsdetails │ │ │ ├── SmsDetailsFragment.java │ │ │ ├── SmsDetailsViewMvc.java │ │ │ └── SmsDetailsViewMvcImpl.java │ │ └── sms │ │ ├── DefaultSmsAppTester.java │ │ ├── FetchAllSmsUseCase.java │ │ ├── FetchSmsByIdUseCase.java │ │ ├── FetchSmsUseCaseSync.java │ │ ├── MarkSmsAsReadUseCase.java │ │ └── SmsMessage.java │ └── res │ ├── drawable │ └── ic_navigate_up.xml │ ├── layout │ ├── layout_my_toolbar.xml │ ├── layout_single_frame.xml │ ├── layout_sms_all.xml │ ├── layout_sms_details.xml │ └── layout_sms_list_item.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | #built application files 3 | *.apk 4 | *.ap_ 5 | 6 | # files for the dex VM 7 | *.dex 8 | 9 | # Java class files 10 | *.class 11 | 12 | # generated files 13 | bin/ 14 | gen/ 15 | 16 | # Local configuration file (sdk path, etc) 17 | local.properties 18 | 19 | # Windows thumbnail db 20 | Thumbs.db 21 | 22 | # OSX files 23 | .DS_Store 24 | 25 | # Android Studio 26 | .idea 27 | .gradle 28 | build/ 29 | *.iml -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android MVC Tutorial 2 | 3 | This simple SMS inbox reader application demonstrates the best implementation of MVC architectural pattern in Android. 4 | 5 | # Detailed Walkthrough 6 | 7 | I wrote a series of posts about this particular implementation of MVC for Android in a [blog](http://www.techyourchance.com/mvc-and-mvp-architectural-patterns-in-android-part-1/). 8 | 9 | # Professional Course 10 | 11 | If you're looking for an architecture for a professional project, or just want to understand the topic of Android architecture at a professional level, check out my [course about Android architecture](https://go.techyourchance.com/android-architecture-course-mvc-github). 12 | 13 | # License 14 | 15 | android_mvc_tutorial binaries and source code can be used according to the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0). 16 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 28 5 | buildToolsVersion "28.0.3" 6 | 7 | defaultConfig { 8 | applicationId "com.techyourchance.android_mvc_template" 9 | minSdkVersion 19 10 | targetSdkVersion 28 11 | versionCode 1 12 | versionName "1.0" 13 | 14 | vectorDrawables.useSupportLibrary = true 15 | } 16 | 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | 24 | compileOptions { 25 | sourceCompatibility = 1.8 26 | targetCompatibility = 1.8 27 | } 28 | } 29 | 30 | dependencies { 31 | implementation fileTree(dir: 'libs', include: ['*.jar']) 32 | 33 | implementation 'androidx.appcompat:appcompat:1.1.0' 34 | 35 | // Multithreading 36 | implementation 'com.techyourchance.threadposter:threadposter:0.8.3' 37 | 38 | // Fragment navigation 39 | implementation 'com.ncapdevi:frag-nav:3.2.0' 40 | } 41 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in C:\Users\Vasiliy\AppData\Local\Android\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/android_mvc_tutorial/MvcTutorialApplication.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.android_mvc_tutorial; 2 | 3 | import android.app.Application; 4 | 5 | import com.techyourchance.android_mvc_tutorial.common.dependencyinjection.ApplicationCompositionRoot; 6 | 7 | public class MvcTutorialApplication extends Application { 8 | 9 | private ApplicationCompositionRoot mApplicationCompositionRoot; 10 | 11 | public ApplicationCompositionRoot getApplicationCompositionRoot() { 12 | if (mApplicationCompositionRoot == null) { 13 | mApplicationCompositionRoot = new ApplicationCompositionRoot(this); 14 | } 15 | return mApplicationCompositionRoot; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/android_mvc_tutorial/common/BaseObservable.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.android_mvc_tutorial.common; 2 | 3 | import java.util.Collections; 4 | import java.util.HashSet; 5 | import java.util.Set; 6 | 7 | public abstract class BaseObservable { 8 | 9 | private final Object MONITOR = new Object(); 10 | 11 | private final Set mListeners = new HashSet<>(); 12 | 13 | public void registerListener(LISTENER_CLASS listener) { 14 | synchronized (MONITOR) { 15 | boolean hadNoListeners = mListeners.size() == 0; 16 | mListeners.add(listener); 17 | if (hadNoListeners && mListeners.size() == 1) { 18 | onFirstListenerRegistered(); 19 | } 20 | } 21 | } 22 | 23 | public void unregisterListener(LISTENER_CLASS listener) { 24 | synchronized (MONITOR) { 25 | boolean hadOneListener = mListeners.size() == 1; 26 | mListeners.remove(listener); 27 | if (hadOneListener && mListeners.size() == 0) { 28 | onLastListenerUnregistered(); 29 | } 30 | } 31 | } 32 | 33 | protected Set getListeners() { 34 | synchronized (MONITOR) { 35 | return Collections.unmodifiableSet(new HashSet<>(mListeners)); 36 | } 37 | } 38 | 39 | protected void onFirstListenerRegistered() { 40 | 41 | } 42 | 43 | protected void onLastListenerUnregistered() { 44 | 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/android_mvc_tutorial/common/dependencyinjection/ActivityCompositionRoot.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.android_mvc_tutorial.common.dependencyinjection; 2 | 3 | import android.content.ContentResolver; 4 | import android.view.LayoutInflater; 5 | 6 | import com.ncapdevi.fragnav.FragNavController; 7 | import com.techyourchance.android_mvc_tutorial.R; 8 | import com.techyourchance.android_mvc_tutorial.common.permissions.PermissionsHelper; 9 | import com.techyourchance.android_mvc_tutorial.screens.ScreensNavigator; 10 | import com.techyourchance.android_mvc_tutorial.screens.common.ViewMvcFactory; 11 | import com.techyourchance.android_mvc_tutorial.sms.DefaultSmsAppTester; 12 | import com.techyourchance.android_mvc_tutorial.sms.FetchAllSmsUseCase; 13 | import com.techyourchance.android_mvc_tutorial.sms.FetchSmsByIdUseCase; 14 | import com.techyourchance.android_mvc_tutorial.sms.FetchSmsUseCaseSync; 15 | import com.techyourchance.android_mvc_tutorial.sms.MarkSmsAsReadUseCase; 16 | import com.techyourchance.threadposter.BackgroundThreadPoster; 17 | import com.techyourchance.threadposter.UiThreadPoster; 18 | 19 | import androidx.appcompat.app.AppCompatActivity; 20 | 21 | public class ActivityCompositionRoot { 22 | 23 | private final ApplicationCompositionRoot mApplicationCompositionRoot; 24 | private final AppCompatActivity mActivity; 25 | 26 | private PermissionsHelper mPermissionsHelper; 27 | private ScreensNavigator mScreensNavigator; 28 | 29 | public ActivityCompositionRoot(ApplicationCompositionRoot applicationCompositionRoot, AppCompatActivity activity) { 30 | mApplicationCompositionRoot = applicationCompositionRoot; 31 | mActivity = activity; 32 | } 33 | 34 | public PermissionsHelper getPermissionsHelper() { 35 | if (mPermissionsHelper == null) { 36 | mPermissionsHelper = new PermissionsHelper(mActivity); 37 | } 38 | return mPermissionsHelper; 39 | } 40 | 41 | public ViewMvcFactory getViewMvcFactory() { 42 | return new ViewMvcFactory(LayoutInflater.from(mActivity)); 43 | } 44 | 45 | private ContentResolver getContentResolver() { 46 | return mActivity.getContentResolver(); 47 | } 48 | 49 | private BackgroundThreadPoster getBackgroundThreadPoster() { 50 | return mApplicationCompositionRoot.getBackgroundThreadPoster(); 51 | } 52 | 53 | private UiThreadPoster getUiThreadPoster() { 54 | return mApplicationCompositionRoot.getUiThreadPoster(); 55 | } 56 | 57 | public ScreensNavigator getScreenNavigator() { 58 | if (mScreensNavigator == null) { 59 | mScreensNavigator = new ScreensNavigator( 60 | new FragNavController(mActivity.getSupportFragmentManager(), R.id.frame_contents) 61 | ); 62 | } 63 | return mScreensNavigator; 64 | } 65 | 66 | public FetchAllSmsUseCase getFetchAllSmsUseCase() { 67 | return new FetchAllSmsUseCase( 68 | getUiThreadPoster(), 69 | getBackgroundThreadPoster(), 70 | getFetchSmsUseCaseSync() 71 | ); 72 | } 73 | 74 | public FetchSmsByIdUseCase getFetchSmsByIdUseCase() { 75 | return new FetchSmsByIdUseCase( 76 | getUiThreadPoster(), 77 | getBackgroundThreadPoster(), 78 | getFetchSmsUseCaseSync() 79 | ); 80 | } 81 | 82 | private FetchSmsUseCaseSync getFetchSmsUseCaseSync() { 83 | return new FetchSmsUseCaseSync(getContentResolver()); 84 | } 85 | 86 | public MarkSmsAsReadUseCase getMarkSmsAsReadUseCase() { 87 | return new MarkSmsAsReadUseCase( 88 | getUiThreadPoster(), 89 | getBackgroundThreadPoster(), 90 | getContentResolver() 91 | ); 92 | } 93 | 94 | public DefaultSmsAppTester getDefaultSmsAppTester() { 95 | return new DefaultSmsAppTester(mActivity); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/android_mvc_tutorial/common/dependencyinjection/ApplicationCompositionRoot.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.android_mvc_tutorial.common.dependencyinjection; 2 | 3 | import android.app.Application; 4 | 5 | import com.techyourchance.threadposter.BackgroundThreadPoster; 6 | import com.techyourchance.threadposter.UiThreadPoster; 7 | 8 | public class ApplicationCompositionRoot { 9 | 10 | private final Application mApplication; 11 | 12 | private UiThreadPoster mUiThreadPoster; 13 | private BackgroundThreadPoster mBackgroundThreadPoster; 14 | 15 | public ApplicationCompositionRoot(Application application) { 16 | mApplication = application; 17 | } 18 | 19 | public UiThreadPoster getUiThreadPoster() { 20 | if (mUiThreadPoster == null) { 21 | mUiThreadPoster = new UiThreadPoster(); 22 | } 23 | return mUiThreadPoster; 24 | } 25 | 26 | public BackgroundThreadPoster getBackgroundThreadPoster() { 27 | if (mBackgroundThreadPoster == null) { 28 | mBackgroundThreadPoster = new BackgroundThreadPoster(); 29 | } 30 | return mBackgroundThreadPoster; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/android_mvc_tutorial/common/permissions/MyPermission.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.android_mvc_tutorial.common.permissions; 2 | 3 | import android.Manifest; 4 | 5 | public enum MyPermission { 6 | 7 | READ_SMS(Manifest.permission.READ_SMS); 8 | 9 | private final String mCode; 10 | 11 | MyPermission(String code) { 12 | mCode = code; 13 | } 14 | 15 | public String getCode() { 16 | return mCode; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/android_mvc_tutorial/common/permissions/PermissionsHelper.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.android_mvc_tutorial.common.permissions; 2 | 3 | import android.content.pm.PackageManager; 4 | 5 | import com.techyourchance.android_mvc_tutorial.common.BaseObservable; 6 | 7 | import androidx.annotation.UiThread; 8 | import androidx.appcompat.app.AppCompatActivity; 9 | import androidx.core.app.ActivityCompat; 10 | import androidx.core.content.ContextCompat; 11 | 12 | @UiThread 13 | public class PermissionsHelper extends BaseObservable { 14 | 15 | public interface Listener { 16 | void onPermissionGranted(MyPermission permission); 17 | void onPermissionDenied(MyPermission permission); 18 | void onPermissionDeniedAndDontAskAgain(MyPermission permission); 19 | void onPermissionRequestCancelled(); 20 | } 21 | 22 | public static final int REQUEST_CODE = 1001; 23 | 24 | private final AppCompatActivity mActivity; 25 | 26 | public PermissionsHelper(AppCompatActivity activity) { 27 | mActivity = activity; 28 | } 29 | 30 | public boolean hasPermission(MyPermission permission) { 31 | return ContextCompat.checkSelfPermission(mActivity, permission.getCode()) == PackageManager.PERMISSION_GRANTED; 32 | } 33 | 34 | public void requestPermission(MyPermission permission) { 35 | if (hasPermission(permission)) { 36 | throw new RuntimeException("mustn't ask for already granted permission"); 37 | } 38 | if (isDeniedAndDontAskAgain(permission)) { 39 | notifyDeniedAndDontAskAgain(permission); 40 | return; 41 | } 42 | ActivityCompat.requestPermissions(mActivity, new String[] {permission.getCode()}, REQUEST_CODE); 43 | } 44 | 45 | private boolean isDeniedAndDontAskAgain(MyPermission permission) { 46 | return !ActivityCompat.shouldShowRequestPermissionRationale(mActivity, permission.getCode()); 47 | } 48 | 49 | public void onRequestPermissionsResult(String[] permissions, int[] grantResults) { 50 | if (permissions.length == 0) { 51 | notifyCancelled(); 52 | return; 53 | } 54 | 55 | MyPermission permission = permissionFromCode(permissions[0]); 56 | 57 | if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { 58 | notifyGranted(permission); 59 | } else { 60 | if (isDeniedAndDontAskAgain(permission)) { 61 | notifyDeniedAndDontAskAgain(permission); 62 | } else { 63 | notifyDenied(permission); 64 | } 65 | } 66 | } 67 | 68 | private MyPermission permissionFromCode(String permissionCode) { 69 | for (MyPermission permission : MyPermission.values()) { 70 | if (permission.getCode().equals(permissionCode)) { 71 | return permission; 72 | } 73 | } 74 | throw new RuntimeException("unsupported permission: " + permissionCode); 75 | } 76 | 77 | private void notifyGranted(MyPermission permission) { 78 | for (Listener listener : getListeners()) { 79 | listener.onPermissionGranted(permission); 80 | } 81 | } 82 | 83 | private void notifyDenied(MyPermission permission) { 84 | for (Listener listener : getListeners()) { 85 | listener.onPermissionDenied(permission); 86 | } 87 | } 88 | 89 | private void notifyDeniedAndDontAskAgain(MyPermission permission) { 90 | for (Listener listener : getListeners()) { 91 | listener.onPermissionDeniedAndDontAskAgain(permission); 92 | } 93 | } 94 | 95 | private void notifyCancelled() { 96 | for (Listener listener : getListeners()) { 97 | listener.onPermissionRequestCancelled(); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/android_mvc_tutorial/screens/ScreensNavigator.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.android_mvc_tutorial.screens; 2 | 3 | import android.os.Bundle; 4 | 5 | import com.ncapdevi.fragnav.FragNavController; 6 | import com.techyourchance.android_mvc_tutorial.screens.smsall.SmsAllFragment; 7 | import com.techyourchance.android_mvc_tutorial.screens.smsdetails.SmsDetailsFragment; 8 | 9 | import androidx.fragment.app.Fragment; 10 | 11 | public class ScreensNavigator { 12 | 13 | private final FragNavController mFragNavController; 14 | 15 | public ScreensNavigator(FragNavController fragNavController) { 16 | mFragNavController = fragNavController; 17 | } 18 | 19 | public void init(Bundle savedInstanceState) { 20 | mFragNavController.setRootFragmentListener(new FragNavController.RootFragmentListener() { 21 | @Override 22 | public int getNumberOfRootFragments() { 23 | return 1; 24 | } 25 | 26 | @Override 27 | public Fragment getRootFragment(int i) { 28 | return SmsAllFragment.newInstance(); 29 | } 30 | }); 31 | 32 | mFragNavController.initialize(FragNavController.TAB1, savedInstanceState); 33 | } 34 | 35 | public boolean navigateBack() { 36 | if (mFragNavController.isRootFragment()) { 37 | return false; 38 | } else { 39 | mFragNavController.popFragment(); 40 | return true; 41 | } 42 | } 43 | 44 | public void toSmsAllScreen() { 45 | mFragNavController.pushFragment(SmsAllFragment.newInstance()); 46 | } 47 | 48 | public void toSmsDetailsScreen(long smsId) { 49 | mFragNavController.pushFragment(SmsDetailsFragment.newInstance(smsId)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/android_mvc_tutorial/screens/common/BaseFragment.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.android_mvc_tutorial.screens.common; 2 | 3 | import com.techyourchance.android_mvc_tutorial.common.dependencyinjection.ActivityCompositionRoot; 4 | import com.techyourchance.android_mvc_tutorial.screens.main.MainActivity; 5 | 6 | import androidx.fragment.app.Fragment; 7 | 8 | public abstract class BaseFragment extends Fragment { 9 | 10 | protected ActivityCompositionRoot getCompositionRoot() { 11 | return ((MainActivity)requireActivity()).getCompositionRoot(); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/android_mvc_tutorial/screens/common/BaseViewMvc.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.android_mvc_tutorial.screens.common; 2 | 3 | import android.view.View; 4 | 5 | import com.techyourchance.android_mvc_tutorial.common.BaseObservable; 6 | 7 | import java.text.SimpleDateFormat; 8 | import java.util.Date; 9 | 10 | import androidx.annotation.ColorRes; 11 | import androidx.annotation.IdRes; 12 | 13 | public abstract class BaseViewMvc extends BaseObservable implements ViewMvc { 14 | 15 | private View mRootView; 16 | 17 | protected void setRootView(View rootView) { 18 | mRootView = rootView; 19 | } 20 | 21 | @Override 22 | public View getRootView() { 23 | return mRootView; 24 | } 25 | 26 | protected T findViewById(@IdRes int id) { 27 | return getRootView().findViewById(id); 28 | } 29 | 30 | protected int getColor(@ColorRes int colorId) { 31 | return mRootView.getContext().getResources().getColor(colorId); 32 | } 33 | 34 | public static String fromUnixTimestampToHumanReadableFormat(String timestamp) { 35 | SimpleDateFormat fmtOut = new SimpleDateFormat("yyyy-MM-dd HH:mm"); 36 | return fmtOut.format(new Date(Long.valueOf(timestamp))); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/android_mvc_tutorial/screens/common/ViewMvc.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.android_mvc_tutorial.screens.common; 2 | 3 | import android.view.View; 4 | 5 | 6 | /** 7 | * MVC view is a "dumb" component used for presenting information to the user and capturing user's input. 8 | */ 9 | public interface ViewMvc { 10 | 11 | /** 12 | * @return the root Android View which is used internally by this MVC View. 13 | */ 14 | View getRootView(); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/android_mvc_tutorial/screens/common/ViewMvcFactory.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.android_mvc_tutorial.screens.common; 2 | 3 | import android.view.LayoutInflater; 4 | import android.view.ViewGroup; 5 | 6 | import com.techyourchance.android_mvc_tutorial.screens.smsall.SmsAllViewMvc; 7 | import com.techyourchance.android_mvc_tutorial.screens.smsall.SmsAllViewMvcImpl; 8 | import com.techyourchance.android_mvc_tutorial.screens.smsall.smslistitem.SmsListItemViewMvc; 9 | import com.techyourchance.android_mvc_tutorial.screens.smsall.smslistitem.SmsListItemViewMvcImpl; 10 | import com.techyourchance.android_mvc_tutorial.screens.smsdetails.SmsDetailsViewMvc; 11 | import com.techyourchance.android_mvc_tutorial.screens.smsdetails.SmsDetailsViewMvcImpl; 12 | 13 | import androidx.annotation.Nullable; 14 | 15 | public class ViewMvcFactory { 16 | 17 | private final LayoutInflater mLayoutInflater; 18 | 19 | public ViewMvcFactory(LayoutInflater layoutInflater) { 20 | mLayoutInflater = layoutInflater; 21 | } 22 | 23 | public SmsAllViewMvc newSmsAllViewMvc(@Nullable ViewGroup parent) { 24 | return new SmsAllViewMvcImpl(mLayoutInflater, parent, this); 25 | } 26 | 27 | public SmsListItemViewMvc newSmsListItemViewMvc(@Nullable ViewGroup parent) { 28 | return new SmsListItemViewMvcImpl(mLayoutInflater, parent); 29 | } 30 | 31 | public SmsDetailsViewMvc newSmsDetailsViewMvc(@Nullable ViewGroup parent) { 32 | return new SmsDetailsViewMvcImpl(mLayoutInflater, parent); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/android_mvc_tutorial/screens/common/toolbar/MyToolbar.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.android_mvc_tutorial.screens.common.toolbar; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.widget.FrameLayout; 8 | 9 | import com.techyourchance.android_mvc_tutorial.R; 10 | 11 | import androidx.annotation.Nullable; 12 | import androidx.appcompat.widget.Toolbar; 13 | 14 | public class MyToolbar extends Toolbar { 15 | 16 | public interface NavigateUpListener { 17 | void onNavigationUpClicked(); 18 | } 19 | 20 | private NavigateUpListener mNavigateUpListener; 21 | 22 | private FrameLayout mNavigateUp; 23 | 24 | public MyToolbar(Context context) { 25 | super(context); 26 | init(context); 27 | } 28 | 29 | public MyToolbar(Context context, @Nullable AttributeSet attrs) { 30 | super(context, attrs); 31 | init(context); 32 | } 33 | 34 | public MyToolbar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 35 | super(context, attrs, defStyleAttr); 36 | init(context); 37 | } 38 | 39 | private void init(Context context) { 40 | View view = LayoutInflater.from(context).inflate(R.layout.layout_my_toolbar, this, true); 41 | setContentInsetsRelative(0, 0); 42 | 43 | mNavigateUp = view.findViewById(R.id.navigate_up); 44 | 45 | setListeners(); 46 | } 47 | 48 | private void setListeners() { 49 | mNavigateUp.setOnClickListener(view -> mNavigateUpListener.onNavigationUpClicked()); 50 | } 51 | 52 | public void setNavigateUpListener(NavigateUpListener navigateUpListener) { 53 | mNavigateUpListener = navigateUpListener; 54 | mNavigateUp.setVisibility(VISIBLE); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/android_mvc_tutorial/screens/main/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.android_mvc_tutorial.screens.main; 2 | 3 | import android.os.Bundle; 4 | 5 | import com.techyourchance.android_mvc_tutorial.MvcTutorialApplication; 6 | import com.techyourchance.android_mvc_tutorial.R; 7 | import com.techyourchance.android_mvc_tutorial.common.dependencyinjection.ActivityCompositionRoot; 8 | import com.techyourchance.android_mvc_tutorial.common.permissions.PermissionsHelper; 9 | import com.techyourchance.android_mvc_tutorial.screens.ScreensNavigator; 10 | 11 | import androidx.annotation.NonNull; 12 | import androidx.appcompat.app.AppCompatActivity; 13 | 14 | 15 | public class MainActivity extends AppCompatActivity { 16 | 17 | private ActivityCompositionRoot mActivityCompositionRoot; 18 | 19 | private PermissionsHelper mPermissionsHelper; 20 | private ScreensNavigator mScreensNavigator; 21 | 22 | @Override 23 | protected void onCreate(Bundle savedInstanceState) { 24 | super.onCreate(savedInstanceState); 25 | 26 | mPermissionsHelper = getCompositionRoot().getPermissionsHelper(); 27 | mScreensNavigator = getCompositionRoot().getScreenNavigator(); 28 | 29 | setContentView(R.layout.layout_single_frame); 30 | 31 | mScreensNavigator.init(savedInstanceState); 32 | } 33 | 34 | public ActivityCompositionRoot getCompositionRoot() { 35 | if (mActivityCompositionRoot == null) { 36 | mActivityCompositionRoot = new ActivityCompositionRoot( 37 | ((MvcTutorialApplication)getApplication()).getApplicationCompositionRoot(), 38 | this 39 | ); 40 | } 41 | return mActivityCompositionRoot; 42 | } 43 | 44 | @Override 45 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { 46 | if (requestCode == PermissionsHelper.REQUEST_CODE) { 47 | mPermissionsHelper.onRequestPermissionsResult(permissions, grantResults); 48 | } 49 | } 50 | 51 | @Override 52 | public void onBackPressed() { 53 | if (!mScreensNavigator.navigateBack()) { 54 | super.onBackPressed(); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/android_mvc_tutorial/screens/smsall/SmsAllFragment.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.android_mvc_tutorial.screens.smsall; 2 | 3 | import android.os.Bundle; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | 8 | import com.techyourchance.android_mvc_tutorial.common.permissions.MyPermission; 9 | import com.techyourchance.android_mvc_tutorial.common.permissions.PermissionsHelper; 10 | import com.techyourchance.android_mvc_tutorial.screens.ScreensNavigator; 11 | import com.techyourchance.android_mvc_tutorial.screens.common.BaseFragment; 12 | import com.techyourchance.android_mvc_tutorial.screens.common.ViewMvcFactory; 13 | import com.techyourchance.android_mvc_tutorial.sms.FetchAllSmsUseCase; 14 | import com.techyourchance.android_mvc_tutorial.sms.SmsMessage; 15 | 16 | import java.util.List; 17 | 18 | import androidx.annotation.NonNull; 19 | import androidx.annotation.Nullable; 20 | import androidx.fragment.app.Fragment; 21 | 22 | public class SmsAllFragment extends BaseFragment implements 23 | SmsAllViewMvc.Listener, 24 | FetchAllSmsUseCase.Listener, 25 | PermissionsHelper.Listener { 26 | 27 | public static Fragment newInstance() { 28 | return new SmsAllFragment(); 29 | } 30 | 31 | private PermissionsHelper mPermissionsHelper; 32 | private ViewMvcFactory mViewMvcFactory; 33 | private ScreensNavigator mScreensNavigator; 34 | private FetchAllSmsUseCase mFetchAllSmsUseCase; 35 | 36 | private SmsAllViewMvc mViewMVC; 37 | 38 | @Override 39 | public void onCreate(@Nullable Bundle savedInstanceState) { 40 | super.onCreate(savedInstanceState); 41 | mPermissionsHelper = getCompositionRoot().getPermissionsHelper(); 42 | mViewMvcFactory = getCompositionRoot().getViewMvcFactory(); 43 | mFetchAllSmsUseCase = getCompositionRoot().getFetchAllSmsUseCase(); 44 | mScreensNavigator = getCompositionRoot().getScreenNavigator(); 45 | } 46 | 47 | @Override 48 | public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 49 | mViewMVC = mViewMvcFactory.newSmsAllViewMvc(container); 50 | return mViewMVC.getRootView(); 51 | } 52 | 53 | 54 | @Override 55 | public void onStart() { 56 | super.onStart(); 57 | mViewMVC.registerListener(this); 58 | mFetchAllSmsUseCase.registerListener(this); 59 | mPermissionsHelper.registerListener(this); 60 | 61 | checkReadSmsPermissionAndFetchAllSms(); 62 | } 63 | 64 | @Override 65 | public void onStop() { 66 | super.onStop(); 67 | mViewMVC.unregisterListener(this); 68 | mFetchAllSmsUseCase.unregisterListener(this); 69 | mPermissionsHelper.unregisterListener(this); 70 | } 71 | 72 | private void checkReadSmsPermissionAndFetchAllSms() { 73 | if (mPermissionsHelper.hasPermission(MyPermission.READ_SMS)) { 74 | mFetchAllSmsUseCase.fetchAllSmsMessages(); 75 | } else { 76 | mPermissionsHelper.requestPermission(MyPermission.READ_SMS); 77 | } 78 | } 79 | 80 | @Override 81 | public void onPermissionGranted(MyPermission permission) { 82 | if (permission == MyPermission.READ_SMS) { 83 | mFetchAllSmsUseCase.fetchAllSmsMessages(); 84 | } 85 | } 86 | 87 | @Override 88 | public void onPermissionDenied(MyPermission permission) { 89 | mViewMVC.showPermissionDenied(); 90 | } 91 | 92 | @Override 93 | public void onPermissionDeniedAndDontAskAgain(MyPermission permission) { 94 | mViewMVC.showPermissionDeniedAndDontAskAgain(); 95 | } 96 | 97 | @Override 98 | public void onPermissionRequestCancelled() { 99 | checkReadSmsPermissionAndFetchAllSms(); 100 | } 101 | 102 | @Override 103 | public void onSmsMessageClicked(long id) { 104 | mScreensNavigator.toSmsDetailsScreen(id); 105 | } 106 | 107 | @Override 108 | public void onAskForPermissionClicked() { 109 | checkReadSmsPermissionAndFetchAllSms(); 110 | } 111 | 112 | @Override 113 | public void onAllSmsFetched(List smsMessages) { 114 | mViewMVC.bindSmsMessages(smsMessages); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/android_mvc_tutorial/screens/smsall/SmsAllListAdapter.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.android_mvc_tutorial.screens.smsall; 2 | 3 | import android.content.Context; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | import android.widget.ArrayAdapter; 7 | 8 | import com.techyourchance.android_mvc_tutorial.screens.common.ViewMvcFactory; 9 | import com.techyourchance.android_mvc_tutorial.screens.smsall.smslistitem.SmsListItemViewMvc; 10 | import com.techyourchance.android_mvc_tutorial.sms.SmsMessage; 11 | 12 | public class SmsAllListAdapter extends ArrayAdapter { 13 | 14 | private final ViewMvcFactory mViewMvcFactory; 15 | 16 | public SmsAllListAdapter(Context context, ViewMvcFactory viewMvcFactory) { 17 | super(context, 0); 18 | mViewMvcFactory = viewMvcFactory; 19 | } 20 | 21 | @Override 22 | public View getView(int position, View convertView, ViewGroup parent) { 23 | SmsListItemViewMvc smsListItemViewMvc; 24 | if (convertView == null) { 25 | smsListItemViewMvc = mViewMvcFactory.newSmsListItemViewMvc(parent); 26 | /* 27 | Since this kind of adapters store just references to Android Views, we need to "attach" 28 | the entire MVC view as a tag to its root view in order to be able to retrieve it later. 29 | Usage of MVC view in such a way completely eliminates a need for ViewHolder. 30 | */ 31 | smsListItemViewMvc.getRootView().setTag(smsListItemViewMvc); 32 | } else { 33 | smsListItemViewMvc = ((SmsListItemViewMvc) convertView.getTag()); 34 | } 35 | 36 | smsListItemViewMvc.bindSmsMessage(getItem(position)); 37 | return smsListItemViewMvc.getRootView(); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/android_mvc_tutorial/screens/smsall/SmsAllViewMvc.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.android_mvc_tutorial.screens.smsall; 2 | 3 | import com.techyourchance.android_mvc_tutorial.sms.SmsMessage; 4 | import com.techyourchance.android_mvc_tutorial.screens.common.ViewMvc; 5 | 6 | import java.util.List; 7 | 8 | public interface SmsAllViewMvc extends ViewMvc { 9 | 10 | 11 | interface Listener { 12 | void onSmsMessageClicked(long id); 13 | 14 | void onAskForPermissionClicked(); 15 | } 16 | 17 | void showPermissionDenied(); 18 | void showPermissionDeniedAndDontAskAgain(); 19 | 20 | void bindSmsMessages(List smsMessages); 21 | 22 | void registerListener(Listener listener); 23 | void unregisterListener(Listener listener); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/android_mvc_tutorial/screens/smsall/SmsAllViewMvcImpl.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.android_mvc_tutorial.screens.smsall; 2 | 3 | import android.view.LayoutInflater; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | import android.widget.AdapterView; 7 | import android.widget.Button; 8 | import android.widget.ListView; 9 | 10 | import com.techyourchance.android_mvc_tutorial.R; 11 | import com.techyourchance.android_mvc_tutorial.screens.common.BaseViewMvc; 12 | import com.techyourchance.android_mvc_tutorial.screens.common.ViewMvcFactory; 13 | import com.techyourchance.android_mvc_tutorial.sms.SmsMessage; 14 | 15 | import java.util.List; 16 | 17 | public class SmsAllViewMvcImpl extends BaseViewMvc implements SmsAllViewMvc { 18 | 19 | private ListView mListView; 20 | private SmsAllListAdapter mSmsAllListAdapter; 21 | private View mViewPermissionDenied; 22 | private Button mBtnAskForPermission; 23 | private View mViewPermissionDeniedDontAskAgain; 24 | 25 | public SmsAllViewMvcImpl(LayoutInflater inflater, ViewGroup container, ViewMvcFactory viewMvcFactory) { 26 | setRootView(inflater.inflate(R.layout.layout_sms_all, container, false)); 27 | 28 | mSmsAllListAdapter = new SmsAllListAdapter(inflater.getContext(), viewMvcFactory); 29 | mListView = findViewById(R.id.list_sms_messages); 30 | mListView.setAdapter(mSmsAllListAdapter); 31 | 32 | mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { 33 | @Override 34 | public void onItemClick(AdapterView adapterView, View view, int position, long id) { 35 | for (Listener listener : getListeners()) { 36 | listener.onSmsMessageClicked(mSmsAllListAdapter.getItem(position).getId()); 37 | } 38 | } 39 | }); 40 | 41 | mViewPermissionDenied = findViewById(R.id.view_permission_denied); 42 | mBtnAskForPermission = findViewById(R.id.btn_request_permission); 43 | 44 | mBtnAskForPermission.setOnClickListener(new View.OnClickListener() { 45 | @Override 46 | public void onClick(View v) { 47 | for (Listener listener : getListeners()) { 48 | listener.onAskForPermissionClicked(); 49 | } 50 | } 51 | }); 52 | 53 | mViewPermissionDeniedDontAskAgain = findViewById(R.id.view_permission_denied_dont_ask_again); 54 | } 55 | 56 | @Override 57 | public void showPermissionDenied() { 58 | hideAllViews(); 59 | mViewPermissionDenied.setVisibility(View.VISIBLE); 60 | } 61 | 62 | @Override 63 | public void showPermissionDeniedAndDontAskAgain() { 64 | hideAllViews(); 65 | mViewPermissionDeniedDontAskAgain.setVisibility(View.VISIBLE); 66 | } 67 | 68 | @Override 69 | public void bindSmsMessages(List smsMessages) { 70 | hideAllViews(); 71 | mListView.setVisibility(View.VISIBLE); 72 | 73 | mSmsAllListAdapter.clear(); 74 | mSmsAllListAdapter.addAll(smsMessages); 75 | mSmsAllListAdapter.notifyDataSetChanged(); 76 | } 77 | 78 | private void hideAllViews() { 79 | mListView.setVisibility(View.GONE); 80 | mViewPermissionDenied.setVisibility(View.GONE); 81 | mViewPermissionDeniedDontAskAgain.setVisibility(View.GONE); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/android_mvc_tutorial/screens/smsall/smslistitem/SmsListItemViewMvc.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.android_mvc_tutorial.screens.smsall.smslistitem; 2 | 3 | import com.techyourchance.android_mvc_tutorial.sms.SmsMessage; 4 | import com.techyourchance.android_mvc_tutorial.screens.common.ViewMvc; 5 | 6 | public interface SmsListItemViewMvc extends ViewMvc { 7 | 8 | interface Listener { 9 | // currently no interactions 10 | } 11 | 12 | void bindSmsMessage(SmsMessage smsMessage); 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/android_mvc_tutorial/screens/smsall/smslistitem/SmsListItemViewMvcImpl.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.android_mvc_tutorial.screens.smsall.smslistitem; 2 | 3 | import android.view.LayoutInflater; 4 | import android.view.ViewGroup; 5 | import android.widget.TextView; 6 | 7 | import com.techyourchance.android_mvc_tutorial.R; 8 | import com.techyourchance.android_mvc_tutorial.screens.common.BaseViewMvc; 9 | import com.techyourchance.android_mvc_tutorial.sms.SmsMessage; 10 | 11 | public class SmsListItemViewMvcImpl extends BaseViewMvc 12 | implements SmsListItemViewMvc { 13 | 14 | private final TextView mTxtAddress; 15 | private final TextView mTxtDate; 16 | 17 | public SmsListItemViewMvcImpl(LayoutInflater inflater, ViewGroup container) { 18 | setRootView(inflater.inflate(R.layout.layout_sms_list_item, container, false)); 19 | 20 | mTxtAddress = findViewById(R.id.txt_sms_address); 21 | mTxtDate = findViewById(R.id.txt_sms_date); 22 | } 23 | 24 | @Override 25 | public void bindSmsMessage(SmsMessage smsMessage) { 26 | mTxtAddress.setText(smsMessage.getAddress()); 27 | mTxtDate.setText(fromUnixTimestampToHumanReadableFormat(smsMessage.getDateString())); 28 | 29 | // Change the background depending on whether the message has already been read 30 | if (smsMessage.isUnread()) { 31 | getRootView().setBackgroundColor(getColor(android.R.color.holo_green_light)); 32 | } else { 33 | getRootView().setBackgroundColor(getColor(android.R.color.white)); 34 | } 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/android_mvc_tutorial/screens/smsdetails/SmsDetailsFragment.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.android_mvc_tutorial.screens.smsdetails; 2 | 3 | import android.os.Build; 4 | import android.os.Bundle; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.Toast; 9 | 10 | import com.techyourchance.android_mvc_tutorial.screens.ScreensNavigator; 11 | import com.techyourchance.android_mvc_tutorial.screens.common.ViewMvcFactory; 12 | import com.techyourchance.android_mvc_tutorial.sms.DefaultSmsAppTester; 13 | import com.techyourchance.android_mvc_tutorial.sms.FetchSmsByIdUseCase; 14 | import com.techyourchance.android_mvc_tutorial.sms.MarkSmsAsReadUseCase; 15 | import com.techyourchance.android_mvc_tutorial.sms.SmsMessage; 16 | import com.techyourchance.android_mvc_tutorial.screens.common.BaseFragment; 17 | 18 | import androidx.annotation.Nullable; 19 | import androidx.fragment.app.Fragment; 20 | 21 | public class SmsDetailsFragment extends BaseFragment implements 22 | SmsDetailsViewMvc.Listener, 23 | MarkSmsAsReadUseCase.Listener, 24 | FetchSmsByIdUseCase.Listener { 25 | 26 | private static final String ARG_SMS_MESSAGE_ID = "arg_sms_message_id"; 27 | 28 | public static Fragment newInstance(long smsId) { 29 | Bundle args = new Bundle(1); 30 | args.putLong(SmsDetailsFragment.ARG_SMS_MESSAGE_ID, smsId); 31 | Fragment fragment = new SmsDetailsFragment(); 32 | fragment.setArguments(args); 33 | return fragment; 34 | } 35 | 36 | private ViewMvcFactory mViewMvcFactory; 37 | private ScreensNavigator mScreensNavigator; 38 | private FetchSmsByIdUseCase mFetchSmsByIdUseCase; 39 | private MarkSmsAsReadUseCase mMarkMessageAsReadUseCase; 40 | private DefaultSmsAppTester mDefaultSmsAppTester; 41 | 42 | private SmsDetailsViewMvc mViewMVC; 43 | 44 | private long mSmsId; 45 | 46 | @Override 47 | public void onCreate(@Nullable Bundle savedInstanceState) { 48 | super.onCreate(savedInstanceState); 49 | mFetchSmsByIdUseCase = getCompositionRoot().getFetchSmsByIdUseCase(); 50 | mViewMvcFactory = getCompositionRoot().getViewMvcFactory(); 51 | mScreensNavigator = getCompositionRoot().getScreenNavigator(); 52 | mMarkMessageAsReadUseCase = getCompositionRoot().getMarkSmsAsReadUseCase(); 53 | mDefaultSmsAppTester = getCompositionRoot().getDefaultSmsAppTester(); 54 | 55 | mSmsId = getArguments().getLong(ARG_SMS_MESSAGE_ID); 56 | } 57 | 58 | @Override 59 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 60 | 61 | mViewMVC = mViewMvcFactory.newSmsDetailsViewMvc(container); 62 | 63 | /* 64 | Starting with API 19 (KitKat), only applications designated as default SMS applications 65 | can alter SMS attributes (though they still can read SMSs). Therefore, on post KitKat 66 | versions "mark as read" button is only relevant if this app is the default SMS app. 67 | */ 68 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 69 | if (!mDefaultSmsAppTester.isDefaultSmsApp()) { 70 | mViewMVC.hideMarkAsReadButton(); 71 | } 72 | } 73 | 74 | return mViewMVC.getRootView(); 75 | } 76 | 77 | @Override 78 | public void onStart() { 79 | super.onStart(); 80 | mViewMVC.registerListener(this); 81 | mFetchSmsByIdUseCase.registerListener(this); 82 | mMarkMessageAsReadUseCase.registerListener(this); 83 | 84 | mFetchSmsByIdUseCase.fetchSmsById(mSmsId); 85 | } 86 | 87 | @Override 88 | public void onStop() { 89 | super.onStop(); 90 | mViewMVC.unregisterListener(this); 91 | mFetchSmsByIdUseCase.unregisterListener(this); 92 | mMarkMessageAsReadUseCase.unregisterListener(this); 93 | } 94 | 95 | 96 | @Override 97 | public void onSmsFetched(SmsMessage sms) { 98 | mViewMVC.bindSmsMessage(sms); 99 | } 100 | 101 | @Override 102 | public void onSmsFetchFailed(long smsId) { 103 | Toast.makeText(getActivity(), "Couldn't fetch the SMS message of interest!", Toast.LENGTH_LONG).show(); 104 | } 105 | 106 | @Override 107 | public void onNavigateUpClicked() { 108 | mScreensNavigator.navigateBack(); 109 | } 110 | 111 | @Override 112 | public void onMarkAsReadClicked() { 113 | mMarkMessageAsReadUseCase.markSmsAsRead(mSmsId); 114 | } 115 | 116 | @Override 117 | public void onSmsMarkedAsRead(long smsId) { 118 | if (smsId == mSmsId) { 119 | mFetchSmsByIdUseCase.fetchSmsById(mSmsId); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/android_mvc_tutorial/screens/smsdetails/SmsDetailsViewMvc.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.android_mvc_tutorial.screens.smsdetails; 2 | 3 | import com.techyourchance.android_mvc_tutorial.screens.common.ViewMvc; 4 | import com.techyourchance.android_mvc_tutorial.sms.SmsMessage; 5 | 6 | public interface SmsDetailsViewMvc extends ViewMvc { 7 | 8 | interface Listener { 9 | void onMarkAsReadClicked(); 10 | void onNavigateUpClicked(); 11 | } 12 | 13 | void hideMarkAsReadButton(); 14 | 15 | void bindSmsMessage(SmsMessage smsMessage); 16 | 17 | void registerListener(Listener listener); 18 | void unregisterListener(Listener listener); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/android_mvc_tutorial/screens/smsdetails/SmsDetailsViewMvcImpl.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.android_mvc_tutorial.screens.smsdetails; 2 | 3 | import android.view.LayoutInflater; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | import android.widget.Button; 7 | import android.widget.TextView; 8 | 9 | import com.techyourchance.android_mvc_tutorial.R; 10 | import com.techyourchance.android_mvc_tutorial.screens.common.BaseViewMvc; 11 | import com.techyourchance.android_mvc_tutorial.screens.common.toolbar.MyToolbar; 12 | import com.techyourchance.android_mvc_tutorial.sms.SmsMessage; 13 | 14 | public class SmsDetailsViewMvcImpl extends BaseViewMvc 15 | implements SmsDetailsViewMvc { 16 | 17 | private MyToolbar mToolbar; 18 | private TextView mTxtAddress; 19 | private TextView mTxtDate; 20 | private TextView mTxtBody; 21 | private Button mBtnMarkAsRead; 22 | 23 | private boolean mMarkAsReadSupported = true; 24 | 25 | public SmsDetailsViewMvcImpl(LayoutInflater inflater, ViewGroup container) { 26 | setRootView(inflater.inflate(R.layout.layout_sms_details, container, false)); 27 | 28 | mToolbar = findViewById(R.id.toolbar); 29 | mTxtAddress = findViewById(R.id.txt_sms_address); 30 | mTxtDate = findViewById(R.id.txt_sms_date); 31 | mTxtBody = findViewById(R.id.txt_sms_body); 32 | mBtnMarkAsRead = findViewById(R.id.btn_mark_as_read); 33 | 34 | mToolbar.setNavigateUpListener(() -> { 35 | for (Listener listener : getListeners()) { 36 | listener.onNavigateUpClicked(); 37 | } 38 | }); 39 | 40 | mBtnMarkAsRead.setOnClickListener(view -> { 41 | for (Listener listener : getListeners()) { 42 | listener.onMarkAsReadClicked(); 43 | } 44 | }); 45 | } 46 | 47 | @Override 48 | public void hideMarkAsReadButton() { 49 | mMarkAsReadSupported = false; 50 | } 51 | 52 | 53 | @Override 54 | public void bindSmsMessage(SmsMessage smsMessage) { 55 | mTxtAddress.setText(smsMessage.getAddress()); 56 | mTxtDate.setText(smsMessage.getDateString()); 57 | mTxtBody.setText(smsMessage.getBody()); 58 | 59 | int backgroundColor = 60 | smsMessage.isUnread() ? android.R.color.holo_green_light : android.R.color.white; 61 | 62 | getRootView().setBackgroundColor(getColor(backgroundColor)); 63 | 64 | mBtnMarkAsRead.setVisibility( 65 | smsMessage.isUnread() && mMarkAsReadSupported ? View.VISIBLE : View.GONE 66 | ); 67 | 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/android_mvc_tutorial/sms/DefaultSmsAppTester.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.android_mvc_tutorial.sms; 2 | 3 | import android.provider.Telephony; 4 | 5 | import androidx.appcompat.app.AppCompatActivity; 6 | 7 | public class DefaultSmsAppTester { 8 | 9 | private final AppCompatActivity mActivity; 10 | 11 | public DefaultSmsAppTester(AppCompatActivity activity) { 12 | mActivity = activity; 13 | } 14 | 15 | public boolean isDefaultSmsApp() { 16 | String defaultSmsPackage = Telephony.Sms.getDefaultSmsPackage(mActivity); 17 | return mActivity.getPackageName().equals(defaultSmsPackage); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/android_mvc_tutorial/sms/FetchAllSmsUseCase.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.android_mvc_tutorial.sms; 2 | 3 | import com.techyourchance.android_mvc_tutorial.common.BaseObservable; 4 | import com.techyourchance.threadposter.BackgroundThreadPoster; 5 | import com.techyourchance.threadposter.UiThreadPoster; 6 | 7 | import java.util.ArrayList; 8 | import java.util.Collections; 9 | import java.util.List; 10 | 11 | public class FetchAllSmsUseCase extends BaseObservable { 12 | 13 | public interface Listener { 14 | void onAllSmsFetched(List smsMessages); 15 | } 16 | 17 | private final UiThreadPoster mUiThreadPoster; 18 | private final BackgroundThreadPoster mBackgroundThreadPoster; 19 | private final FetchSmsUseCaseSync mFetchSmsUseCaseSync; 20 | 21 | public FetchAllSmsUseCase(UiThreadPoster uiThreadPoster, BackgroundThreadPoster backgroundThreadPoster, FetchSmsUseCaseSync fetchSmsUseCaseSync) { 22 | mUiThreadPoster = uiThreadPoster; 23 | mBackgroundThreadPoster = backgroundThreadPoster; 24 | mFetchSmsUseCaseSync = fetchSmsUseCaseSync; 25 | } 26 | 27 | public void fetchAllSmsMessages() { 28 | mBackgroundThreadPoster.post(() -> { 29 | notifySuccess(mFetchSmsUseCaseSync.fetchSms(Collections.emptyList())); 30 | }); 31 | } 32 | 33 | private void notifySuccess(final List smsMessages) { 34 | mUiThreadPoster.post(() -> { 35 | for (Listener listener : getListeners()) { 36 | listener.onAllSmsFetched(smsMessages); 37 | } 38 | }); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/android_mvc_tutorial/sms/FetchSmsByIdUseCase.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.android_mvc_tutorial.sms; 2 | 3 | import com.techyourchance.android_mvc_tutorial.common.BaseObservable; 4 | import com.techyourchance.threadposter.BackgroundThreadPoster; 5 | import com.techyourchance.threadposter.UiThreadPoster; 6 | 7 | import java.util.Collections; 8 | import java.util.List; 9 | 10 | public class FetchSmsByIdUseCase extends BaseObservable { 11 | 12 | public interface Listener { 13 | void onSmsFetched(SmsMessage smsMessage); 14 | void onSmsFetchFailed(long smsId); 15 | } 16 | 17 | private final UiThreadPoster mUiThreadPoster; 18 | private final BackgroundThreadPoster mBackgroundThreadPoster; 19 | private final FetchSmsUseCaseSync mFetchSmsUseCaseSync; 20 | 21 | public FetchSmsByIdUseCase(UiThreadPoster uiThreadPoster, BackgroundThreadPoster backgroundThreadPoster, FetchSmsUseCaseSync fetchSmsUseCaseSync) { 22 | mUiThreadPoster = uiThreadPoster; 23 | mBackgroundThreadPoster = backgroundThreadPoster; 24 | mFetchSmsUseCaseSync = fetchSmsUseCaseSync; 25 | } 26 | 27 | public void fetchSmsById(long id) { 28 | mBackgroundThreadPoster.post(() -> { 29 | List smsMessages = mFetchSmsUseCaseSync.fetchSms(Collections.singletonList(id)); 30 | if (!smsMessages.isEmpty()) { 31 | notifySuccess(smsMessages.get(0)); 32 | } else { 33 | notifyFailure(id); 34 | } 35 | }); 36 | } 37 | 38 | private void notifySuccess(final SmsMessage smsMessage) { 39 | mUiThreadPoster.post(() -> { 40 | for (Listener listener : getListeners()) { 41 | listener.onSmsFetched(smsMessage); 42 | } 43 | }); 44 | } 45 | 46 | private void notifyFailure(long id) { 47 | mUiThreadPoster.post(() -> { 48 | for (Listener listener : getListeners()) { 49 | listener.onSmsFetchFailed(id); 50 | } 51 | }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/android_mvc_tutorial/sms/FetchSmsUseCaseSync.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.android_mvc_tutorial.sms; 2 | 3 | import android.content.ContentResolver; 4 | import android.database.Cursor; 5 | import android.net.Uri; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | import androidx.annotation.Nullable; 11 | import androidx.annotation.WorkerThread; 12 | 13 | public class FetchSmsUseCaseSync { 14 | 15 | private static final String CONTENT_URI = "content://sms/inbox"; 16 | 17 | private static final String[] COLUMNS_OF_INTEREST = new String[] { 18 | "_id", 19 | "address", 20 | "date", 21 | "body", 22 | "read" 23 | }; 24 | 25 | private static final String DEFAULT_SORT_ORDER = "date DESC"; 26 | 27 | 28 | private final ContentResolver mContentResolver; 29 | 30 | public FetchSmsUseCaseSync(ContentResolver contentResolver) { 31 | mContentResolver = contentResolver; 32 | } 33 | 34 | /** 35 | * @param smsIds IDs of SMS messages to fetch; if empty - all messages will be fetched 36 | */ 37 | @WorkerThread 38 | public List fetchSms(List smsIds) { 39 | Cursor cursor = null; 40 | try { 41 | cursor = mContentResolver.query( 42 | Uri.parse(CONTENT_URI), 43 | COLUMNS_OF_INTEREST, 44 | selectionStringForIds(smsIds), 45 | selectionArgsForIds(smsIds), 46 | DEFAULT_SORT_ORDER 47 | ); 48 | 49 | return extractSmsFromCursorAndClose(cursor); 50 | } finally { 51 | if (cursor != null) cursor.close(); 52 | } 53 | } 54 | 55 | private @Nullable String selectionStringForIds(List ids) { 56 | if (ids.isEmpty()) { 57 | return null; 58 | } 59 | 60 | String selection = "_id in ("; 61 | for (int i = 0; i < ids.size(); i++) { 62 | selection += "?, "; 63 | } 64 | selection = selection.substring(0, selection.length() - 2) + ")"; 65 | return selection; 66 | } 67 | 68 | private @Nullable String[] selectionArgsForIds(List smsIds) { 69 | if (smsIds.isEmpty()) { 70 | return null; 71 | } 72 | 73 | String[] stingIds = new String[smsIds.size()]; 74 | for (int i = 0; i < smsIds.size(); i++) { 75 | stingIds[i] = String.valueOf(smsIds.get(i)); 76 | } 77 | return stingIds; 78 | } 79 | 80 | public List extractSmsFromCursorAndClose(Cursor cursor) { 81 | if (cursor != null && cursor.moveToFirst()) { 82 | List result = new ArrayList<>(cursor.getCount()); 83 | do { 84 | result.add(new SmsMessage( 85 | cursor.getLong(cursor.getColumnIndexOrThrow("_id")), 86 | cursor.getString(cursor.getColumnIndexOrThrow("address")), 87 | cursor.getString(cursor.getColumnIndexOrThrow("body")), 88 | cursor.getString(cursor.getColumnIndexOrThrow("date")), 89 | cursor.getInt(cursor.getColumnIndexOrThrow("read")) == 0)); 90 | } while (cursor.moveToNext()); 91 | return result; 92 | } else { 93 | return new ArrayList<>(0); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/android_mvc_tutorial/sms/MarkSmsAsReadUseCase.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.android_mvc_tutorial.sms; 2 | 3 | import android.content.ContentResolver; 4 | import android.content.ContentUris; 5 | import android.content.ContentValues; 6 | import android.net.Uri; 7 | 8 | import com.techyourchance.android_mvc_tutorial.common.BaseObservable; 9 | import com.techyourchance.threadposter.BackgroundThreadPoster; 10 | import com.techyourchance.threadposter.UiThreadPoster; 11 | 12 | import java.util.List; 13 | 14 | public class MarkSmsAsReadUseCase extends BaseObservable { 15 | 16 | private static final String CONTENT_URI = "content://sms/inbox"; 17 | 18 | public interface Listener { 19 | void onSmsMarkedAsRead(long smsId); 20 | } 21 | 22 | private final UiThreadPoster mUiThreadPoster; 23 | private final BackgroundThreadPoster mBackgroundThreadPoster; 24 | private final ContentResolver mContentResolver; 25 | 26 | public MarkSmsAsReadUseCase(UiThreadPoster uiThreadPoster, BackgroundThreadPoster backgroundThreadPoster, ContentResolver contentResolver) { 27 | mUiThreadPoster = uiThreadPoster; 28 | mBackgroundThreadPoster = backgroundThreadPoster; 29 | mContentResolver = contentResolver; 30 | } 31 | 32 | public void markSmsAsRead(long smsId) { 33 | mBackgroundThreadPoster.post(() -> { 34 | // Designating the fields that should be updated 35 | ContentValues values = new ContentValues(); 36 | values.put("read", true); 37 | 38 | mContentResolver.update( 39 | ContentUris.withAppendedId(Uri.parse(CONTENT_URI), smsId), 40 | values, 41 | null, 42 | null); 43 | 44 | notifySuccess(smsId); 45 | }); 46 | } 47 | 48 | private void notifySuccess(long smsId) { 49 | mUiThreadPoster.post(() -> { 50 | for (Listener listener : getListeners()) { 51 | listener.onSmsMarkedAsRead(smsId); 52 | } 53 | }); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/android_mvc_tutorial/sms/SmsMessage.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.android_mvc_tutorial.sms; 2 | 3 | /** 4 | * Instances of this class are POJOs (Plain Old Java Object) which represent SMS messages. 5 | */ 6 | public class SmsMessage { 7 | 8 | private long mId; 9 | private String mAddress; 10 | private String mBody; 11 | private String mDate; 12 | private boolean mUnread; 13 | 14 | public SmsMessage(long id, String address, String body, String date, boolean unread) { 15 | mId = id; 16 | mAddress = address; 17 | mBody = body; 18 | mDate = date; 19 | mUnread = unread; 20 | } 21 | 22 | public long getId() { 23 | return mId; 24 | } 25 | 26 | public String getAddress() { 27 | return mAddress; 28 | } 29 | 30 | public String getBody() { 31 | return mBody; 32 | } 33 | 34 | public String getDateString() { 35 | return mDate; 36 | } 37 | 38 | public boolean isUnread() { 39 | return mUnread; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_navigate_up.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_my_toolbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | 15 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_single_frame.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_sms_all.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 17 | 18 | 24 | 25 | 33 | 34 | 41 | 42 |