├── .gitattributes ├── .gitignore ├── Data ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── .classpath │ ├── .project │ ├── .settings │ └── org.eclipse.jdt.core.prefs │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── fernandocejas │ │ └── android10 │ │ └── sample │ │ └── data │ │ ├── cache │ │ ├── FileManager.java │ │ ├── UserCache.java │ │ ├── UserCacheImpl.java │ │ └── serializer │ │ │ └── JsonSerializer.java │ │ ├── entity │ │ ├── UserEntity.java │ │ └── mapper │ │ │ ├── UserEntityDataMapper.java │ │ │ └── UserEntityJsonMapper.java │ │ ├── exception │ │ ├── NetworkConnectionException.java │ │ ├── RepositoryErrorBundle.java │ │ └── UserNotFoundException.java │ │ ├── executor │ │ └── JobExecutor.java │ │ ├── net │ │ ├── ApiConnection.java │ │ ├── RestApi.java │ │ └── RestApiImpl.java │ │ └── repository │ │ ├── UserDataRepository.java │ │ └── datasource │ │ ├── CloudUserDataStore.java │ │ ├── DiskUserDataStore.java │ │ ├── UserDataStore.java │ │ └── UserDataStoreFactory.java │ ├── libs │ ├── dexmaker-1.0.jar │ ├── dexmaker-mockito-1.0.jar │ ├── espresso-1.1-bundled.jar │ ├── gson-2.2.4.jar │ └── mockito-all-1.9.5.jar │ └── project.properties ├── Domain ├── .classpath ├── .project ├── AndroidManifest.xml ├── ic_launcher-web.png ├── libs │ └── android-support-v4.jar ├── proguard-project.txt ├── project.properties ├── res │ ├── drawable-hdpi │ │ └── ic_launcher.png │ ├── drawable-mdpi │ │ └── ic_launcher.png │ ├── drawable-xhdpi │ │ └── ic_launcher.png │ ├── drawable-xxhdpi │ │ └── ic_launcher.png │ ├── values-v11 │ │ └── styles.xml │ ├── values-v14 │ │ └── styles.xml │ └── values │ │ ├── strings.xml │ │ └── styles.xml └── src │ └── com │ └── fernandocejas │ └── android10 │ └── sample │ └── domain │ ├── User.java │ ├── exception │ └── ErrorBundle.java │ ├── executor │ ├── PostExecutionThread.java │ └── ThreadExecutor.java │ ├── interactor │ ├── GetUserDetailsUseCase.java │ ├── GetUserDetailsUseCaseImpl.java │ ├── GetUserListUseCase.java │ ├── GetUserListUseCaseImpl.java │ └── Interactor.java │ └── repository │ └── UserRepository.java ├── Presentation ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── src │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── fernandocejas │ │ │ └── android10 │ │ │ └── sample │ │ │ └── test │ │ │ ├── exception │ │ │ └── ErrorMessageFactoryTest.java │ │ │ ├── mapper │ │ │ └── UserModelDataMapperTest.java │ │ │ ├── presenter │ │ │ ├── UserDetailsPresenterTest.java │ │ │ └── UserListPresenterTest.java │ │ │ └── view │ │ │ └── activity │ │ │ ├── UserDetailsActivityTest.java │ │ │ └── UserListActivityTest.java │ └── main │ │ ├── .classpath │ │ ├── .project │ │ ├── .settings │ │ └── org.eclipse.jdt.core.prefs │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── fernandocejas │ │ │ └── android10 │ │ │ └── sample │ │ │ └── presentation │ │ │ ├── UIThread.java │ │ │ ├── exception │ │ │ └── ErrorMessageFactory.java │ │ │ ├── mapper │ │ │ └── UserModelDataMapper.java │ │ │ ├── model │ │ │ └── UserModel.java │ │ │ ├── navigation │ │ │ └── Navigator.java │ │ │ ├── presenter │ │ │ ├── Presenter.java │ │ │ ├── UserDetailsPresenter.java │ │ │ └── UserListPresenter.java │ │ │ └── view │ │ │ ├── LoadDataView.java │ │ │ ├── UserDetailsView.java │ │ │ ├── UserListView.java │ │ │ ├── activity │ │ │ ├── BaseActivity.java │ │ │ ├── MainActivity.java │ │ │ ├── UserDetailsActivity.java │ │ │ └── UserListActivity.java │ │ │ ├── adapter │ │ │ └── UsersAdapter.java │ │ │ ├── component │ │ │ └── AutoLoadImageView.java │ │ │ └── fragment │ │ │ ├── BaseFragment.java │ │ │ ├── UserDetailsFragment.java │ │ │ └── UserListFragment.java │ │ ├── project.properties │ │ └── res │ │ ├── drawable-hdpi │ │ ├── ic_launcher.png │ │ └── logo.png │ │ ├── drawable-mdpi │ │ └── ic_launcher.png │ │ ├── drawable-xhdpi │ │ └── ic_launcher.png │ │ ├── drawable-xxhdpi │ │ └── ic_launcher.png │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── activity_user_details.xml │ │ ├── activity_user_list.xml │ │ ├── fragment_user_details.xml │ │ ├── fragment_user_list.xml │ │ ├── row_user.xml │ │ ├── view_progress.xml │ │ ├── view_retry.xml │ │ └── view_user_details.xml │ │ └── values │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml └── testLibs │ ├── dexmaker-1.0.jar │ ├── dexmaker-mockito-1.0.jar │ ├── espresso-1.1-bundled.jar │ └── mockito-all-1.9.5.jar └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Gradle files 16 | .gradle/ 17 | build/ 18 | 19 | # Local configuration file (sdk path, etc) 20 | local.properties 21 | 22 | # Proguard folder generated by Eclipse 23 | proguard/ 24 | 25 | # Log Files 26 | *.log 27 | 28 | # ========================= 29 | # Operating System Files 30 | # ========================= 31 | 32 | # OSX 33 | # ========================= 34 | 35 | .DS_Store 36 | .AppleDouble 37 | .LSOverride 38 | 39 | # Icon must end with two \r 40 | Icon 41 | 42 | # Thumbnails 43 | ._* 44 | 45 | # Files that might appear on external disk 46 | .Spotlight-V100 47 | .Trashes 48 | 49 | # Directories potentially created on remote AFP share 50 | .AppleDB 51 | .AppleDesktop 52 | Network Trash Folder 53 | Temporary Items 54 | .apdisk 55 | 56 | # Windows 57 | # ========================= 58 | 59 | # Windows image file caches 60 | Thumbs.db 61 | ehthumbs.db 62 | 63 | # Folder config file 64 | Desktop.ini 65 | 66 | # Recycle Bin used on file shares 67 | $RECYCLE.BIN/ 68 | 69 | # Windows Installer files 70 | *.cab 71 | *.msi 72 | *.msm 73 | *.msp 74 | -------------------------------------------------------------------------------- /Data/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /Data/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'android-library' 2 | 3 | android { 4 | compileSdkVersion 19 5 | buildToolsVersion '19.1.0' 6 | 7 | defaultConfig { 8 | applicationId "com.fernandocejas.android10.sample.data" 9 | minSdkVersion 15 10 | targetSdkVersion 19 11 | } 12 | 13 | packagingOptions { 14 | exclude 'META-INF/DEPENDENCIES' 15 | exclude 'META-INF/ASL2.0' 16 | exclude 'META-INF/NOTICE' 17 | exclude 'META-INF/LICENSE' 18 | } 19 | 20 | lintOptions { 21 | abortOnError false; 22 | disable 'InvalidPackage' // Some libraries have issues with this 23 | disable 'OldTargetApi' // Due to Robolectric that modifies the manifest when running tests 24 | } 25 | } 26 | 27 | dependencies { 28 | def domainLayer = project(':domain') 29 | 30 | //project dependencies 31 | compile domainLayer 32 | 33 | //library dependencies 34 | compile('com.google.code.gson:gson:2.2.4') 35 | } 36 | -------------------------------------------------------------------------------- /Data/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 /Users/fcejas/Software/SDKs/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 | -------------------------------------------------------------------------------- /Data/src/main/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Data/src/main/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | Data 4 | 5 | 6 | 7 | 8 | 9 | com.android.ide.eclipse.adt.ResourceManagerBuilder 10 | 11 | 12 | 13 | 14 | com.android.ide.eclipse.adt.PreCompilerBuilder 15 | 16 | 17 | 18 | 19 | org.eclipse.jdt.core.javabuilder 20 | 21 | 22 | 23 | 24 | com.android.ide.eclipse.adt.ApkBuilder 25 | 26 | 27 | 28 | 29 | 30 | com.android.ide.eclipse.adt.AndroidNature 31 | org.eclipse.jdt.core.javanature 32 | 33 | 34 | -------------------------------------------------------------------------------- /Data/src/main/.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 3 | org.eclipse.jdt.core.compiler.compliance=1.6 4 | org.eclipse.jdt.core.compiler.source=1.6 5 | -------------------------------------------------------------------------------- /Data/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Data/src/main/java/com/fernandocejas/android10/sample/data/cache/FileManager.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.data.cache; 6 | 7 | import android.content.Context; 8 | import android.content.SharedPreferences; 9 | import java.io.BufferedReader; 10 | import java.io.File; 11 | import java.io.FileNotFoundException; 12 | import java.io.FileReader; 13 | import java.io.FileWriter; 14 | import java.io.IOException; 15 | 16 | /** 17 | * Helper class to do operations on regular files/directories. 18 | */ 19 | public class FileManager { 20 | 21 | private FileManager() {} 22 | 23 | private static class LazyHolder { 24 | private static final FileManager INSTANCE = new FileManager(); 25 | } 26 | 27 | public static FileManager getInstance() { 28 | return LazyHolder.INSTANCE; 29 | } 30 | 31 | /** 32 | * Writes a file to Disk. 33 | * This is an I/O operation and this method executes in the main thread, so it is recommended to 34 | * perform this operation using another thread. 35 | * 36 | * @param file The file to write to Disk. 37 | */ 38 | public void writeToFile(File file, String fileContent) { 39 | if (!file.exists()) { 40 | try { 41 | FileWriter writer = new FileWriter(file); 42 | writer.write(fileContent); 43 | writer.close(); 44 | } catch (FileNotFoundException e) { 45 | e.printStackTrace(); 46 | } catch (IOException e) { 47 | e.printStackTrace(); 48 | } finally { 49 | 50 | } 51 | } 52 | } 53 | 54 | /** 55 | * Reads a content from a file. 56 | * This is an I/O operation and this method executes in the main thread, so it is recommended to 57 | * perform the operation using another thread. 58 | * 59 | * @param file The file to read from. 60 | * @return A string with the content of the file. 61 | */ 62 | public String readFileContent(File file) { 63 | StringBuilder fileContentBuilder = new StringBuilder(); 64 | if (file.exists()) { 65 | String stringLine; 66 | try { 67 | FileReader fileReader = new FileReader(file); 68 | BufferedReader bufferedReader = new BufferedReader(fileReader); 69 | while ((stringLine = bufferedReader.readLine()) != null) { 70 | fileContentBuilder.append(stringLine + "\n"); 71 | } 72 | bufferedReader.close(); 73 | fileReader.close(); 74 | } catch (FileNotFoundException e) { 75 | e.printStackTrace(); 76 | } catch (IOException e) { 77 | e.printStackTrace(); 78 | } 79 | } 80 | 81 | return fileContentBuilder.toString(); 82 | } 83 | 84 | /** 85 | * Returns a boolean indicating whether this file can be found on the underlying file system. 86 | * 87 | * @param file The file to check existence. 88 | * @return true if this file exists, false otherwise. 89 | */ 90 | public boolean exists(File file) { 91 | return file.exists(); 92 | } 93 | 94 | /** 95 | * Warning: Deletes the content of a directory. 96 | * This is an I/O operation and this method executes in the main thread, so it is recommended to 97 | * perform the operation using another thread. 98 | * 99 | * @param directory The directory which its content will be deleted. 100 | */ 101 | public void clearDirectory(File directory) { 102 | if (directory.exists()) { 103 | for (File file : directory.listFiles()) { 104 | file.delete(); 105 | } 106 | } 107 | } 108 | 109 | /** 110 | * Write a value to a user preferences file. 111 | * 112 | * @param context {@link android.content.Context} to retrieve android user preferences. 113 | * @param preferenceFileName A file name reprensenting where data will be written to. 114 | * @param key A string for the key that will be used to retrieve the value in the future. 115 | * @param value A long representing the value to be inserted. 116 | */ 117 | public void writeToPreferences(Context context, String preferenceFileName, String key, 118 | long value) { 119 | 120 | SharedPreferences sharedPreferences = context.getSharedPreferences(preferenceFileName, 121 | Context.MODE_PRIVATE); 122 | SharedPreferences.Editor editor = sharedPreferences.edit(); 123 | editor.putLong(key, value); 124 | editor.apply(); 125 | } 126 | 127 | /** 128 | * Get a value from a user preferences file. 129 | * 130 | * @param context {@link android.content.Context} to retrieve android user preferences. 131 | * @param preferenceFileName A file name representing where data will be get from. 132 | * @param key A key that will be used to retrieve the value from the preference file. 133 | * @return A long representing the value retrieved from the preferences file. 134 | */ 135 | public long getFromPreferences(Context context, String preferenceFileName, String key) { 136 | SharedPreferences sharedPreferences = context.getSharedPreferences(preferenceFileName, 137 | Context.MODE_PRIVATE); 138 | return sharedPreferences.getLong(key, 0); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Data/src/main/java/com/fernandocejas/android10/sample/data/cache/UserCache.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.data.cache; 6 | 7 | import com.fernandocejas.android10.sample.data.entity.UserEntity; 8 | 9 | /** 10 | * An interface representing a user Cache. 11 | */ 12 | public interface UserCache { 13 | 14 | /** 15 | * Callback used to be notified when a {@link UserEntity} has been loaded. 16 | */ 17 | interface UserCacheCallback { 18 | void onUserEntityLoaded(UserEntity userEntity); 19 | 20 | void onError(Exception exception); 21 | } 22 | 23 | /** 24 | * Gets an element from the cache using a {@link UserCacheCallback}. 25 | * 26 | * @param userId The user id to retrieve data. 27 | * @param callback The {@link UserCacheCallback} to notify the client. 28 | */ 29 | void get(final int userId, final UserCacheCallback callback); 30 | 31 | /** 32 | * Puts and element into the cache. 33 | * 34 | * @param userEntity Element to insert in the cache. 35 | */ 36 | void put(UserEntity userEntity); 37 | 38 | /** 39 | * Checks if an element (User) exists in the cache. 40 | * 41 | * @param userId The id used to look for inside the cache. 42 | * @return true if the element is cached, otherwise false. 43 | */ 44 | boolean isCached(final int userId); 45 | 46 | /** 47 | * Checks if the cache is expired. 48 | * 49 | * @return true, the cache is expired, otherwise false. 50 | */ 51 | boolean isExpired(); 52 | 53 | /** 54 | * Evict all elements of the cache. 55 | */ 56 | void evictAll(); 57 | } 58 | -------------------------------------------------------------------------------- /Data/src/main/java/com/fernandocejas/android10/sample/data/cache/UserCacheImpl.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.data.cache; 6 | 7 | import android.content.Context; 8 | import com.fernandocejas.android10.sample.data.cache.serializer.JsonSerializer; 9 | import com.fernandocejas.android10.sample.data.entity.UserEntity; 10 | import com.fernandocejas.android10.sample.data.exception.UserNotFoundException; 11 | import com.fernandocejas.android10.sample.domain.executor.ThreadExecutor; 12 | import java.io.File; 13 | 14 | /** 15 | * {@link UserCache} implementation. 16 | */ 17 | public class UserCacheImpl implements UserCache { 18 | 19 | private static UserCacheImpl INSTANCE; 20 | 21 | public static synchronized UserCacheImpl getInstance(Context context, 22 | JsonSerializer userCacheSerializer, FileManager fileManager, ThreadExecutor threadExecutor) { 23 | if (INSTANCE == null) { 24 | INSTANCE = new UserCacheImpl(context, userCacheSerializer, fileManager, threadExecutor); 25 | } 26 | return INSTANCE; 27 | } 28 | 29 | private static final String SETTINGS_FILE_NAME = "com.fernandocejas.android10.SETTINGS"; 30 | private static final String SETTINGS_KEY_LAST_CACHE_UPDATE = "last_cache_update"; 31 | 32 | private static final String DEFAULT_FILE_NAME = "user_"; 33 | private static final long EXPIRATION_TIME = 60 * 10 * 1000; 34 | 35 | private final Context context; 36 | private final File cacheDir; 37 | private final JsonSerializer serializer; 38 | private final FileManager fileManager; 39 | private final ThreadExecutor threadExecutor; 40 | 41 | /** 42 | * Constructor of the class {@link UserCacheImpl}. 43 | * 44 | * @param context A 45 | * @param userCacheSerializer {@link JsonSerializer} for object serialization. 46 | * @param fileManager {@link FileManager} for saving serialized objects to the file system. 47 | */ 48 | private UserCacheImpl(Context context, JsonSerializer userCacheSerializer, 49 | FileManager fileManager, ThreadExecutor executor) { 50 | if (context == null || userCacheSerializer == null || fileManager == null || executor == null) { 51 | throw new IllegalArgumentException("Invalid null parameter"); 52 | } 53 | this.context = context.getApplicationContext(); 54 | this.cacheDir = this.context.getCacheDir(); 55 | this.serializer = userCacheSerializer; 56 | this.fileManager = fileManager; 57 | this.threadExecutor = executor; 58 | } 59 | 60 | /** 61 | * {@inheritDoc} 62 | * 63 | * @param userId The user id to retrieve data. 64 | * @param callback The {@link UserCacheCallback} to notify the client. 65 | */ 66 | @Override public synchronized void get(int userId, UserCacheCallback callback) { 67 | File userEntitiyFile = this.buildFile(userId); 68 | String fileContent = this.fileManager.readFileContent(userEntitiyFile); 69 | UserEntity userEntity = this.serializer.deserialize(fileContent); 70 | 71 | if (userEntity != null) { 72 | callback.onUserEntityLoaded(userEntity); 73 | } else { 74 | callback.onError(new UserNotFoundException()); 75 | } 76 | } 77 | 78 | /** 79 | * {@inheritDoc} 80 | * 81 | * @param userEntity Element to insert in the cache. 82 | */ 83 | @Override public synchronized void put(UserEntity userEntity) { 84 | if (userEntity != null) { 85 | File userEntitiyFile = this.buildFile(userEntity.getUserId()); 86 | if (!isCached(userEntity.getUserId())) { 87 | String jsonString = this.serializer.serialize(userEntity); 88 | this.executeAsynchronously(new CacheWriter(this.fileManager, userEntitiyFile, 89 | jsonString)); 90 | setLastCacheUpdateTimeMillis(); 91 | } 92 | } 93 | } 94 | 95 | /** 96 | * {@inheritDoc} 97 | * 98 | * @param userId The id used to look for inside the cache. 99 | * @return true if the element is cached, otherwise false. 100 | */ 101 | @Override public boolean isCached(int userId) { 102 | File userEntitiyFile = this.buildFile(userId); 103 | return this.fileManager.exists(userEntitiyFile); 104 | } 105 | 106 | /** 107 | * {@inheritDoc} 108 | * 109 | * @return true, the cache is expired, otherwise false. 110 | */ 111 | @Override public boolean isExpired() { 112 | long currentTime = System.currentTimeMillis(); 113 | long lastUpdateTime = this.getLastCacheUpdateTimeMillis(); 114 | 115 | boolean expired = ((currentTime - lastUpdateTime) > EXPIRATION_TIME); 116 | 117 | if (expired) { 118 | this.evictAll(); 119 | } 120 | 121 | return expired; 122 | } 123 | 124 | /** 125 | * {@inheritDoc} 126 | */ 127 | @Override public synchronized void evictAll() { 128 | this.executeAsynchronously(new CacheEvictor(this.fileManager, this.cacheDir)); 129 | } 130 | 131 | /** 132 | * Build a file, used to be inserted in the disk cache. 133 | * 134 | * @param userId The id user to build the file. 135 | * @return A valid file. 136 | */ 137 | private File buildFile(int userId) { 138 | StringBuilder fileNameBuilder = new StringBuilder(); 139 | fileNameBuilder.append(this.cacheDir.getPath()); 140 | fileNameBuilder.append(File.separator); 141 | fileNameBuilder.append(DEFAULT_FILE_NAME); 142 | fileNameBuilder.append(userId); 143 | 144 | return new File(fileNameBuilder.toString()); 145 | } 146 | 147 | /** 148 | * Set in millis, the last time the cache was accessed. 149 | */ 150 | private void setLastCacheUpdateTimeMillis() { 151 | long currentMillis = System.currentTimeMillis(); 152 | this.fileManager.writeToPreferences(this.context, SETTINGS_FILE_NAME, 153 | SETTINGS_KEY_LAST_CACHE_UPDATE, currentMillis); 154 | } 155 | 156 | /** 157 | * Get in millis, the last time the cache was accessed. 158 | */ 159 | private long getLastCacheUpdateTimeMillis() { 160 | return this.fileManager.getFromPreferences(this.context, SETTINGS_FILE_NAME, 161 | SETTINGS_KEY_LAST_CACHE_UPDATE); 162 | } 163 | 164 | /** 165 | * Executes a {@link Runnable} in another Thread. 166 | * 167 | * @param runnable {@link Runnable} to execute 168 | */ 169 | private void executeAsynchronously(Runnable runnable) { 170 | this.threadExecutor.execute(runnable); 171 | } 172 | 173 | /** 174 | * {@link Runnable} class for writing to disk. 175 | */ 176 | private static class CacheWriter implements Runnable { 177 | private final FileManager fileManager; 178 | private final File fileToWrite; 179 | private final String fileContent; 180 | 181 | CacheWriter(FileManager fileManager, File fileToWrite, String fileContent) { 182 | this.fileManager = fileManager; 183 | this.fileToWrite = fileToWrite; 184 | this.fileContent = fileContent; 185 | } 186 | 187 | @Override public void run() { 188 | this.fileManager.writeToFile(fileToWrite, fileContent); 189 | } 190 | } 191 | 192 | /** 193 | * {@link Runnable} class for evicting all the cached files 194 | */ 195 | private static class CacheEvictor implements Runnable { 196 | private final FileManager fileManager; 197 | private final File cacheDir; 198 | 199 | CacheEvictor(FileManager fileManager, File cacheDir) { 200 | this.fileManager = fileManager; 201 | this.cacheDir = cacheDir; 202 | } 203 | 204 | @Override public void run() { 205 | this.fileManager.clearDirectory(this.cacheDir); 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /Data/src/main/java/com/fernandocejas/android10/sample/data/cache/serializer/JsonSerializer.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.data.cache.serializer; 6 | 7 | import com.fernandocejas.android10.sample.data.entity.UserEntity; 8 | import com.google.gson.Gson; 9 | 10 | /** 11 | * Class user as Serializer/Deserializer for user entities. 12 | */ 13 | public class JsonSerializer { 14 | 15 | private final Gson gson = new Gson(); 16 | 17 | public JsonSerializer() { 18 | //empty 19 | } 20 | 21 | /** 22 | * Serialize an object to Json. 23 | * 24 | * @param userEntity {@link UserEntity} to serialize. 25 | */ 26 | public String serialize(UserEntity userEntity) { 27 | String jsonString = gson.toJson(userEntity, UserEntity.class); 28 | return jsonString; 29 | } 30 | 31 | /** 32 | * Deserialize a json representation of an object. 33 | * 34 | * @param jsonString A json string to deserialize. 35 | * @return {@link UserEntity} 36 | */ 37 | public UserEntity deserialize(String jsonString) { 38 | UserEntity userEntity = gson.fromJson(jsonString, UserEntity.class); 39 | return userEntity; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Data/src/main/java/com/fernandocejas/android10/sample/data/entity/UserEntity.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.data.entity; 6 | 7 | import com.google.gson.annotations.SerializedName; 8 | 9 | /** 10 | * User Entity used in the data layer. 11 | */ 12 | public class UserEntity { 13 | 14 | @SerializedName("id") 15 | private int userId; 16 | 17 | @SerializedName("cover_url") 18 | private String coverUrl; 19 | 20 | @SerializedName("full_name") 21 | private String fullname; 22 | 23 | @SerializedName("description") 24 | private String description; 25 | 26 | @SerializedName("followers") 27 | private int followers; 28 | 29 | @SerializedName("email") 30 | private String email; 31 | 32 | public UserEntity() { 33 | //empty 34 | } 35 | 36 | public int getUserId() { 37 | return userId; 38 | } 39 | 40 | public void setUserId(int userId) { 41 | this.userId = userId; 42 | } 43 | 44 | public String getCoverUrl() { 45 | return coverUrl; 46 | } 47 | 48 | public void setCoverUrl(String coverUrl) { 49 | this.coverUrl = coverUrl; 50 | } 51 | 52 | public String getFullname() { 53 | return fullname; 54 | } 55 | 56 | public void setFullname(String fullname) { 57 | this.fullname = fullname; 58 | } 59 | 60 | public String getDescription() { 61 | return description; 62 | } 63 | 64 | public void setDescription(String description) { 65 | this.description = description; 66 | } 67 | 68 | public int getFollowers() { 69 | return followers; 70 | } 71 | 72 | public void setFollowers(int followers) { 73 | this.followers = followers; 74 | } 75 | 76 | public String getEmail() { 77 | return email; 78 | } 79 | 80 | public void setEmail(String email) { 81 | this.email = email; 82 | } 83 | 84 | @Override public String toString() { 85 | StringBuilder stringBuilder = new StringBuilder(); 86 | 87 | stringBuilder.append("***** User Entity Details *****\n"); 88 | stringBuilder.append("id=" + this.getUserId() + "\n"); 89 | stringBuilder.append("cover url=" + this.getCoverUrl() + "\n"); 90 | stringBuilder.append("fullname=" + this.getFullname() + "\n"); 91 | stringBuilder.append("email=" + this.getEmail() + "\n"); 92 | stringBuilder.append("description=" + this.getDescription() + "\n"); 93 | stringBuilder.append("followers=" + this.getFollowers() + "\n"); 94 | stringBuilder.append("*******************************"); 95 | 96 | return stringBuilder.toString(); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Data/src/main/java/com/fernandocejas/android10/sample/data/entity/mapper/UserEntityDataMapper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.data.entity.mapper; 6 | 7 | import com.fernandocejas.android10.sample.data.entity.UserEntity; 8 | import com.fernandocejas.android10.sample.domain.User; 9 | import java.util.ArrayList; 10 | import java.util.Collection; 11 | import java.util.List; 12 | 13 | /** 14 | * Mapper class used to transform {@link UserEntity} (in the data layer) to {@link User} in the 15 | * domain layer. 16 | */ 17 | public class UserEntityDataMapper { 18 | 19 | public UserEntityDataMapper() { 20 | //empty 21 | } 22 | 23 | /** 24 | * Transform a {@link UserEntity} into an {@link User}. 25 | * 26 | * @param userEntity Object to be transformed. 27 | * @return {@link User} if valid {@link UserEntity} otherwise null. 28 | */ 29 | public User transform(UserEntity userEntity) { 30 | User user = null; 31 | if (userEntity != null) { 32 | user = new User(userEntity.getUserId()); 33 | user.setCoverUrl(userEntity.getCoverUrl()); 34 | user.setFullName(userEntity.getFullname()); 35 | user.setDescription(userEntity.getDescription()); 36 | user.setFollowers(userEntity.getFollowers()); 37 | user.setEmail(userEntity.getEmail()); 38 | } 39 | 40 | return user; 41 | } 42 | 43 | /** 44 | * Transform a Collection of {@link UserEntity} into a Collection of {@link User}. 45 | * 46 | * @param userEntityCollection Object Collection to be transformed. 47 | * @return {@link User} if valid {@link UserEntity} otherwise null. 48 | */ 49 | public Collection transform(Collection userEntityCollection) { 50 | List userList = new ArrayList(20); 51 | User user; 52 | for (UserEntity userEntity : userEntityCollection) { 53 | user = transform(userEntity); 54 | if (user != null) { 55 | userList.add(user); 56 | } 57 | } 58 | 59 | return userList; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Data/src/main/java/com/fernandocejas/android10/sample/data/entity/mapper/UserEntityJsonMapper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.data.entity.mapper; 6 | 7 | import com.fernandocejas.android10.sample.data.entity.UserEntity; 8 | import com.google.gson.Gson; 9 | import com.google.gson.JsonSyntaxException; 10 | import com.google.gson.reflect.TypeToken; 11 | import java.lang.reflect.Type; 12 | import java.util.Collection; 13 | 14 | /** 15 | * Class used to transform from Strings representing json to valid objects. 16 | */ 17 | public class UserEntityJsonMapper { 18 | 19 | private final Gson gson; 20 | 21 | public UserEntityJsonMapper() { 22 | this.gson = new Gson(); 23 | } 24 | 25 | /** 26 | * Transform from valid json string to {@link UserEntity}. 27 | * 28 | * @param userJsonResponse A json representing a user profile. 29 | * @return {@link UserEntity}. 30 | * @throws com.google.gson.JsonSyntaxException if the json string is not a valid json structure. 31 | */ 32 | public UserEntity transformUserEntity(String userJsonResponse) throws JsonSyntaxException { 33 | try { 34 | Type userEntityType = new TypeToken() {}.getType(); 35 | UserEntity userEntity = this.gson.fromJson(userJsonResponse, userEntityType); 36 | 37 | return userEntity; 38 | } catch (JsonSyntaxException jsonException) { 39 | throw jsonException; 40 | } 41 | } 42 | 43 | /** 44 | * Transform from valid json string to Collection of {@link UserEntity}. 45 | * 46 | * @param userListJsonResponse A json representing a collection of users. 47 | * @return Collection of {@link UserEntity}. 48 | * @throws com.google.gson.JsonSyntaxException if the json string is not a valid json structure. 49 | */ 50 | public Collection transformUserEntityCollection(String userListJsonResponse) 51 | throws JsonSyntaxException { 52 | 53 | Collection userEntityCollection; 54 | try { 55 | Type listOfUserEntityType = new TypeToken>() {}.getType(); 56 | userEntityCollection = this.gson.fromJson(userListJsonResponse, listOfUserEntityType); 57 | 58 | return userEntityCollection; 59 | } catch (JsonSyntaxException jsonException) { 60 | throw jsonException; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Data/src/main/java/com/fernandocejas/android10/sample/data/exception/NetworkConnectionException.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.data.exception; 6 | 7 | /** 8 | * Exception throw by the application when a there is a network connection exception. 9 | */ 10 | public class NetworkConnectionException extends Exception { 11 | 12 | public NetworkConnectionException() { 13 | super(); 14 | } 15 | 16 | public NetworkConnectionException(final String message) { 17 | super(message); 18 | } 19 | 20 | public NetworkConnectionException(final String message, final Throwable cause) { 21 | super(message, cause); 22 | } 23 | 24 | public NetworkConnectionException(final Throwable cause) { 25 | super(cause); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Data/src/main/java/com/fernandocejas/android10/sample/data/exception/RepositoryErrorBundle.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.data.exception; 6 | 7 | import com.fernandocejas.android10.sample.domain.exception.ErrorBundle; 8 | 9 | /** 10 | * Wrapper around Exceptions used to manage errors in the repository. 11 | */ 12 | public class RepositoryErrorBundle implements ErrorBundle { 13 | 14 | private final Exception exception; 15 | 16 | public RepositoryErrorBundle(Exception exception) { 17 | this.exception = exception; 18 | } 19 | 20 | public Exception getException() { 21 | return exception; 22 | } 23 | 24 | public String getErrorMessage() { 25 | String message = ""; 26 | if (this.exception != null) { 27 | this.exception.getMessage(); 28 | } 29 | return message; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Data/src/main/java/com/fernandocejas/android10/sample/data/exception/UserNotFoundException.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.data.exception; 6 | 7 | /** 8 | * Exception throw by the application when a User search can't return a valid result. 9 | */ 10 | public class UserNotFoundException extends Exception { 11 | 12 | public UserNotFoundException() { 13 | super(); 14 | } 15 | 16 | public UserNotFoundException(final String message) { 17 | super(message); 18 | } 19 | 20 | public UserNotFoundException(final String message, final Throwable cause) { 21 | super(message, cause); 22 | } 23 | 24 | public UserNotFoundException(final Throwable cause) { 25 | super(cause); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Data/src/main/java/com/fernandocejas/android10/sample/data/executor/JobExecutor.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.data.executor; 6 | 7 | import com.fernandocejas.android10.sample.domain.executor.ThreadExecutor; 8 | import java.util.concurrent.BlockingQueue; 9 | import java.util.concurrent.LinkedBlockingQueue; 10 | import java.util.concurrent.ThreadPoolExecutor; 11 | import java.util.concurrent.TimeUnit; 12 | 13 | /** 14 | * Decorated {@link java.util.concurrent.ThreadPoolExecutor} Singleton class based on 15 | * 'Initialization on Demand Holder' pattern. 16 | */ 17 | public class JobExecutor implements ThreadExecutor { 18 | 19 | private static class LazyHolder { 20 | private static final JobExecutor INSTANCE = new JobExecutor(); 21 | } 22 | 23 | public static JobExecutor getInstance() { 24 | return LazyHolder.INSTANCE; 25 | } 26 | 27 | private static final int INITIAL_POOL_SIZE = 3; 28 | private static final int MAX_POOL_SIZE = 5; 29 | 30 | // Sets the amount of time an idle thread waits before terminating 31 | private static final int KEEP_ALIVE_TIME = 10; 32 | 33 | // Sets the Time Unit to seconds 34 | private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS; 35 | 36 | private final BlockingQueue workQueue; 37 | 38 | private final ThreadPoolExecutor threadPoolExecutor; 39 | 40 | private JobExecutor() { 41 | this.workQueue = new LinkedBlockingQueue(); 42 | this.threadPoolExecutor = new ThreadPoolExecutor(INITIAL_POOL_SIZE, MAX_POOL_SIZE, 43 | KEEP_ALIVE_TIME, KEEP_ALIVE_TIME_UNIT, this.workQueue); 44 | } 45 | 46 | /** 47 | * {@inheritDoc} 48 | * 49 | * @param runnable The class that implements {@link Runnable} interface. 50 | */ 51 | @Override public void execute(Runnable runnable) { 52 | if (runnable == null) { 53 | throw new IllegalArgumentException("Runnable to execute cannot be null"); 54 | } 55 | this.threadPoolExecutor.execute(runnable); 56 | } 57 | } -------------------------------------------------------------------------------- /Data/src/main/java/com/fernandocejas/android10/sample/data/net/ApiConnection.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.data.net; 6 | 7 | import java.io.BufferedReader; 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.io.InputStreamReader; 11 | import java.net.HttpURLConnection; 12 | import java.net.MalformedURLException; 13 | import java.net.URL; 14 | import java.util.concurrent.Callable; 15 | 16 | /** 17 | * Api Connection class used to retrieve data from the cloud. 18 | * Implements {@link java.util.concurrent.Callable} so when executed asynchronously can 19 | * return a value. 20 | */ 21 | public class ApiConnection implements Callable { 22 | 23 | private static final String CONTENT_TYPE_LABEL = "Content-Type"; 24 | private static final String CONTENT_TYPE_VALUE_JSON = "application/json; charset=utf-8"; 25 | 26 | public static final String REQUEST_METHOD_GET = "GET"; 27 | 28 | private URL url; 29 | private String requestVerb; 30 | private int responseCode = 0; 31 | private String response = ""; 32 | 33 | private ApiConnection(String url, String requestVerb) throws MalformedURLException { 34 | this.url = new URL(url); 35 | this.requestVerb = requestVerb; 36 | } 37 | 38 | public static ApiConnection createGET(String url) throws MalformedURLException { 39 | return new ApiConnection(url, REQUEST_METHOD_GET); 40 | } 41 | 42 | /** 43 | * Do a request to an api asynchronously. 44 | * It should not be executed in the main thread of the application. 45 | * 46 | * @return A string response 47 | */ 48 | public String requestSyncCall() { 49 | connectToApi(); 50 | return response; 51 | } 52 | 53 | private void connectToApi() { 54 | HttpURLConnection urlConnection = null; 55 | 56 | try { 57 | urlConnection = (HttpURLConnection) url.openConnection(); 58 | setupConnection(urlConnection); 59 | 60 | responseCode = urlConnection.getResponseCode(); 61 | if (responseCode == HttpURLConnection.HTTP_OK) { 62 | response = getStringFromInputStream(urlConnection.getInputStream()); 63 | } else { response = getStringFromInputStream(urlConnection.getErrorStream()); } 64 | } catch (Exception e) { 65 | e.printStackTrace(); 66 | } finally { 67 | if (urlConnection != null) { urlConnection.disconnect(); } 68 | } 69 | } 70 | 71 | private String getStringFromInputStream(InputStream inputStream) { 72 | 73 | BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); 74 | StringBuilder stringBuilderResult = new StringBuilder(); 75 | 76 | String line; 77 | try { 78 | while ((line = bufferedReader.readLine()) != null) { 79 | stringBuilderResult.append(line); 80 | } 81 | return stringBuilderResult.toString(); 82 | } catch (IOException e) { 83 | e.printStackTrace(); 84 | return ""; 85 | } 86 | } 87 | 88 | private void setupConnection(HttpURLConnection connection) throws IOException { 89 | if (connection != null) { 90 | connection.setRequestMethod(requestVerb); 91 | connection.setReadTimeout(10000); 92 | connection.setConnectTimeout(15000); 93 | connection.setDoInput(true); 94 | connection.setRequestProperty(CONTENT_TYPE_LABEL, CONTENT_TYPE_VALUE_JSON); 95 | } 96 | } 97 | 98 | @Override public String call() throws Exception { 99 | return requestSyncCall(); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Data/src/main/java/com/fernandocejas/android10/sample/data/net/RestApi.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.data.net; 6 | 7 | import com.fernandocejas.android10.sample.data.entity.UserEntity; 8 | import com.fernandocejas.android10.sample.domain.User; 9 | import java.util.Collection; 10 | 11 | /** 12 | * RestApi for retrieving data from the network. 13 | */ 14 | public interface RestApi { 15 | /** 16 | * Callback used to be notified when either a user list has been loaded or an error happened. 17 | */ 18 | interface UserListCallback { 19 | void onUserListLoaded(Collection usersCollection); 20 | 21 | void onError(Exception exception); 22 | } 23 | 24 | /** 25 | * Callback to be notified when getting a user from the network. 26 | */ 27 | interface UserDetailsCallback { 28 | void onUserEntityLoaded(UserEntity userEntity); 29 | 30 | void onError(Exception exception); 31 | } 32 | 33 | static final String API_BASE_URL = "http://www.android10.org/myapi/"; 34 | 35 | /** Api url for getting all users */ 36 | static final String API_URL_GET_USER_LIST = API_BASE_URL + "users.json"; 37 | /** Api url for getting a user profile: Remember to concatenate id + 'json' */ 38 | static final String API_URL_GET_USER_DETAILS = API_BASE_URL + "user_"; 39 | 40 | /** 41 | * Get a collection of {@link User}. 42 | * 43 | * @param userListCallback A {@link UserListCallback} used for notifying clients. 44 | */ 45 | void getUserList(UserListCallback userListCallback); 46 | 47 | /** 48 | * Retrieves a user by id from the network. 49 | * 50 | * @param userId The user id used to get user data. 51 | * @param userDetailsCallback {@link UserDetailsCallback} to be notified when user data has been 52 | * retrieved. 53 | */ 54 | void getUserById(final int userId, final UserDetailsCallback userDetailsCallback); 55 | } 56 | -------------------------------------------------------------------------------- /Data/src/main/java/com/fernandocejas/android10/sample/data/net/RestApiImpl.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.data.net; 6 | 7 | import android.content.Context; 8 | import android.net.ConnectivityManager; 9 | import android.net.NetworkInfo; 10 | import com.fernandocejas.android10.sample.data.entity.UserEntity; 11 | import com.fernandocejas.android10.sample.data.entity.mapper.UserEntityJsonMapper; 12 | import com.fernandocejas.android10.sample.data.exception.NetworkConnectionException; 13 | import java.util.Collection; 14 | 15 | /** 16 | * {@link RestApi} implementation for retrieving data from the network. 17 | */ 18 | public class RestApiImpl implements RestApi { 19 | 20 | private final Context context; 21 | private final UserEntityJsonMapper userEntityJsonMapper; 22 | 23 | /** 24 | * Constructor of the class 25 | * 26 | * @param context {@link android.content.Context}. 27 | * @param userEntityJsonMapper {@link UserEntityJsonMapper}. 28 | */ 29 | public RestApiImpl(Context context, UserEntityJsonMapper userEntityJsonMapper) { 30 | if (context == null || userEntityJsonMapper == null) { 31 | throw new IllegalArgumentException("The constructor parameters cannot be null!!!"); 32 | } 33 | this.context = context.getApplicationContext(); 34 | this.userEntityJsonMapper = userEntityJsonMapper; 35 | } 36 | 37 | @Override public void getUserList(UserListCallback userListCallback) { 38 | if (userListCallback == null) { 39 | throw new IllegalArgumentException("Callback cannot be null!!!"); 40 | } 41 | 42 | if (isThereInternetConnection()) { 43 | try { 44 | ApiConnection getUserListConnection = 45 | ApiConnection.createGET(RestApi.API_URL_GET_USER_LIST); 46 | String responseUserList = getUserListConnection.requestSyncCall(); 47 | Collection userEntityList = 48 | this.userEntityJsonMapper.transformUserEntityCollection(responseUserList); 49 | 50 | userListCallback.onUserListLoaded(userEntityList); 51 | } catch (Exception e) { 52 | userListCallback.onError(new NetworkConnectionException(e.getCause())); 53 | } 54 | } else { 55 | userListCallback.onError(new NetworkConnectionException()); 56 | } 57 | } 58 | 59 | /** 60 | * {@inheritDoc} 61 | */ 62 | @Override public void getUserById(final int userId, 63 | final UserDetailsCallback userDetailsCallback) { 64 | if (userDetailsCallback == null) { 65 | throw new IllegalArgumentException("Callback cannot be null!!!"); 66 | } 67 | 68 | if (isThereInternetConnection()) { 69 | try { 70 | String apiUrl = RestApi.API_URL_GET_USER_DETAILS + userId + ".json"; 71 | ApiConnection getUserDetailsConnection = ApiConnection.createGET(apiUrl); 72 | String responseUserDetails = getUserDetailsConnection.requestSyncCall(); 73 | UserEntity userEntity = this.userEntityJsonMapper.transformUserEntity(responseUserDetails); 74 | 75 | userDetailsCallback.onUserEntityLoaded(userEntity); 76 | } catch (Exception e) { 77 | userDetailsCallback.onError(new NetworkConnectionException(e.getCause())); 78 | } 79 | } else { 80 | userDetailsCallback.onError(new NetworkConnectionException()); 81 | } 82 | } 83 | 84 | /** 85 | * Checks if the device has any active internet connection. 86 | * 87 | * @return true device with internet connection, otherwise false. 88 | */ 89 | private boolean isThereInternetConnection() { 90 | boolean isConnected; 91 | 92 | ConnectivityManager connectivityManager = 93 | (ConnectivityManager) this.context.getSystemService(Context.CONNECTIVITY_SERVICE); 94 | NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); 95 | isConnected = (networkInfo != null && networkInfo.isConnectedOrConnecting()); 96 | 97 | return isConnected; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Data/src/main/java/com/fernandocejas/android10/sample/data/repository/UserDataRepository.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.data.repository; 6 | 7 | import com.fernandocejas.android10.sample.data.entity.UserEntity; 8 | import com.fernandocejas.android10.sample.data.entity.mapper.UserEntityDataMapper; 9 | import com.fernandocejas.android10.sample.data.exception.RepositoryErrorBundle; 10 | import com.fernandocejas.android10.sample.data.exception.UserNotFoundException; 11 | import com.fernandocejas.android10.sample.data.repository.datasource.UserDataStore; 12 | import com.fernandocejas.android10.sample.data.repository.datasource.UserDataStoreFactory; 13 | import com.fernandocejas.android10.sample.domain.User; 14 | import com.fernandocejas.android10.sample.domain.repository.UserRepository; 15 | import java.util.Collection; 16 | 17 | /** 18 | * {@link UserRepository} for retrieving user data. 19 | */ 20 | public class UserDataRepository implements UserRepository { 21 | 22 | private static UserDataRepository INSTANCE; 23 | 24 | public static synchronized UserDataRepository getInstance(UserDataStoreFactory dataStoreFactory, 25 | UserEntityDataMapper userEntityDataMapper) { 26 | if (INSTANCE == null) { 27 | INSTANCE = new UserDataRepository(dataStoreFactory, userEntityDataMapper); 28 | } 29 | return INSTANCE; 30 | } 31 | 32 | private final UserDataStoreFactory userDataStoreFactory; 33 | private final UserEntityDataMapper userEntityDataMapper; 34 | 35 | /** 36 | * Constructs a {@link UserRepository}. 37 | * 38 | * @param dataStoreFactory A factory to construct different data source implementations. 39 | * @param userEntityDataMapper {@link UserEntityDataMapper}. 40 | */ 41 | protected UserDataRepository(UserDataStoreFactory dataStoreFactory, 42 | UserEntityDataMapper userEntityDataMapper) { 43 | if (dataStoreFactory == null || userEntityDataMapper == null) { 44 | throw new IllegalArgumentException("Invalid null parameters in constructor!!!"); 45 | } 46 | this.userDataStoreFactory = dataStoreFactory; 47 | this.userEntityDataMapper = userEntityDataMapper; 48 | } 49 | 50 | /** 51 | * {@inheritDoc} 52 | * 53 | * @param userListCallback A {@link UserListCallback} used for notifying clients. 54 | */ 55 | @Override public void getUserList(final UserListCallback userListCallback) { 56 | //we always get all users from the cloud 57 | final UserDataStore userDataStore = this.userDataStoreFactory.createCloudDataStore(); 58 | userDataStore.getUsersEntityList(new UserDataStore.UserListCallback() { 59 | @Override public void onUserListLoaded(Collection usersCollection) { 60 | Collection users = 61 | UserDataRepository.this.userEntityDataMapper.transform(usersCollection); 62 | userListCallback.onUserListLoaded(users); 63 | } 64 | 65 | @Override public void onError(Exception exception) { 66 | userListCallback.onError(new RepositoryErrorBundle(exception)); 67 | } 68 | }); 69 | } 70 | 71 | /** 72 | * {@inheritDoc} 73 | * 74 | * @param userId The user id used to retrieve user data. 75 | * @param userCallback A {@link com.fernandocejas.android10.sample.domain.repository.UserRepository.UserDetailsCallback} 76 | * used for notifying clients. 77 | */ 78 | @Override public void getUserById(final int userId, final UserDetailsCallback userCallback) { 79 | UserDataStore userDataStore = this.userDataStoreFactory.create(userId); 80 | userDataStore.getUserEntityDetails(userId, new UserDataStore.UserDetailsCallback() { 81 | @Override public void onUserEntityLoaded(UserEntity userEntity) { 82 | User user = UserDataRepository.this.userEntityDataMapper.transform(userEntity); 83 | if (user != null) { 84 | userCallback.onUserLoaded(user); 85 | } else { 86 | userCallback.onError(new RepositoryErrorBundle(new UserNotFoundException())); 87 | } 88 | } 89 | 90 | @Override public void onError(Exception exception) { 91 | userCallback.onError(new RepositoryErrorBundle(exception)); 92 | } 93 | }); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Data/src/main/java/com/fernandocejas/android10/sample/data/repository/datasource/CloudUserDataStore.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.data.repository.datasource; 6 | 7 | import com.fernandocejas.android10.sample.data.cache.UserCache; 8 | import com.fernandocejas.android10.sample.data.entity.UserEntity; 9 | import com.fernandocejas.android10.sample.data.net.RestApi; 10 | import java.util.Collection; 11 | 12 | /** 13 | * {@link UserDataStore} implementation based on connections to the api (Cloud). 14 | */ 15 | public class CloudUserDataStore implements UserDataStore { 16 | 17 | private final RestApi restApi; 18 | private final UserCache userCache; 19 | 20 | /** 21 | * Construct a {@link UserDataStore} based on connections to the api (Cloud). 22 | * 23 | * @param restApi The {@link RestApi} implementation to use. 24 | * @param userCache A {@link UserCache} to cache data retrieved from the api. 25 | */ 26 | public CloudUserDataStore(RestApi restApi, UserCache userCache) { 27 | this.restApi = restApi; 28 | this.userCache = userCache; 29 | } 30 | 31 | /** 32 | * {@inheritDoc} 33 | * 34 | * @param userListCallback A {@link UserListCallback} used for notifying clients. 35 | */ 36 | @Override public void getUsersEntityList(final UserListCallback userListCallback) { 37 | this.restApi.getUserList(new RestApi.UserListCallback() { 38 | @Override public void onUserListLoaded(Collection usersCollection) { 39 | userListCallback.onUserListLoaded(usersCollection); 40 | } 41 | 42 | @Override public void onError(Exception exception) { 43 | userListCallback.onError(exception); 44 | } 45 | }); 46 | } 47 | 48 | /** 49 | * {@inheritDoc} 50 | * 51 | * @param id The user id used to retrieve user data. 52 | * @param userDetailsCallback A {@link UserDetailsCallback} used for notifying clients. 53 | */ 54 | @Override public void getUserEntityDetails(int id, 55 | final UserDetailsCallback userDetailsCallback) { 56 | this.restApi.getUserById(id, new RestApi.UserDetailsCallback() { 57 | @Override public void onUserEntityLoaded(UserEntity userEntity) { 58 | userDetailsCallback.onUserEntityLoaded(userEntity); 59 | CloudUserDataStore.this.putUserEntityInCache(userEntity); 60 | } 61 | 62 | @Override public void onError(Exception exception) { 63 | userDetailsCallback.onError(exception); 64 | } 65 | }); 66 | } 67 | 68 | /** 69 | * Saves a {@link UserEntity} into cache. 70 | * 71 | * @param userEntity The {@link UserEntity} to save. 72 | */ 73 | private void putUserEntityInCache(UserEntity userEntity) { 74 | if (userEntity != null) { 75 | this.userCache.put(userEntity); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Data/src/main/java/com/fernandocejas/android10/sample/data/repository/datasource/DiskUserDataStore.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.data.repository.datasource; 6 | 7 | import com.fernandocejas.android10.sample.data.cache.UserCache; 8 | import com.fernandocejas.android10.sample.data.entity.UserEntity; 9 | 10 | /** 11 | * {@link UserDataStore} implementation based on file system data store. 12 | */ 13 | public class DiskUserDataStore implements UserDataStore { 14 | 15 | private final UserCache userCache; 16 | 17 | /** 18 | * Construct a {@link UserDataStore} based file system data store. 19 | * 20 | * @param userCache A {@link UserCache} to cache data retrieved from the api. 21 | */ 22 | public DiskUserDataStore(UserCache userCache) { 23 | this.userCache = userCache; 24 | } 25 | 26 | /** 27 | * {@inheritDoc} 28 | * 29 | * @param userListCallback A {@link UserListCallback} used for notifying clients. 30 | */ 31 | @Override public void getUsersEntityList(UserListCallback userListCallback) { 32 | //TODO: implement simple cache for storing/retrieving collections of users. 33 | throw new UnsupportedOperationException("Operation is not available!!!"); 34 | } 35 | 36 | /** 37 | * {@inheritDoc} 38 | * 39 | * @param id The id to retrieve user data. 40 | * @param userDetailsCallback A {@link UserDataStore.UserDetailsCallback} to notify the client. 41 | */ 42 | @Override public void getUserEntityDetails(int id, 43 | final UserDetailsCallback userDetailsCallback) { 44 | this.userCache.get(id, new UserCache.UserCacheCallback() { 45 | @Override public void onUserEntityLoaded(UserEntity userEntity) { 46 | userDetailsCallback.onUserEntityLoaded(userEntity); 47 | } 48 | 49 | @Override public void onError(Exception exception) { 50 | userDetailsCallback.onError(exception); 51 | } 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Data/src/main/java/com/fernandocejas/android10/sample/data/repository/datasource/UserDataStore.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.data.repository.datasource; 6 | 7 | import com.fernandocejas.android10.sample.data.entity.UserEntity; 8 | import java.util.Collection; 9 | 10 | /** 11 | * Interface that represents a data store from where data is retrieved. 12 | */ 13 | public interface UserDataStore { 14 | /** 15 | * Callback used for clients to be notified when either a user list has been loaded or any error 16 | * occurred. 17 | */ 18 | interface UserListCallback { 19 | void onUserListLoaded(Collection usersCollection); 20 | 21 | void onError(Exception exception); 22 | } 23 | 24 | /** 25 | * Callback used for clients to be notified when either user data has been retrieved successfully 26 | * or any error occurred. 27 | */ 28 | interface UserDetailsCallback { 29 | void onUserEntityLoaded(UserEntity userEntity); 30 | 31 | void onError(Exception exception); 32 | } 33 | 34 | /** 35 | * Get a collection of {@link com.fernandocejas.android10.sample.domain.User}. 36 | * 37 | * @param userListCallback A {@link UserListCallback} used for notifying clients. 38 | */ 39 | void getUsersEntityList(UserListCallback userListCallback); 40 | 41 | /** 42 | * Get a {@link UserEntity} by its id. 43 | * 44 | * @param id The id to retrieve user data. 45 | * @param userDetailsCallback A {@link UserDetailsCallback} for notifications. 46 | */ 47 | void getUserEntityDetails(int id, UserDetailsCallback userDetailsCallback); 48 | } 49 | -------------------------------------------------------------------------------- /Data/src/main/java/com/fernandocejas/android10/sample/data/repository/datasource/UserDataStoreFactory.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.data.repository.datasource; 6 | 7 | import android.content.Context; 8 | import com.fernandocejas.android10.sample.data.cache.UserCache; 9 | import com.fernandocejas.android10.sample.data.entity.mapper.UserEntityJsonMapper; 10 | import com.fernandocejas.android10.sample.data.net.RestApi; 11 | import com.fernandocejas.android10.sample.data.net.RestApiImpl; 12 | 13 | /** 14 | * Factory that creates different implementations of {@link UserDataStore}. 15 | */ 16 | public class UserDataStoreFactory { 17 | 18 | private final Context context; 19 | private final UserCache userCache; 20 | 21 | public UserDataStoreFactory(Context context, UserCache userCache) { 22 | if (context == null || userCache == null) { 23 | throw new IllegalArgumentException("Constructor parameters cannot be null!!!"); 24 | } 25 | this.context = context.getApplicationContext(); 26 | this.userCache = userCache; 27 | } 28 | 29 | /** 30 | * Create {@link UserDataStore} from a user id. 31 | */ 32 | public UserDataStore create(int userId) { 33 | UserDataStore userDataStore; 34 | 35 | if (!this.userCache.isExpired() && this.userCache.isCached(userId)) { 36 | userDataStore = new DiskUserDataStore(this.userCache); 37 | } else { 38 | userDataStore = createCloudDataStore(); 39 | } 40 | 41 | return userDataStore; 42 | } 43 | 44 | /** 45 | * Create {@link UserDataStore} to retrieve data from the Cloud. 46 | */ 47 | public UserDataStore createCloudDataStore() { 48 | UserEntityJsonMapper userEntityJsonMapper = new UserEntityJsonMapper(); 49 | RestApi restApi = new RestApiImpl(this.context, userEntityJsonMapper); 50 | 51 | return new CloudUserDataStore(restApi, this.userCache); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Data/src/main/libs/dexmaker-1.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxy1228/MVP/ed2b4ee61abb5b2df57dc71e8d55e4349f9cfb5a/Data/src/main/libs/dexmaker-1.0.jar -------------------------------------------------------------------------------- /Data/src/main/libs/dexmaker-mockito-1.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxy1228/MVP/ed2b4ee61abb5b2df57dc71e8d55e4349f9cfb5a/Data/src/main/libs/dexmaker-mockito-1.0.jar -------------------------------------------------------------------------------- /Data/src/main/libs/espresso-1.1-bundled.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxy1228/MVP/ed2b4ee61abb5b2df57dc71e8d55e4349f9cfb5a/Data/src/main/libs/espresso-1.1-bundled.jar -------------------------------------------------------------------------------- /Data/src/main/libs/gson-2.2.4.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxy1228/MVP/ed2b4ee61abb5b2df57dc71e8d55e4349f9cfb5a/Data/src/main/libs/gson-2.2.4.jar -------------------------------------------------------------------------------- /Data/src/main/libs/mockito-all-1.9.5.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxy1228/MVP/ed2b4ee61abb5b2df57dc71e8d55e4349f9cfb5a/Data/src/main/libs/mockito-all-1.9.5.jar -------------------------------------------------------------------------------- /Data/src/main/project.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system edit 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | # 10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): 11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt 12 | 13 | # Project target. 14 | target=android-10 15 | android.library=true 16 | android.library.reference.1=../../../Domain 17 | -------------------------------------------------------------------------------- /Domain/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Domain/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | Domain 4 | 5 | 6 | 7 | 8 | 9 | com.android.ide.eclipse.adt.ResourceManagerBuilder 10 | 11 | 12 | 13 | 14 | com.android.ide.eclipse.adt.PreCompilerBuilder 15 | 16 | 17 | 18 | 19 | org.eclipse.jdt.core.javabuilder 20 | 21 | 22 | 23 | 24 | com.android.ide.eclipse.adt.ApkBuilder 25 | 26 | 27 | 28 | 29 | 30 | com.android.ide.eclipse.adt.AndroidNature 31 | org.eclipse.jdt.core.javanature 32 | 33 | 34 | -------------------------------------------------------------------------------- /Domain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 9 | 10 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Domain/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxy1228/MVP/ed2b4ee61abb5b2df57dc71e8d55e4349f9cfb5a/Domain/ic_launcher-web.png -------------------------------------------------------------------------------- /Domain/libs/android-support-v4.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxy1228/MVP/ed2b4ee61abb5b2df57dc71e8d55e4349f9cfb5a/Domain/libs/android-support-v4.jar -------------------------------------------------------------------------------- /Domain/proguard-project.txt: -------------------------------------------------------------------------------- 1 | # To enable ProGuard in your project, edit project.properties 2 | # to define the proguard.config property as described in that file. 3 | # 4 | # Add project specific ProGuard rules here. 5 | # By default, the flags in this file are appended to flags specified 6 | # in ${sdk.dir}/tools/proguard/proguard-android.txt 7 | # You can edit the include path and order by changing the ProGuard 8 | # include property in project.properties. 9 | # 10 | # For more details, see 11 | # http://developer.android.com/guide/developing/tools/proguard.html 12 | 13 | # Add any project specific keep options here: 14 | 15 | # If your project uses WebView with JS, uncomment the following 16 | # and specify the fully qualified class name to the JavaScript interface 17 | # class: 18 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 19 | # public *; 20 | #} 21 | -------------------------------------------------------------------------------- /Domain/project.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system edit 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | # 10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): 11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt 12 | 13 | # Project target. 14 | target=android-20 15 | android.library=true 16 | -------------------------------------------------------------------------------- /Domain/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxy1228/MVP/ed2b4ee61abb5b2df57dc71e8d55e4349f9cfb5a/Domain/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /Domain/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxy1228/MVP/ed2b4ee61abb5b2df57dc71e8d55e4349f9cfb5a/Domain/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /Domain/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxy1228/MVP/ed2b4ee61abb5b2df57dc71e8d55e4349f9cfb5a/Domain/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /Domain/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxy1228/MVP/ed2b4ee61abb5b2df57dc71e8d55e4349f9cfb5a/Domain/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /Domain/res/values-v11/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Domain/res/values-v14/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Domain/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Domain 4 | 5 | 6 | -------------------------------------------------------------------------------- /Domain/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 14 | 15 | 16 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Domain/src/com/fernandocejas/android10/sample/domain/User.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.domain; 6 | 7 | /** 8 | * Class that represents a User in the domain layer. 9 | */ 10 | public class User { 11 | 12 | private final int userId; 13 | 14 | public User(int userId) { 15 | this.userId = userId; 16 | } 17 | 18 | private String coverUrl; 19 | private String fullName; 20 | private String email; 21 | private String description; 22 | private int followers; 23 | 24 | public int getUserId() { 25 | return userId; 26 | } 27 | 28 | public String getCoverUrl() { 29 | return coverUrl; 30 | } 31 | 32 | public void setCoverUrl(String coverUrl) { 33 | this.coverUrl = coverUrl; 34 | } 35 | 36 | public String getFullName() { 37 | return fullName; 38 | } 39 | 40 | public void setFullName(String fullName) { 41 | this.fullName = fullName; 42 | } 43 | 44 | public String getEmail() { 45 | return email; 46 | } 47 | 48 | public void setEmail(String email) { 49 | this.email = email; 50 | } 51 | 52 | public String getDescription() { 53 | return description; 54 | } 55 | 56 | public void setDescription(String description) { 57 | this.description = description; 58 | } 59 | 60 | public int getFollowers() { 61 | return followers; 62 | } 63 | 64 | public void setFollowers(int followers) { 65 | this.followers = followers; 66 | } 67 | 68 | @Override public String toString() { 69 | StringBuilder stringBuilder = new StringBuilder(); 70 | 71 | stringBuilder.append("***** User Details *****\n"); 72 | stringBuilder.append("id=" + this.getUserId() + "\n"); 73 | stringBuilder.append("cover url=" + this.getCoverUrl() + "\n"); 74 | stringBuilder.append("fullname=" + this.getFullName() + "\n"); 75 | stringBuilder.append("email=" + this.getEmail() + "\n"); 76 | stringBuilder.append("description=" + this.getDescription() + "\n"); 77 | stringBuilder.append("followers=" + this.getFollowers() + "\n"); 78 | stringBuilder.append("*******************************"); 79 | 80 | return stringBuilder.toString(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Domain/src/com/fernandocejas/android10/sample/domain/exception/ErrorBundle.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.domain.exception; 6 | 7 | /** 8 | * Interface to represent a wrapper around an {@link java.lang.Exception} to manage errors. 9 | */ 10 | public interface ErrorBundle { 11 | Exception getException(); 12 | 13 | String getErrorMessage(); 14 | } 15 | -------------------------------------------------------------------------------- /Domain/src/com/fernandocejas/android10/sample/domain/executor/PostExecutionThread.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.domain.executor; 6 | 7 | /** 8 | * Thread abstraction created to change the execution context from any thread to any other thread. 9 | * Useful to encapsulate a UI Thread for example, since some job will be done in background, an 10 | * implementation of this interface will change context and update the UI. 11 | */ 12 | public interface PostExecutionThread { 13 | /** 14 | * Causes the {@link Runnable} to be added to the message queue of the Main UI Thread 15 | * of the application. 16 | * 17 | * @param runnable {@link Runnable} to be executed. 18 | */ 19 | void post(Runnable runnable); 20 | } 21 | -------------------------------------------------------------------------------- /Domain/src/com/fernandocejas/android10/sample/domain/executor/ThreadExecutor.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.domain.executor; 6 | 7 | import com.fernandocejas.android10.sample.domain.interactor.Interactor; 8 | 9 | /** 10 | * Executor implementation can be based on different frameworks or techniques of asynchronous 11 | * execution, but every implementation will execute the {@link Interactor} out of the UI thread. 12 | * 13 | * Use this class to execute an {@link Interactor}. 14 | */ 15 | public interface ThreadExecutor { 16 | /** 17 | * Executes a {@link Runnable}. 18 | * 19 | * @param runnable The class that implements {@link Runnable} interface. 20 | */ 21 | void execute(final Runnable runnable); 22 | } 23 | -------------------------------------------------------------------------------- /Domain/src/com/fernandocejas/android10/sample/domain/interactor/GetUserDetailsUseCase.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.domain.interactor; 6 | 7 | import com.fernandocejas.android10.sample.domain.User; 8 | import com.fernandocejas.android10.sample.domain.exception.ErrorBundle; 9 | 10 | /** 11 | * This interface represents a execution unit for a use case to get data for an specific user. 12 | * By convention this use case ({@link Interactor}) implementation will return the result using a 13 | * callback that should be executed in the UI thread. 14 | */ 15 | public interface GetUserDetailsUseCase extends Interactor { 16 | /** 17 | * Callback used to be notified when either a user has been loaded or an error happened. 18 | */ 19 | interface Callback { 20 | void onUserDataLoaded(User user); 21 | void onError(ErrorBundle errorBundle); 22 | } 23 | 24 | /** 25 | * Executes this user case. 26 | * 27 | * @param userId The user id to retrieve. 28 | * @param callback A {@link GetUserDetailsUseCase.Callback} used for notify the client. 29 | */ 30 | public void execute(int userId, Callback callback); 31 | } 32 | -------------------------------------------------------------------------------- /Domain/src/com/fernandocejas/android10/sample/domain/interactor/GetUserDetailsUseCaseImpl.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.domain.interactor; 6 | 7 | import com.fernandocejas.android10.sample.domain.User; 8 | import com.fernandocejas.android10.sample.domain.exception.ErrorBundle; 9 | import com.fernandocejas.android10.sample.domain.executor.PostExecutionThread; 10 | import com.fernandocejas.android10.sample.domain.executor.ThreadExecutor; 11 | import com.fernandocejas.android10.sample.domain.repository.UserRepository; 12 | 13 | /** 14 | * This class is an implementation of {@link GetUserDetailsUseCase} that represents a use case for 15 | * retrieving data related to an specific {@link User}. 16 | */ 17 | public class GetUserDetailsUseCaseImpl implements GetUserDetailsUseCase { 18 | 19 | private final UserRepository userRepository; 20 | private final ThreadExecutor threadExecutor; 21 | private final PostExecutionThread postExecutionThread; 22 | 23 | private int userId = -1; 24 | private GetUserDetailsUseCase.Callback callback; 25 | 26 | /** 27 | * Constructor of the class. 28 | * 29 | * @param userRepository A {@link UserRepository} as a source for retrieving data. 30 | * @param threadExecutor {@link ThreadExecutor} used to execute this use case in a background 31 | * thread. 32 | * @param postExecutionThread {@link PostExecutionThread} used to post updates when the use case 33 | * has been executed. 34 | */ 35 | public GetUserDetailsUseCaseImpl(UserRepository userRepository, ThreadExecutor threadExecutor, 36 | PostExecutionThread postExecutionThread) { 37 | if (userRepository == null || threadExecutor == null || postExecutionThread == null) { 38 | throw new IllegalArgumentException("Constructor parameters cannot be null!!!"); 39 | } 40 | this.userRepository = userRepository; 41 | this.threadExecutor = threadExecutor; 42 | this.postExecutionThread = postExecutionThread; 43 | } 44 | 45 | @Override public void execute(int userId, Callback callback) { 46 | if (userId < 0 || callback == null) { 47 | throw new IllegalArgumentException("Invalid parameter!!!"); 48 | } 49 | this.userId = userId; 50 | this.callback = callback; 51 | this.threadExecutor.execute(this); 52 | } 53 | 54 | @Override public void run() { 55 | this.userRepository.getUserById(this.userId, this.repositoryCallback); 56 | } 57 | 58 | private final UserRepository.UserDetailsCallback repositoryCallback = 59 | new UserRepository.UserDetailsCallback() { 60 | @Override public void onUserLoaded(User user) { 61 | notifyGetUserDetailsSuccessfully(user); 62 | } 63 | 64 | @Override public void onError(ErrorBundle errorBundle) { 65 | notifyError(errorBundle); 66 | } 67 | }; 68 | 69 | private void notifyGetUserDetailsSuccessfully(final User user) { 70 | this.postExecutionThread.post(new Runnable() { 71 | @Override public void run() { 72 | callback.onUserDataLoaded(user); 73 | } 74 | }); 75 | } 76 | 77 | private void notifyError(final ErrorBundle errorBundle) { 78 | this.postExecutionThread.post(new Runnable() { 79 | @Override public void run() { 80 | callback.onError(errorBundle); 81 | } 82 | }); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Domain/src/com/fernandocejas/android10/sample/domain/interactor/GetUserListUseCase.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.domain.interactor; 6 | 7 | import com.fernandocejas.android10.sample.domain.User; 8 | import com.fernandocejas.android10.sample.domain.exception.ErrorBundle; 9 | import java.util.Collection; 10 | 11 | /** 12 | * This interface represents a execution unit for a use case to get a collection of {@link User}. 13 | * By convention this use case (Interactor) implementation will return the result using a Callback. 14 | * That callback should be executed in the UI thread. 15 | */ 16 | public interface GetUserListUseCase extends Interactor { 17 | /** 18 | * Callback used to be notified when either a users collection has been loaded or an error 19 | * happened. 20 | */ 21 | interface Callback { 22 | void onUserListLoaded(Collection usersCollection); 23 | void onError(ErrorBundle errorBundle); 24 | } 25 | 26 | /** 27 | * Executes this user case. 28 | * 29 | * @param callback A {@link GetUserListUseCase.Callback} used to notify the client. 30 | */ 31 | void execute(Callback callback); 32 | } 33 | -------------------------------------------------------------------------------- /Domain/src/com/fernandocejas/android10/sample/domain/interactor/GetUserListUseCaseImpl.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.domain.interactor; 6 | 7 | import com.fernandocejas.android10.sample.domain.User; 8 | import com.fernandocejas.android10.sample.domain.exception.ErrorBundle; 9 | import com.fernandocejas.android10.sample.domain.executor.PostExecutionThread; 10 | import com.fernandocejas.android10.sample.domain.executor.ThreadExecutor; 11 | import com.fernandocejas.android10.sample.domain.repository.UserRepository; 12 | import java.util.Collection; 13 | 14 | /** 15 | * This class is an implementation of {@link GetUserListUseCase} that represents a use case for 16 | * retrieving a collection of all {@link User}. 17 | */ 18 | public class GetUserListUseCaseImpl implements GetUserListUseCase { 19 | 20 | private final UserRepository userRepository; 21 | private final ThreadExecutor threadExecutor; 22 | private final PostExecutionThread postExecutionThread; 23 | 24 | private Callback callback; 25 | 26 | /** 27 | * Constructor of the class. 28 | * 29 | * @param userRepository A {@link UserRepository} as a source for retrieving data. 30 | * @param threadExecutor {@link ThreadExecutor} used to execute this use case in a background 31 | * thread. 32 | * @param postExecutionThread {@link PostExecutionThread} used to post updates when the use case 33 | * has been executed. 34 | */ 35 | public GetUserListUseCaseImpl(UserRepository userRepository, ThreadExecutor threadExecutor, 36 | PostExecutionThread postExecutionThread) { 37 | if (userRepository == null || threadExecutor == null || postExecutionThread == null) { 38 | throw new IllegalArgumentException("Constructor parameters cannot be null!!!"); 39 | } 40 | this.userRepository = userRepository; 41 | this.threadExecutor = threadExecutor; 42 | this.postExecutionThread = postExecutionThread; 43 | } 44 | 45 | @Override public void execute(Callback callback) { 46 | if (callback == null) { 47 | throw new IllegalArgumentException("Interactor callback cannot be null!!!"); 48 | } 49 | this.callback = callback; 50 | this.threadExecutor.execute(this); 51 | } 52 | 53 | @Override public void run() { 54 | this.userRepository.getUserList(this.repositoryCallback); 55 | } 56 | 57 | private final UserRepository.UserListCallback repositoryCallback = 58 | new UserRepository.UserListCallback() { 59 | @Override public void onUserListLoaded(Collection usersCollection) { 60 | notifyGetUserListSuccessfully(usersCollection); 61 | } 62 | 63 | @Override public void onError(ErrorBundle errorBundle) { 64 | notifyError(errorBundle); 65 | } 66 | }; 67 | 68 | private void notifyGetUserListSuccessfully(final Collection usersCollection) { 69 | this.postExecutionThread.post(new Runnable() { 70 | @Override public void run() { 71 | callback.onUserListLoaded(usersCollection); 72 | } 73 | }); 74 | } 75 | 76 | private void notifyError(final ErrorBundle errorBundle) { 77 | this.postExecutionThread.post(new Runnable() { 78 | @Override public void run() { 79 | callback.onError(errorBundle); 80 | } 81 | }); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Domain/src/com/fernandocejas/android10/sample/domain/interactor/Interactor.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.domain.interactor; 6 | 7 | /** 8 | * Common interface for an Interactor {@link java.lang.Runnable} declared in the application. 9 | * This interface represents a execution unit for different use cases (this means any use case 10 | * in the application should implement this contract). 11 | * 12 | * By convention each Interactor implementation will return the result using a Callback that should 13 | * be executed in the UI thread. 14 | */ 15 | public interface Interactor extends Runnable { 16 | /** 17 | * Everything inside this method will be executed asynchronously. 18 | */ 19 | void run(); 20 | } 21 | -------------------------------------------------------------------------------- /Domain/src/com/fernandocejas/android10/sample/domain/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.domain.repository; 6 | 7 | import com.fernandocejas.android10.sample.domain.User; 8 | import com.fernandocejas.android10.sample.domain.exception.ErrorBundle; 9 | import java.util.Collection; 10 | 11 | /** 12 | * Interface that represents a Repository for getting {@link User} related data. 13 | */ 14 | public interface UserRepository { 15 | /** 16 | * Callback used to be notified when either a user list has been loaded or an error happened. 17 | */ 18 | interface UserListCallback { 19 | void onUserListLoaded(Collection usersCollection); 20 | 21 | void onError(ErrorBundle errorBundle); 22 | } 23 | 24 | /** 25 | * Callback used to be notified when either a user has been loaded or an error happened. 26 | */ 27 | interface UserDetailsCallback { 28 | void onUserLoaded(User user); 29 | 30 | void onError(ErrorBundle errorBundle); 31 | } 32 | 33 | /** 34 | * Get a collection of {@link User}. 35 | * 36 | * @param userListCallback A {@link UserListCallback} used for notifying clients. 37 | */ 38 | void getUserList(UserListCallback userListCallback); 39 | 40 | /** 41 | * Get an {@link User} by id. 42 | * 43 | * @param userId The user id used to retrieve user data. 44 | * @param userCallback A {@link UserDetailsCallback} used for notifying clients. 45 | */ 46 | void getUserById(final int userId, UserDetailsCallback userCallback); 47 | } 48 | -------------------------------------------------------------------------------- /Presentation/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /Presentation/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'android' 2 | 3 | android { 4 | compileSdkVersion 19 5 | buildToolsVersion '19.1.0' 6 | 7 | defaultConfig { 8 | applicationId "com.fernandocejas.android10.sample.presentation" 9 | minSdkVersion 15 10 | targetSdkVersion 19 11 | versionCode 1 12 | versionName "1.0" 13 | testInstrumentationRunner "com.google.android.apps.common.testing.testrunner.GoogleInstrumentationTestRunner" 14 | } 15 | 16 | packagingOptions { 17 | exclude 'LICENSE.txt' 18 | exclude 'META-INF/DEPENDENCIES' 19 | exclude 'META-INF/ASL2.0' 20 | exclude 'META-INF/NOTICE' 21 | exclude 'META-INF/LICENSE' 22 | } 23 | 24 | lintOptions { 25 | abortOnError false; 26 | disable 'InvalidPackage' // Some libraries have issues with this. 27 | disable 'OldTargetApi' // Lint gives this warning but SDK 20 would be Android L Beta. 28 | disable 'IconDensities' // For testing purpose. This is safe to remove. 29 | } 30 | 31 | buildTypes { 32 | release { 33 | runProguard false 34 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 35 | } 36 | } 37 | } 38 | 39 | dependencies { 40 | def domainLayer = project(':domain') 41 | def dataLayer = project(':data') 42 | 43 | //project dependencies 44 | compile domainLayer 45 | compile dataLayer 46 | 47 | //compile this only for testing. I had to use a workaround for using Espresso (it is not in the 48 | //maven central repository): since both Mockito and Espresso use 'hamcrest' I had to remove them 49 | //on the mockito library: "zip -d mockito.jar org/hamcrest/*" 50 | androidTestCompile fileTree(dir: 'testLibs', include: '*.jar') 51 | } 52 | -------------------------------------------------------------------------------- /Presentation/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 /Users/fcejas/Software/SDKs/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 | -------------------------------------------------------------------------------- /Presentation/src/androidTest/java/com/fernandocejas/android10/sample/test/exception/ErrorMessageFactoryTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.test.exception; 6 | 7 | import android.test.AndroidTestCase; 8 | import com.fernandocejas.android10.sample.data.exception.NetworkConnectionException; 9 | import com.fernandocejas.android10.sample.data.exception.UserNotFoundException; 10 | import com.fernandocejas.android10.sample.presentation.R; 11 | import com.fernandocejas.android10.sample.presentation.exception.ErrorMessageFactory; 12 | 13 | import static org.hamcrest.CoreMatchers.equalTo; 14 | import static org.hamcrest.CoreMatchers.is; 15 | import static org.hamcrest.MatcherAssert.assertThat; 16 | 17 | public class ErrorMessageFactoryTest extends AndroidTestCase { 18 | 19 | @Override protected void setUp() throws Exception { 20 | super.setUp(); 21 | } 22 | 23 | public void testNetworkConnectionErrorMessage() { 24 | String expectedMessage = getContext().getString(R.string.exception_message_no_connection); 25 | String actualMessage = ErrorMessageFactory.create(getContext(), 26 | new NetworkConnectionException()); 27 | 28 | assertThat(actualMessage, is(equalTo(expectedMessage))); 29 | } 30 | 31 | public void testUserNotFoundErrorMessage() { 32 | String expectedMessage = getContext().getString(R.string.exception_message_user_not_found); 33 | String actualMessage = ErrorMessageFactory.create(getContext(), new UserNotFoundException()); 34 | 35 | assertThat(actualMessage, is(equalTo(expectedMessage))); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Presentation/src/androidTest/java/com/fernandocejas/android10/sample/test/mapper/UserModelDataMapperTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.test.mapper; 6 | 7 | import com.fernandocejas.android10.sample.domain.User; 8 | import com.fernandocejas.android10.sample.presentation.mapper.UserModelDataMapper; 9 | import com.fernandocejas.android10.sample.presentation.model.UserModel; 10 | import java.util.ArrayList; 11 | import java.util.Collection; 12 | import java.util.List; 13 | import junit.framework.TestCase; 14 | 15 | import static org.hamcrest.CoreMatchers.instanceOf; 16 | import static org.hamcrest.CoreMatchers.is; 17 | import static org.hamcrest.MatcherAssert.assertThat; 18 | import static org.mockito.Mockito.mock; 19 | 20 | public class UserModelDataMapperTest extends TestCase { 21 | 22 | private static final int FAKE_USER_ID = 123; 23 | private static final String FAKE_FULLNAME = "Tony Stark"; 24 | 25 | private UserModelDataMapper userModelDataMapper; 26 | 27 | @Override protected void setUp() throws Exception { 28 | super.setUp(); 29 | userModelDataMapper = new UserModelDataMapper(); 30 | } 31 | 32 | public void testTransformUser() { 33 | User user = createFakeUser(); 34 | UserModel userModel = userModelDataMapper.transform(user); 35 | 36 | assertThat(userModel, is(instanceOf(UserModel.class))); 37 | assertThat(userModel.getUserId(), is(FAKE_USER_ID)); 38 | assertThat(userModel.getFullName(), is(FAKE_FULLNAME)); 39 | } 40 | 41 | public void testTransformUserCollection() { 42 | User mockUserOne = mock(User.class); 43 | User mockUserTwo = mock(User.class); 44 | 45 | List userList = new ArrayList(5); 46 | userList.add(mockUserOne); 47 | userList.add(mockUserTwo); 48 | 49 | Collection userModelList = userModelDataMapper.transform(userList); 50 | 51 | assertThat(userModelList.toArray()[0], is(instanceOf(UserModel.class))); 52 | assertThat(userModelList.toArray()[1], is(instanceOf(UserModel.class))); 53 | assertThat(userModelList.size(), is(2)); 54 | } 55 | 56 | private User createFakeUser() { 57 | User user = new User(FAKE_USER_ID); 58 | user.setFullName(FAKE_FULLNAME); 59 | 60 | return user; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Presentation/src/androidTest/java/com/fernandocejas/android10/sample/test/presenter/UserDetailsPresenterTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.test.presenter; 6 | 7 | import android.content.Context; 8 | import android.test.AndroidTestCase; 9 | import com.fernandocejas.android10.sample.domain.interactor.GetUserDetailsUseCase; 10 | import com.fernandocejas.android10.sample.presentation.mapper.UserModelDataMapper; 11 | import com.fernandocejas.android10.sample.presentation.presenter.UserDetailsPresenter; 12 | import com.fernandocejas.android10.sample.presentation.view.UserDetailsView; 13 | import org.mockito.Mock; 14 | import org.mockito.MockitoAnnotations; 15 | 16 | import static org.mockito.BDDMockito.given; 17 | import static org.mockito.Matchers.any; 18 | import static org.mockito.Matchers.anyInt; 19 | import static org.mockito.Mockito.doNothing; 20 | import static org.mockito.Mockito.verify; 21 | 22 | public class UserDetailsPresenterTest extends AndroidTestCase { 23 | 24 | private static final int FAKE_USER_ID = 123; 25 | 26 | private UserDetailsPresenter userDetailsPresenter; 27 | 28 | @Mock 29 | private Context mockContext; 30 | @Mock 31 | private UserDetailsView mockUserDetailsView; 32 | @Mock 33 | private GetUserDetailsUseCase mockGetUserDetailsUseCase; 34 | @Mock 35 | private UserModelDataMapper mockUserModelDataMapper; 36 | 37 | @Override protected void setUp() throws Exception { 38 | super.setUp(); 39 | MockitoAnnotations.initMocks(this); 40 | userDetailsPresenter = new UserDetailsPresenter(mockUserDetailsView, mockGetUserDetailsUseCase, 41 | mockUserModelDataMapper); 42 | } 43 | 44 | public void testUserDetailsPresenterInitialize() { 45 | doNothing().when(mockGetUserDetailsUseCase) 46 | .execute(anyInt(), any(GetUserDetailsUseCase.Callback.class)); 47 | given(mockUserDetailsView.getContext()).willReturn(mockContext); 48 | 49 | userDetailsPresenter.initialize(FAKE_USER_ID); 50 | 51 | verify(mockUserDetailsView).hideRetry(); 52 | verify(mockUserDetailsView).showLoading(); 53 | verify(mockGetUserDetailsUseCase).execute(anyInt(), any(GetUserDetailsUseCase.Callback.class)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Presentation/src/androidTest/java/com/fernandocejas/android10/sample/test/presenter/UserListPresenterTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.test.presenter; 6 | 7 | import android.content.Context; 8 | import android.test.AndroidTestCase; 9 | import com.fernandocejas.android10.sample.domain.interactor.GetUserListUseCase; 10 | import com.fernandocejas.android10.sample.presentation.mapper.UserModelDataMapper; 11 | import com.fernandocejas.android10.sample.presentation.presenter.UserListPresenter; 12 | import com.fernandocejas.android10.sample.presentation.view.UserListView; 13 | import org.mockito.Mock; 14 | import org.mockito.MockitoAnnotations; 15 | 16 | import static org.mockito.BDDMockito.given; 17 | import static org.mockito.Matchers.any; 18 | import static org.mockito.Mockito.doNothing; 19 | import static org.mockito.Mockito.verify; 20 | 21 | public class UserListPresenterTest extends AndroidTestCase { 22 | 23 | private UserListPresenter userListPresenter; 24 | 25 | @Mock 26 | private Context mockContext; 27 | @Mock 28 | private UserListView mockUserListView; 29 | @Mock 30 | private GetUserListUseCase mockGetUserListUseCase; 31 | @Mock 32 | private UserModelDataMapper mockUserModelDataMapper; 33 | 34 | @Override protected void setUp() throws Exception { 35 | super.setUp(); 36 | MockitoAnnotations.initMocks(this); 37 | userListPresenter = new UserListPresenter(mockUserListView, mockGetUserListUseCase, 38 | mockUserModelDataMapper); 39 | } 40 | 41 | public void testUserListPresenterInitialize() { 42 | doNothing().when(mockGetUserListUseCase).execute(any(GetUserListUseCase.Callback.class)); 43 | given(mockUserListView.getContext()).willReturn(mockContext); 44 | 45 | userListPresenter.initialize(); 46 | 47 | verify(mockUserListView).hideRetry(); 48 | verify(mockUserListView).showLoading(); 49 | verify(mockGetUserListUseCase).execute(any(GetUserListUseCase.Callback.class)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Presentation/src/androidTest/java/com/fernandocejas/android10/sample/test/view/activity/UserDetailsActivityTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.test.view.activity; 6 | 7 | import android.app.Fragment; 8 | import android.content.Intent; 9 | import android.test.ActivityInstrumentationTestCase2; 10 | import com.fernandocejas.android10.sample.presentation.R; 11 | import com.fernandocejas.android10.sample.presentation.view.activity.UserDetailsActivity; 12 | 13 | import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView; 14 | import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches; 15 | import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDisplayed; 16 | import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId; 17 | import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText; 18 | import static org.hamcrest.CoreMatchers.is; 19 | import static org.hamcrest.CoreMatchers.notNullValue; 20 | import static org.hamcrest.MatcherAssert.assertThat; 21 | import static org.hamcrest.Matchers.not; 22 | 23 | public class UserDetailsActivityTest extends ActivityInstrumentationTestCase2 { 24 | 25 | private static final int FAKE_USER_ID = 10; 26 | 27 | private UserDetailsActivity userDetailsActivity; 28 | 29 | public UserDetailsActivityTest() { 30 | super(UserDetailsActivity.class); 31 | } 32 | 33 | @Override protected void setUp() throws Exception { 34 | super.setUp(); 35 | this.setActivityIntent(createTargetIntent()); 36 | this.userDetailsActivity = getActivity(); 37 | } 38 | 39 | @Override protected void tearDown() throws Exception { 40 | super.tearDown(); 41 | } 42 | 43 | public void testContainsUserDetailsFragment() { 44 | Fragment userDetailsFragment = 45 | userDetailsActivity.getFragmentManager().findFragmentById(R.id.fl_fragment); 46 | assertThat(userDetailsFragment, is(notNullValue())); 47 | } 48 | 49 | public void testContainsProperTitle() { 50 | String actualTitle = this.userDetailsActivity.getTitle().toString().trim(); 51 | 52 | assertThat(actualTitle, is("User Details")); 53 | } 54 | 55 | public void testLoadUserHappyCaseViews() { 56 | onView(withId(R.id.rl_retry)).check(matches(not(isDisplayed()))); 57 | onView(withId(R.id.rl_progress)).check(matches(not(isDisplayed()))); 58 | 59 | onView(withId(R.id.tv_fullname)).check(matches(isDisplayed())); 60 | onView(withId(R.id.tv_email)).check(matches(isDisplayed())); 61 | onView(withId(R.id.tv_description)).check(matches(isDisplayed())); 62 | } 63 | 64 | public void testLoadUserHappyCaseData() { 65 | onView(withId(R.id.tv_fullname)).check(matches(withText("John Sanchez"))); 66 | onView(withId(R.id.tv_email)).check(matches(withText("dmedina@katz.edu"))); 67 | onView(withId(R.id.tv_followers)).check(matches(withText("4523"))); 68 | } 69 | 70 | private Intent createTargetIntent() { 71 | Intent intentLaunchActivity = 72 | UserDetailsActivity.getCallingIntent(getInstrumentation().getTargetContext(), FAKE_USER_ID); 73 | 74 | return intentLaunchActivity; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Presentation/src/androidTest/java/com/fernandocejas/android10/sample/test/view/activity/UserListActivityTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.test.view.activity; 6 | 7 | import android.app.Fragment; 8 | import android.content.Intent; 9 | import android.test.ActivityInstrumentationTestCase2; 10 | import com.fernandocejas.android10.sample.presentation.R; 11 | import com.fernandocejas.android10.sample.presentation.view.activity.UserListActivity; 12 | 13 | import static org.hamcrest.CoreMatchers.is; 14 | import static org.hamcrest.CoreMatchers.notNullValue; 15 | import static org.hamcrest.MatcherAssert.assertThat; 16 | 17 | public class UserListActivityTest extends ActivityInstrumentationTestCase2 { 18 | 19 | private UserListActivity userListActivity; 20 | 21 | public UserListActivityTest() { 22 | super(UserListActivity.class); 23 | } 24 | 25 | @Override protected void setUp() throws Exception { 26 | super.setUp(); 27 | this.setActivityIntent(createTargetIntent()); 28 | userListActivity = getActivity(); 29 | } 30 | 31 | @Override protected void tearDown() throws Exception { 32 | super.tearDown(); 33 | } 34 | 35 | public void testContainsUserListFragment() { 36 | Fragment userListFragment = 37 | userListActivity.getFragmentManager().findFragmentById(R.id.fragmentUserList); 38 | assertThat(userListFragment, is(notNullValue())); 39 | } 40 | 41 | public void testContainsProperTitle() { 42 | String actualTitle = this.userListActivity.getTitle().toString().trim(); 43 | 44 | assertThat(actualTitle, is("Users List")); 45 | } 46 | 47 | private Intent createTargetIntent() { 48 | Intent intentLaunchActivity = 49 | UserListActivity.getCallingIntent(getInstrumentation().getTargetContext()); 50 | 51 | return intentLaunchActivity; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Presentation/src/main/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Presentation/src/main/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | Presentation 4 | 5 | 6 | 7 | 8 | 9 | com.android.ide.eclipse.adt.ResourceManagerBuilder 10 | 11 | 12 | 13 | 14 | com.android.ide.eclipse.adt.PreCompilerBuilder 15 | 16 | 17 | 18 | 19 | org.eclipse.jdt.core.javabuilder 20 | 21 | 22 | 23 | 24 | com.android.ide.eclipse.adt.ApkBuilder 25 | 26 | 27 | 28 | 29 | 30 | com.android.ide.eclipse.adt.AndroidNature 31 | org.eclipse.jdt.core.javanature 32 | 33 | 34 | -------------------------------------------------------------------------------- /Presentation/src/main/.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 3 | org.eclipse.jdt.core.compiler.compliance=1.6 4 | org.eclipse.jdt.core.compiler.source=1.6 5 | -------------------------------------------------------------------------------- /Presentation/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /Presentation/src/main/java/com/fernandocejas/android10/sample/presentation/UIThread.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.presentation; 6 | 7 | import android.os.Handler; 8 | import android.os.Looper; 9 | import com.fernandocejas.android10.sample.domain.executor.PostExecutionThread; 10 | 11 | /** 12 | * MainThread (UI Thread) implementation based on a Handler instantiated with the main 13 | * application Looper. 14 | */ 15 | public class UIThread implements PostExecutionThread { 16 | 17 | private static class LazyHolder { 18 | private static final UIThread INSTANCE = new UIThread(); 19 | } 20 | 21 | public static UIThread getInstance() { 22 | return LazyHolder.INSTANCE; 23 | } 24 | 25 | private final Handler handler; 26 | 27 | private UIThread() { 28 | this.handler = new Handler(Looper.getMainLooper()); 29 | } 30 | 31 | /** 32 | * Causes the Runnable r to be added to the message queue. 33 | * The runnable will be run on the main thread. 34 | * 35 | * @param runnable {@link Runnable} to be executed. 36 | */ 37 | @Override public void post(Runnable runnable) { 38 | handler.post(runnable); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Presentation/src/main/java/com/fernandocejas/android10/sample/presentation/exception/ErrorMessageFactory.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.presentation.exception; 6 | 7 | import android.content.Context; 8 | import com.fernandocejas.android10.sample.data.exception.NetworkConnectionException; 9 | import com.fernandocejas.android10.sample.data.exception.UserNotFoundException; 10 | import com.fernandocejas.android10.sample.presentation.R; 11 | 12 | /** 13 | * Factory used to create error messages from an Exception as a condition. 14 | */ 15 | public class ErrorMessageFactory { 16 | 17 | private ErrorMessageFactory() { 18 | //empty 19 | } 20 | 21 | /** 22 | * Creates a String representing an error message. 23 | * 24 | * @param context Context needed to retrieve string resources. 25 | * @param exception An exception used as a condition to retrieve the correct error message. 26 | * @return {@link String} an error message. 27 | */ 28 | public static String create(Context context, Exception exception) { 29 | String message = context.getString(R.string.exception_message_generic); 30 | 31 | if (exception instanceof NetworkConnectionException) { 32 | message = context.getString(R.string.exception_message_no_connection); 33 | } else if (exception instanceof UserNotFoundException) { 34 | message = context.getString(R.string.exception_message_user_not_found); 35 | } 36 | 37 | return message; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Presentation/src/main/java/com/fernandocejas/android10/sample/presentation/mapper/UserModelDataMapper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.presentation.mapper; 6 | 7 | import com.fernandocejas.android10.sample.domain.User; 8 | import com.fernandocejas.android10.sample.presentation.model.UserModel; 9 | import java.util.ArrayList; 10 | import java.util.Collection; 11 | import java.util.Collections; 12 | 13 | /** 14 | * Mapper class used to transform {@link User} (in the domain layer) to {@link UserModel} in the 15 | * presentation layer. 16 | */ 17 | public class UserModelDataMapper { 18 | 19 | public UserModelDataMapper() { 20 | //empty 21 | } 22 | 23 | /** 24 | * Transform a {@link User} into an {@link UserModel}. 25 | * 26 | * @param user Object to be transformed. 27 | * @return {@link UserModel}. 28 | */ 29 | public UserModel transform(User user) { 30 | if (user == null) { 31 | throw new IllegalArgumentException("Cannot transform a null value"); 32 | } 33 | UserModel userModel = new UserModel(user.getUserId()); 34 | userModel.setCoverUrl(user.getCoverUrl()); 35 | userModel.setFullName(user.getFullName()); 36 | userModel.setEmail(user.getEmail()); 37 | userModel.setDescription(user.getDescription()); 38 | userModel.setFollowers(user.getFollowers()); 39 | 40 | return userModel; 41 | } 42 | 43 | /** 44 | * Transform a Collection of {@link User} into a Collection of {@link UserModel}. 45 | * 46 | * @param usersCollection Objects to be transformed. 47 | * @return List of {@link UserModel}. 48 | */ 49 | public Collection transform(Collection usersCollection) { 50 | Collection userModelsCollection; 51 | 52 | if (usersCollection != null && !usersCollection.isEmpty()) { 53 | userModelsCollection = new ArrayList(); 54 | for (User user : usersCollection) { 55 | userModelsCollection.add(transform(user)); 56 | } 57 | } else { 58 | userModelsCollection = Collections.emptyList(); 59 | } 60 | 61 | return userModelsCollection; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Presentation/src/main/java/com/fernandocejas/android10/sample/presentation/model/UserModel.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.presentation.model; 6 | 7 | /** 8 | * Class that represents a user in the presentation layer. 9 | */ 10 | public class UserModel { 11 | 12 | private final int userId; 13 | 14 | public UserModel(int userId) { 15 | this.userId = userId; 16 | } 17 | 18 | private String coverUrl; 19 | private String fullName; 20 | private String email; 21 | private String description; 22 | private int followers; 23 | 24 | public int getUserId() { 25 | return userId; 26 | } 27 | 28 | public String getCoverUrl() { 29 | return coverUrl; 30 | } 31 | 32 | public void setCoverUrl(String coverUrl) { 33 | this.coverUrl = coverUrl; 34 | } 35 | 36 | public String getFullName() { 37 | return fullName; 38 | } 39 | 40 | public void setFullName(String fullName) { 41 | this.fullName = fullName; 42 | } 43 | 44 | public String getEmail() { 45 | return email; 46 | } 47 | 48 | public void setEmail(String email) { 49 | this.email = email; 50 | } 51 | 52 | public String getDescription() { 53 | return description; 54 | } 55 | 56 | public void setDescription(String description) { 57 | this.description = description; 58 | } 59 | 60 | public int getFollowers() { 61 | return followers; 62 | } 63 | 64 | public void setFollowers(int followers) { 65 | this.followers = followers; 66 | } 67 | 68 | @Override public String toString() { 69 | StringBuilder stringBuilder = new StringBuilder(); 70 | 71 | stringBuilder.append("***** User Model Details *****\n"); 72 | stringBuilder.append("id=" + this.getUserId() + "\n"); 73 | stringBuilder.append("cover url=" + this.getCoverUrl() + "\n"); 74 | stringBuilder.append("fullname=" + this.getFullName() + "\n"); 75 | stringBuilder.append("email=" + this.getEmail() + "\n"); 76 | stringBuilder.append("description=" + this.getDescription() + "\n"); 77 | stringBuilder.append("followers=" + this.getFollowers() + "\n"); 78 | stringBuilder.append("*******************************"); 79 | 80 | return stringBuilder.toString(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Presentation/src/main/java/com/fernandocejas/android10/sample/presentation/navigation/Navigator.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.presentation.navigation; 6 | 7 | import android.content.Context; 8 | import android.content.Intent; 9 | import com.fernandocejas.android10.sample.presentation.view.activity.UserDetailsActivity; 10 | import com.fernandocejas.android10.sample.presentation.view.activity.UserListActivity; 11 | 12 | /** 13 | * Class used to navigate through the application. 14 | */ 15 | public class Navigator { 16 | 17 | public void Navigator() { 18 | //empty 19 | } 20 | 21 | /** 22 | * Goes to the user list screen. 23 | * 24 | * @param context A Context needed to open the destiny activity. 25 | */ 26 | public void navigateToUserList(Context context) { 27 | if (context != null) { 28 | Intent intentToLaunch = UserListActivity.getCallingIntent(context); 29 | context.startActivity(intentToLaunch); 30 | } 31 | } 32 | 33 | /** 34 | * Goes to the user details screen. 35 | * 36 | * @param context A Context needed to open the destiny activity. 37 | */ 38 | public void navigateToUserDetails(Context context, int userId) { 39 | if (context != null) { 40 | Intent intentToLaunch = UserDetailsActivity.getCallingIntent(context, userId); 41 | context.startActivity(intentToLaunch); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Presentation/src/main/java/com/fernandocejas/android10/sample/presentation/presenter/Presenter.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.presentation.presenter; 6 | 7 | /** 8 | * Interface representing a Presenter in a model view presenter (MVP) pattern. 9 | */ 10 | public interface Presenter { 11 | /** 12 | * Method that control the lifecycle of the view. It should be called in the view's 13 | * (Activity or Fragment) onResume() method. 14 | */ 15 | void resume(); 16 | 17 | /** 18 | * Method that control the lifecycle of the view. It should be called in the view's 19 | * (Activity or Fragment) onPause() method. 20 | */ 21 | void pause(); 22 | } 23 | -------------------------------------------------------------------------------- /Presentation/src/main/java/com/fernandocejas/android10/sample/presentation/presenter/UserDetailsPresenter.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.presentation.presenter; 6 | 7 | import com.fernandocejas.android10.sample.domain.User; 8 | import com.fernandocejas.android10.sample.domain.exception.ErrorBundle; 9 | import com.fernandocejas.android10.sample.domain.interactor.GetUserDetailsUseCase; 10 | import com.fernandocejas.android10.sample.presentation.exception.ErrorMessageFactory; 11 | import com.fernandocejas.android10.sample.presentation.mapper.UserModelDataMapper; 12 | import com.fernandocejas.android10.sample.presentation.model.UserModel; 13 | import com.fernandocejas.android10.sample.presentation.view.UserDetailsView; 14 | 15 | /** 16 | * {@link Presenter} that controls communication between views and models of the presentation 17 | * layer. 18 | */ 19 | public class UserDetailsPresenter implements Presenter { 20 | 21 | /** id used to retrieve user details */ 22 | private int userId; 23 | 24 | private final UserDetailsView viewDetailsView; 25 | private final GetUserDetailsUseCase getUserDetailsUseCase; 26 | private final UserModelDataMapper userModelDataMapper; 27 | 28 | public UserDetailsPresenter(UserDetailsView userDetailsView, 29 | GetUserDetailsUseCase getUserDetailsUseCase, UserModelDataMapper userModelDataMapper) { 30 | if (userDetailsView == null || getUserDetailsUseCase == null || userModelDataMapper == null) { 31 | throw new IllegalArgumentException("Constructor parameters cannot be null!!!"); 32 | } 33 | this.viewDetailsView = userDetailsView; 34 | this.getUserDetailsUseCase = getUserDetailsUseCase; 35 | this.userModelDataMapper = userModelDataMapper; 36 | } 37 | 38 | @Override public void resume() {} 39 | 40 | @Override public void pause() {} 41 | 42 | /** 43 | * Initializes the presenter by start retrieving user details. 44 | */ 45 | public void initialize(int userId) { 46 | this.userId = userId; 47 | this.loadUserDetails(); 48 | } 49 | 50 | /** 51 | * Loads user details. 52 | */ 53 | private void loadUserDetails() { 54 | this.hideViewRetry(); 55 | this.showViewLoading(); 56 | this.getUserDetails(); 57 | } 58 | 59 | private void showViewLoading() { 60 | this.viewDetailsView.showLoading(); 61 | } 62 | 63 | private void hideViewLoading() { 64 | this.viewDetailsView.hideLoading(); 65 | } 66 | 67 | private void showViewRetry() { 68 | this.viewDetailsView.showRetry(); 69 | } 70 | 71 | private void hideViewRetry() { 72 | this.viewDetailsView.hideRetry(); 73 | } 74 | 75 | private void showErrorMessage(ErrorBundle errorBundle) { 76 | String errorMessage = ErrorMessageFactory.create(this.viewDetailsView.getContext(), 77 | errorBundle.getException()); 78 | this.viewDetailsView.showError(errorMessage); 79 | } 80 | 81 | private void showUserDetailsInView(User user) { 82 | final UserModel userModel = this.userModelDataMapper.transform(user); 83 | this.viewDetailsView.renderUser(userModel); 84 | } 85 | 86 | private void getUserDetails() { 87 | this.getUserDetailsUseCase.execute(this.userId, this.userDetailsCallback); 88 | } 89 | 90 | private final GetUserDetailsUseCase.Callback userDetailsCallback = new GetUserDetailsUseCase.Callback() { 91 | @Override public void onUserDataLoaded(User user) { 92 | UserDetailsPresenter.this.showUserDetailsInView(user); 93 | UserDetailsPresenter.this.hideViewLoading(); 94 | } 95 | 96 | @Override public void onError(ErrorBundle errorBundle) { 97 | UserDetailsPresenter.this.hideViewLoading(); 98 | UserDetailsPresenter.this.showErrorMessage(errorBundle); 99 | UserDetailsPresenter.this.showViewRetry(); 100 | } 101 | }; 102 | } 103 | -------------------------------------------------------------------------------- /Presentation/src/main/java/com/fernandocejas/android10/sample/presentation/presenter/UserListPresenter.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.presentation.presenter; 6 | 7 | import com.fernandocejas.android10.sample.domain.User; 8 | import com.fernandocejas.android10.sample.domain.exception.ErrorBundle; 9 | import com.fernandocejas.android10.sample.domain.interactor.GetUserListUseCase; 10 | import com.fernandocejas.android10.sample.presentation.exception.ErrorMessageFactory; 11 | import com.fernandocejas.android10.sample.presentation.mapper.UserModelDataMapper; 12 | import com.fernandocejas.android10.sample.presentation.model.UserModel; 13 | import com.fernandocejas.android10.sample.presentation.view.UserListView; 14 | import java.util.Collection; 15 | 16 | /** 17 | * {@link Presenter} that controls communication between views and models of the presentation 18 | * layer. 19 | */ 20 | public class UserListPresenter implements Presenter { 21 | 22 | private final UserListView viewListView; 23 | private final GetUserListUseCase getUserListUseCase; 24 | private final UserModelDataMapper userModelDataMapper; 25 | 26 | public UserListPresenter(UserListView userListView, GetUserListUseCase getUserListUserCase, 27 | UserModelDataMapper userModelDataMapper) { 28 | if (userListView == null || getUserListUserCase == null || userModelDataMapper == null) { 29 | throw new IllegalArgumentException("Constructor parameters cannot be null!!!"); 30 | } 31 | this.viewListView = userListView; 32 | this.getUserListUseCase = getUserListUserCase; 33 | this.userModelDataMapper = userModelDataMapper; 34 | } 35 | 36 | @Override public void resume() {} 37 | 38 | @Override public void pause() {} 39 | 40 | /** 41 | * Initializes the presenter by start retrieving the user list. 42 | */ 43 | public void initialize() { 44 | this.loadUserList(); 45 | } 46 | 47 | /** 48 | * Loads all users. 49 | */ 50 | private void loadUserList() { 51 | this.hideViewRetry(); 52 | this.showViewLoading(); 53 | this.getUserList(); 54 | } 55 | 56 | public void onUserClicked(UserModel userModel) { 57 | this.viewListView.viewUser(userModel); 58 | } 59 | 60 | private void showViewLoading() { 61 | this.viewListView.showLoading(); 62 | } 63 | 64 | private void hideViewLoading() { 65 | this.viewListView.hideLoading(); 66 | } 67 | 68 | private void showViewRetry() { 69 | this.viewListView.showRetry(); 70 | } 71 | 72 | private void hideViewRetry() { 73 | this.viewListView.hideRetry(); 74 | } 75 | 76 | private void showErrorMessage(ErrorBundle errorBundle) { 77 | String errorMessage = ErrorMessageFactory.create(this.viewListView.getContext(), 78 | errorBundle.getException()); 79 | this.viewListView.showError(errorMessage); 80 | } 81 | 82 | private void showUsersCollectionInView(Collection usersCollection) { 83 | final Collection userModelsCollection = 84 | this.userModelDataMapper.transform(usersCollection); 85 | this.viewListView.renderUserList(userModelsCollection); 86 | } 87 | 88 | private void getUserList() { 89 | this.getUserListUseCase.execute(userListCallback); 90 | } 91 | 92 | private final GetUserListUseCase.Callback userListCallback = new GetUserListUseCase.Callback() { 93 | @Override public void onUserListLoaded(Collection usersCollection) { 94 | UserListPresenter.this.showUsersCollectionInView(usersCollection); 95 | UserListPresenter.this.hideViewLoading(); 96 | } 97 | 98 | @Override public void onError(ErrorBundle errorBundle) { 99 | UserListPresenter.this.hideViewLoading(); 100 | UserListPresenter.this.showErrorMessage(errorBundle); 101 | UserListPresenter.this.showViewRetry(); 102 | } 103 | }; 104 | } 105 | -------------------------------------------------------------------------------- /Presentation/src/main/java/com/fernandocejas/android10/sample/presentation/view/LoadDataView.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.presentation.view; 6 | 7 | import android.content.Context; 8 | 9 | /** 10 | * Interface representing a View that will use to load data. 11 | */ 12 | public interface LoadDataView { 13 | /** 14 | * Show a view with a progress bar indicating a loading process. 15 | */ 16 | void showLoading(); 17 | 18 | /** 19 | * Hide a loading view. 20 | */ 21 | void hideLoading(); 22 | 23 | /** 24 | * Show a retry view in case of an error when retrieving data. 25 | */ 26 | void showRetry(); 27 | 28 | /** 29 | * Hide a retry view shown if there was an error when retrieving data. 30 | */ 31 | void hideRetry(); 32 | 33 | /** 34 | * Show an error message 35 | * 36 | * @param message A string representing an error. 37 | */ 38 | void showError(String message); 39 | 40 | /** 41 | * Get a {@link android.content.Context}. 42 | */ 43 | Context getContext(); 44 | } 45 | -------------------------------------------------------------------------------- /Presentation/src/main/java/com/fernandocejas/android10/sample/presentation/view/UserDetailsView.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.presentation.view; 6 | 7 | import com.fernandocejas.android10.sample.presentation.model.UserModel; 8 | 9 | /** 10 | * Interface representing a View in a model view presenter (MVP) pattern. 11 | * In this case is used as a view representing a user profile. 12 | */ 13 | public interface UserDetailsView extends LoadDataView { 14 | /** 15 | * Render a user in the UI. 16 | * 17 | * @param user The {@link UserModel} that will be shown. 18 | */ 19 | void renderUser(UserModel user); 20 | } 21 | -------------------------------------------------------------------------------- /Presentation/src/main/java/com/fernandocejas/android10/sample/presentation/view/UserListView.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.presentation.view; 6 | 7 | import com.fernandocejas.android10.sample.presentation.model.UserModel; 8 | import java.util.Collection; 9 | 10 | /** 11 | * Interface representing a View in a model view presenter (MVP) pattern. 12 | * In this case is used as a view representing a list of {@link UserModel}. 13 | */ 14 | public interface UserListView extends LoadDataView { 15 | /** 16 | * Render a user list in the UI. 17 | * 18 | * @param userModelCollection The collection of {@link UserModel} that will be shown. 19 | */ 20 | void renderUserList(Collection userModelCollection); 21 | 22 | /** 23 | * View a {@link UserModel} profile/details. 24 | * 25 | * @param userModel The user that will be shown. 26 | */ 27 | void viewUser(UserModel userModel); 28 | } 29 | -------------------------------------------------------------------------------- /Presentation/src/main/java/com/fernandocejas/android10/sample/presentation/view/activity/BaseActivity.java: -------------------------------------------------------------------------------- 1 | package com.fernandocejas.android10.sample.presentation.view.activity; 2 | 3 | import android.app.Activity; 4 | import android.app.Fragment; 5 | import android.app.FragmentTransaction; 6 | 7 | /** 8 | * Base {@link android.app.Activity} class for every Activity in this application. 9 | */ 10 | public abstract class BaseActivity extends Activity { 11 | 12 | /** 13 | * Adds a {@link Fragment} to this activity's layout. 14 | * 15 | * @param containerViewId The container view to where add the fragment. 16 | * @param fragment The fragment to be added. 17 | */ 18 | protected void addFragment(int containerViewId, Fragment fragment) { 19 | FragmentTransaction fragmentTransaction = this.getFragmentManager().beginTransaction(); 20 | fragmentTransaction.add(containerViewId, fragment); 21 | fragmentTransaction.commit(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Presentation/src/main/java/com/fernandocejas/android10/sample/presentation/view/activity/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.fernandocejas.android10.sample.presentation.view.activity; 2 | 3 | import android.os.Bundle; 4 | import android.view.View; 5 | import android.widget.Button; 6 | import com.fernandocejas.android10.sample.presentation.R; 7 | import com.fernandocejas.android10.sample.presentation.navigation.Navigator; 8 | 9 | /** 10 | * Main application screen. This is the app entry point. 11 | */ 12 | public class MainActivity extends BaseActivity { 13 | 14 | private Navigator navigator; 15 | 16 | private Button btn_LoadData; 17 | 18 | @Override 19 | protected void onCreate(Bundle savedInstanceState) { 20 | super.onCreate(savedInstanceState); 21 | setContentView(R.layout.activity_main); 22 | 23 | this.mapGUI(); 24 | this.initialize(); 25 | } 26 | 27 | /** 28 | * Maps the graphical user interface controls. 29 | */ 30 | private void mapGUI() { 31 | btn_LoadData = (Button) findViewById(R.id.btn_LoadData); 32 | btn_LoadData.setOnClickListener(loadDataOnClickListener); 33 | } 34 | 35 | /** 36 | * Initializes activity's private members. 37 | */ 38 | private void initialize() { 39 | //This initialization should be avoided by using a dependency injection framework. 40 | //But this is an example and for testing purpose. 41 | this.navigator = new Navigator(); 42 | } 43 | 44 | /** 45 | * Goes to the user list screen. 46 | */ 47 | private void navigateToUserList() { 48 | this.navigator.navigateToUserList(this); 49 | } 50 | 51 | private final View.OnClickListener loadDataOnClickListener = new View.OnClickListener() { 52 | @Override public void onClick(View v) { 53 | navigateToUserList(); 54 | } 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /Presentation/src/main/java/com/fernandocejas/android10/sample/presentation/view/activity/UserDetailsActivity.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.presentation.view.activity; 6 | 7 | import android.content.Context; 8 | import android.content.Intent; 9 | import android.os.Bundle; 10 | import android.view.Window; 11 | import com.fernandocejas.android10.sample.presentation.R; 12 | import com.fernandocejas.android10.sample.presentation.view.fragment.UserDetailsFragment; 13 | 14 | /** 15 | * Activity that shows details of a certain user. 16 | */ 17 | public class UserDetailsActivity extends BaseActivity { 18 | 19 | private static final String INTENT_EXTRA_PARAM_USER_ID = "org.android10.INTENT_PARAM_USER_ID"; 20 | private static final String INSTANCE_STATE_PARAM_USER_ID = "org.android10.STATE_PARAM_USER_ID"; 21 | 22 | private int userId; 23 | 24 | public static Intent getCallingIntent(Context context, int userId) { 25 | Intent callingIntent = new Intent(context, UserDetailsActivity.class); 26 | callingIntent.putExtra(INTENT_EXTRA_PARAM_USER_ID, userId); 27 | 28 | return callingIntent; 29 | } 30 | 31 | @Override protected void onCreate(Bundle savedInstanceState) { 32 | super.onCreate(savedInstanceState); 33 | requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); 34 | setContentView(R.layout.activity_user_details); 35 | 36 | this.initializeActivity(savedInstanceState); 37 | } 38 | 39 | @Override protected void onSaveInstanceState(Bundle outState) { 40 | if (outState != null) { 41 | outState.putInt(INSTANCE_STATE_PARAM_USER_ID, this.userId); 42 | } 43 | super.onSaveInstanceState(outState); 44 | } 45 | 46 | /** 47 | * Initializes this activity. 48 | */ 49 | private void initializeActivity(Bundle savedInstanceState) { 50 | if (savedInstanceState == null) { 51 | this.userId = getIntent().getIntExtra(INTENT_EXTRA_PARAM_USER_ID, -1); 52 | addFragment(R.id.fl_fragment, UserDetailsFragment.newInstance(this.userId)); 53 | } else { 54 | this.userId = savedInstanceState.getInt(INSTANCE_STATE_PARAM_USER_ID); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Presentation/src/main/java/com/fernandocejas/android10/sample/presentation/view/activity/UserListActivity.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.presentation.view.activity; 6 | 7 | import android.content.Context; 8 | import android.content.Intent; 9 | import android.os.Bundle; 10 | import android.view.Window; 11 | import com.fernandocejas.android10.sample.presentation.R; 12 | import com.fernandocejas.android10.sample.presentation.model.UserModel; 13 | import com.fernandocejas.android10.sample.presentation.navigation.Navigator; 14 | import com.fernandocejas.android10.sample.presentation.view.fragment.UserListFragment; 15 | 16 | /** 17 | * Activity that shows a list of Users. 18 | */ 19 | public class UserListActivity extends BaseActivity implements UserListFragment.UserListListener { 20 | 21 | private Navigator navigator; 22 | 23 | public static Intent getCallingIntent(Context context) { 24 | return new Intent(context, UserListActivity.class); 25 | } 26 | 27 | @Override protected void onCreate(Bundle savedInstanceState) { 28 | super.onCreate(savedInstanceState); 29 | requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); 30 | setContentView(R.layout.activity_user_list); 31 | 32 | this.initialize(); 33 | } 34 | 35 | @Override public void onUserClicked(UserModel userModel) { 36 | this.navigator.navigateToUserDetails(this, userModel.getUserId()); 37 | } 38 | 39 | /** 40 | * Initializes activity's private members. 41 | */ 42 | private void initialize() { 43 | //This initialization should be avoided by using a dependency injection framework. 44 | //But this is an example and for testing purpose. 45 | this.navigator = new Navigator(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Presentation/src/main/java/com/fernandocejas/android10/sample/presentation/view/adapter/UsersAdapter.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.presentation.view.adapter; 6 | 7 | import android.content.Context; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.widget.BaseAdapter; 12 | import android.widget.TextView; 13 | import com.fernandocejas.android10.sample.presentation.R; 14 | import com.fernandocejas.android10.sample.presentation.model.UserModel; 15 | import java.util.Collection; 16 | import java.util.List; 17 | 18 | /** 19 | * Adaptar that manages a collection of {@link UserModel}. 20 | */ 21 | public class UsersAdapter extends BaseAdapter { 22 | 23 | private List usersCollection; 24 | private final LayoutInflater layoutInflater; 25 | 26 | public UsersAdapter(Context context, Collection usersCollection) { 27 | this.validateUsersCollection(usersCollection); 28 | this.layoutInflater = 29 | (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 30 | this.usersCollection = (List) usersCollection; 31 | } 32 | 33 | @Override public int getCount() { 34 | int count = 0; 35 | if (this.usersCollection != null && !this.usersCollection.isEmpty()) { 36 | count = this.usersCollection.size(); 37 | } 38 | return count; 39 | } 40 | 41 | @Override 42 | public boolean isEmpty() { 43 | return (getCount() == 0); 44 | } 45 | 46 | @Override public Object getItem(int position) { 47 | return this.usersCollection.get(position); 48 | } 49 | 50 | @Override public long getItemId(int position) { 51 | return position; 52 | } 53 | 54 | @Override public View getView(int position, View convertView, ViewGroup parent) { 55 | UserViewHolder userViewHolder; 56 | 57 | if (convertView == null) { 58 | convertView = this.layoutInflater.inflate(R.layout.row_user, parent, false); 59 | 60 | userViewHolder = new UserViewHolder(); 61 | userViewHolder.textViewTitle = (TextView) convertView.findViewById(R.id.title); 62 | 63 | convertView.setTag(userViewHolder); 64 | } else { 65 | userViewHolder = (UserViewHolder) convertView.getTag(); 66 | } 67 | 68 | UserModel userModel = this.usersCollection.get(position); 69 | userViewHolder.textViewTitle.setText(userModel.getFullName()); 70 | 71 | return convertView; 72 | } 73 | 74 | public void setUsersCollection(Collection usersCollection) { 75 | this.validateUsersCollection(usersCollection); 76 | this.usersCollection = (List) usersCollection; 77 | this.notifyDataSetChanged(); 78 | } 79 | 80 | private void validateUsersCollection(Collection usersCollection) { 81 | if (usersCollection == null) { 82 | throw new IllegalArgumentException("The track list cannot be null"); 83 | } 84 | } 85 | 86 | static class UserViewHolder { 87 | TextView textViewTitle; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Presentation/src/main/java/com/fernandocejas/android10/sample/presentation/view/component/AutoLoadImageView.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.presentation.view.component; 6 | 7 | import android.app.Activity; 8 | import android.content.Context; 9 | import android.graphics.Bitmap; 10 | import android.graphics.BitmapFactory; 11 | import android.net.ConnectivityManager; 12 | import android.net.NetworkInfo; 13 | import android.util.AttributeSet; 14 | import android.util.Log; 15 | import android.widget.ImageView; 16 | import java.io.File; 17 | import java.io.FileNotFoundException; 18 | import java.io.FileOutputStream; 19 | import java.io.IOException; 20 | import java.net.MalformedURLException; 21 | import java.net.URL; 22 | import java.net.URLConnection; 23 | 24 | /** 25 | * Simple implementation of {@link android.widget.ImageView} with extended features like setting an 26 | * image from an url and an internal file cache using the application cache directory. 27 | */ 28 | public class AutoLoadImageView extends ImageView { 29 | 30 | private static final String BASE_IMAGE_NAME_CACHED = "image_"; 31 | 32 | private int imagePlaceHolderResourceId = -1; 33 | private DiskCache cache = new DiskCache(getContext().getCacheDir()); 34 | 35 | public AutoLoadImageView(Context context) { 36 | super(context); 37 | } 38 | 39 | public AutoLoadImageView(Context context, AttributeSet attrs) { 40 | super(context, attrs); 41 | } 42 | 43 | public AutoLoadImageView(Context context, AttributeSet attrs, int defStyle) { 44 | super(context, attrs, defStyle); 45 | } 46 | 47 | /** 48 | * Set an image from a remote url. 49 | * 50 | * @param imageUrl The url of the resource to load. 51 | */ 52 | public void setImageUrl(final String imageUrl) { 53 | AutoLoadImageView.this.loadImagePlaceHolder(); 54 | if (imageUrl != null) { 55 | this.loadImageFromUrl(imageUrl); 56 | } else { 57 | this.loadImagePlaceHolder(); 58 | } 59 | } 60 | 61 | /** 62 | * Set a place holder used for loading when an image is being downloaded from the internet. 63 | * 64 | * @param resourceId The resource id to use as a place holder. 65 | */ 66 | public void setImagePlaceHolder(int resourceId) { 67 | this.imagePlaceHolderResourceId = resourceId; 68 | this.loadImagePlaceHolder(); 69 | } 70 | 71 | /** 72 | * Invalidate the internal cache by evicting all cached elements. 73 | */ 74 | public void invalidateImageCache() { 75 | if (this.cache != null) { 76 | this.cache.evictAll(); 77 | } 78 | } 79 | 80 | /** 81 | * Loads and image from the internet (and cache it) or from the internal cache. 82 | * 83 | * @param imageUrl The remote image url to load. 84 | */ 85 | private void loadImageFromUrl(final String imageUrl) { 86 | new Thread() { 87 | @Override public void run() { 88 | final Bitmap bitmap = AutoLoadImageView.this.getFromCache(getFileNameFromUrl(imageUrl)); 89 | if (bitmap != null) { 90 | AutoLoadImageView.this.loadBitmap(bitmap); 91 | } else { 92 | if (isThereInternetConnection()) { 93 | final ImageDownloader imageDownloader = new ImageDownloader(); 94 | imageDownloader.download(imageUrl, new ImageDownloader.Callback() { 95 | @Override public void onImageDownloaded(Bitmap bitmap) { 96 | AutoLoadImageView.this.cacheBitmap(bitmap, getFileNameFromUrl(imageUrl)); 97 | AutoLoadImageView.this.loadBitmap(bitmap); 98 | } 99 | 100 | @Override public void onError() { 101 | AutoLoadImageView.this.loadImagePlaceHolder(); 102 | } 103 | }); 104 | } else { 105 | AutoLoadImageView.this.loadImagePlaceHolder(); 106 | } 107 | } 108 | } 109 | }.start(); 110 | } 111 | 112 | /** 113 | * Run the operation of loading a bitmap on the UI thread. 114 | * 115 | * @param bitmap The image to load. 116 | */ 117 | private void loadBitmap(final Bitmap bitmap) { 118 | ((Activity) getContext()).runOnUiThread(new Runnable() { 119 | @Override public void run() { 120 | AutoLoadImageView.this.setImageBitmap(bitmap); 121 | } 122 | }); 123 | } 124 | 125 | /** 126 | * Loads the image place holder if any has been assigned. 127 | */ 128 | private void loadImagePlaceHolder() { 129 | if (this.imagePlaceHolderResourceId != -1) { 130 | ((Activity) getContext()).runOnUiThread(new Runnable() { 131 | @Override public void run() { 132 | AutoLoadImageView.this.setImageResource( 133 | AutoLoadImageView.this.imagePlaceHolderResourceId); 134 | } 135 | }); 136 | } 137 | } 138 | 139 | /** 140 | * Get a {@link android.graphics.Bitmap} from the internal cache or null if it does not exist. 141 | * 142 | * @param fileName The name of the file to look for in the cache. 143 | * @return A valid cached bitmap, otherwise null. 144 | */ 145 | private Bitmap getFromCache(String fileName) { 146 | Bitmap bitmap = null; 147 | if (this.cache != null) { 148 | bitmap = this.cache.get(fileName); 149 | } 150 | return bitmap; 151 | } 152 | 153 | /** 154 | * Cache an image using the internal cache. 155 | * 156 | * @param bitmap The bitmap to cache. 157 | * @param fileName The file name used for caching the bitmap. 158 | */ 159 | private void cacheBitmap(Bitmap bitmap, String fileName) { 160 | if (this.cache != null) { 161 | this.cache.put(bitmap, fileName); 162 | } 163 | } 164 | 165 | /** 166 | * Checks if the device has any active internet connection. 167 | * 168 | * @return true device with internet connection, otherwise false. 169 | */ 170 | private boolean isThereInternetConnection() { 171 | boolean isConnected; 172 | 173 | ConnectivityManager connectivityManager = 174 | (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE); 175 | NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); 176 | isConnected = (networkInfo != null && networkInfo.isConnectedOrConnecting()); 177 | 178 | return isConnected; 179 | } 180 | 181 | /** 182 | * Creates a file name from an image url 183 | * 184 | * @param imageUrl The image url used to build the file name. 185 | * @return An String representing a unique file name. 186 | */ 187 | private String getFileNameFromUrl(String imageUrl) { 188 | //we could generate an unique MD5/SHA-1 here 189 | String hash = String.valueOf(imageUrl.hashCode()); 190 | if (hash.startsWith("-")) { 191 | hash = hash.substring(1); 192 | } 193 | return BASE_IMAGE_NAME_CACHED + hash; 194 | } 195 | 196 | /** 197 | * Class used to download images from the internet 198 | */ 199 | private static class ImageDownloader { 200 | interface Callback { 201 | void onImageDownloaded(Bitmap bitmap); 202 | 203 | void onError(); 204 | } 205 | 206 | ImageDownloader() {} 207 | 208 | /** 209 | * Download an image from an url. 210 | * 211 | * @param imageUrl The url of the image to download. 212 | * @param callback A callback used to be reported when the task is finished. 213 | */ 214 | void download(String imageUrl, Callback callback) { 215 | try { 216 | URLConnection conn = new URL(imageUrl).openConnection(); 217 | conn.connect(); 218 | Bitmap bitmap = BitmapFactory.decodeStream(conn.getInputStream()); 219 | if (callback != null) { 220 | callback.onImageDownloaded(bitmap); 221 | } 222 | } catch (MalformedURLException e) { 223 | reportError(callback); 224 | } catch (IOException e) { 225 | reportError(callback); 226 | } 227 | } 228 | 229 | /** 230 | * Report an error to the caller 231 | * 232 | * @param callback Caller implementing {@link Callback} 233 | */ 234 | private void reportError(Callback callback) { 235 | if (callback != null) { 236 | callback.onError(); 237 | } 238 | } 239 | } 240 | 241 | /** 242 | * A simple disk cache implementation 243 | */ 244 | private static class DiskCache { 245 | 246 | private static final String TAG = "DiskCache"; 247 | 248 | private final File cacheDir; 249 | 250 | DiskCache(File cacheDir) { 251 | this.cacheDir = cacheDir; 252 | } 253 | 254 | /** 255 | * Get an element from the cache. 256 | * 257 | * @param fileName The name of the file to look for. 258 | * @return A valid element, otherwise false. 259 | */ 260 | synchronized Bitmap get(String fileName) { 261 | Bitmap bitmap = null; 262 | File file = buildFileFromFilename(fileName); 263 | if (file.exists()) { 264 | bitmap = BitmapFactory.decodeFile(file.getPath()); 265 | } 266 | return bitmap; 267 | } 268 | 269 | /** 270 | * Cache an element. 271 | * 272 | * @param bitmap The bitmap to be put in the cache. 273 | * @param fileName A string representing the name of the file to be cached. 274 | */ 275 | synchronized void put(Bitmap bitmap, String fileName) { 276 | File file = buildFileFromFilename(fileName); 277 | if (!file.exists()) { 278 | try { 279 | FileOutputStream fileOutputStream = new FileOutputStream(file); 280 | bitmap.compress(Bitmap.CompressFormat.PNG, 90, fileOutputStream); 281 | fileOutputStream.flush(); 282 | fileOutputStream.close(); 283 | } catch (FileNotFoundException e) { 284 | Log.e(TAG, e.getMessage()); 285 | } catch (IOException e) { 286 | Log.e(TAG, e.getMessage()); 287 | } 288 | } 289 | } 290 | 291 | /** 292 | * Invalidate and expire the cache. 293 | */ 294 | void evictAll() { 295 | if (cacheDir.exists()) { 296 | for (File file : cacheDir.listFiles()) { 297 | file.delete(); 298 | } 299 | } 300 | } 301 | 302 | /** 303 | * Creates a file name from an image url 304 | * 305 | * @param fileName The image url used to build the file name. 306 | * @return A {@link java.io.File} representing a unique element. 307 | */ 308 | private File buildFileFromFilename(String fileName) { 309 | String fullPath = this.cacheDir.getPath() + File.separator + fileName; 310 | return new File(fullPath); 311 | } 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /Presentation/src/main/java/com/fernandocejas/android10/sample/presentation/view/fragment/BaseFragment.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.presentation.view.fragment; 6 | 7 | import android.app.Fragment; 8 | import android.os.Bundle; 9 | import android.widget.Toast; 10 | 11 | /** 12 | * Base {@link android.app.Fragment} class for every fragment in this application. 13 | */ 14 | public abstract class BaseFragment extends Fragment { 15 | 16 | @Override public void onCreate(Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | setRetainInstance(true); 19 | initializePresenter(); 20 | } 21 | 22 | /** 23 | * Initializes the {@link com.fernandocejas.android10.sample.presentation.presenter.Presenter} 24 | * for this fragment in a MVP pattern used to architect the application presentation layer. 25 | */ 26 | abstract void initializePresenter(); 27 | 28 | /** 29 | * Shows a {@link android.widget.Toast} message. 30 | * 31 | * @param message An string representing a message to be shown. 32 | */ 33 | protected void showToastMessage(String message) { 34 | Toast.makeText(getActivity(), message, Toast.LENGTH_SHORT).show(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Presentation/src/main/java/com/fernandocejas/android10/sample/presentation/view/fragment/UserDetailsFragment.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.presentation.view.fragment; 6 | 7 | import android.content.Context; 8 | import android.os.Bundle; 9 | import android.view.LayoutInflater; 10 | import android.view.View; 11 | import android.view.ViewGroup; 12 | import android.widget.Button; 13 | import android.widget.RelativeLayout; 14 | import android.widget.TextView; 15 | import com.fernandocejas.android10.sample.data.cache.FileManager; 16 | import com.fernandocejas.android10.sample.data.cache.UserCache; 17 | import com.fernandocejas.android10.sample.data.cache.UserCacheImpl; 18 | import com.fernandocejas.android10.sample.data.cache.serializer.JsonSerializer; 19 | import com.fernandocejas.android10.sample.data.entity.mapper.UserEntityDataMapper; 20 | import com.fernandocejas.android10.sample.data.executor.JobExecutor; 21 | import com.fernandocejas.android10.sample.data.repository.UserDataRepository; 22 | import com.fernandocejas.android10.sample.data.repository.datasource.UserDataStoreFactory; 23 | import com.fernandocejas.android10.sample.domain.executor.PostExecutionThread; 24 | import com.fernandocejas.android10.sample.domain.executor.ThreadExecutor; 25 | import com.fernandocejas.android10.sample.domain.interactor.GetUserDetailsUseCase; 26 | import com.fernandocejas.android10.sample.domain.interactor.GetUserDetailsUseCaseImpl; 27 | import com.fernandocejas.android10.sample.domain.repository.UserRepository; 28 | import com.fernandocejas.android10.sample.presentation.R; 29 | import com.fernandocejas.android10.sample.presentation.UIThread; 30 | import com.fernandocejas.android10.sample.presentation.mapper.UserModelDataMapper; 31 | import com.fernandocejas.android10.sample.presentation.model.UserModel; 32 | import com.fernandocejas.android10.sample.presentation.presenter.UserDetailsPresenter; 33 | import com.fernandocejas.android10.sample.presentation.view.UserDetailsView; 34 | import com.fernandocejas.android10.sample.presentation.view.component.AutoLoadImageView; 35 | 36 | /** 37 | * Fragment that shows details of a certain user. 38 | */ 39 | public class UserDetailsFragment extends BaseFragment implements UserDetailsView { 40 | 41 | private static final String ARGUMENT_KEY_USER_ID = "org.android10.ARGUMENT_USER_ID"; 42 | 43 | private int userId; 44 | private UserDetailsPresenter userDetailsPresenter; 45 | 46 | private AutoLoadImageView iv_cover; 47 | private TextView tv_fullname; 48 | private TextView tv_email; 49 | private TextView tv_followers; 50 | private TextView tv_description; 51 | private RelativeLayout rl_progress; 52 | private RelativeLayout rl_retry; 53 | private Button bt_retry; 54 | 55 | public UserDetailsFragment() { super(); } 56 | 57 | public static UserDetailsFragment newInstance(int userId) { 58 | UserDetailsFragment userDetailsFragment = new UserDetailsFragment(); 59 | 60 | Bundle argumentsBundle = new Bundle(); 61 | argumentsBundle.putInt(ARGUMENT_KEY_USER_ID, userId); 62 | userDetailsFragment.setArguments(argumentsBundle); 63 | 64 | return userDetailsFragment; 65 | } 66 | 67 | @Override public void onCreate(Bundle savedInstanceState) { 68 | super.onCreate(savedInstanceState); 69 | this.initialize(); 70 | } 71 | 72 | @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, 73 | Bundle savedInstanceState) { 74 | 75 | View fragmentView = inflater.inflate(R.layout.fragment_user_details, container, false); 76 | 77 | this.iv_cover = (AutoLoadImageView) fragmentView.findViewById(R.id.iv_cover); 78 | this.tv_fullname = (TextView) fragmentView.findViewById(R.id.tv_fullname); 79 | this.tv_email = (TextView) fragmentView.findViewById(R.id.tv_email); 80 | this.tv_followers = (TextView) fragmentView.findViewById(R.id.tv_followers); 81 | this.tv_description = (TextView) fragmentView.findViewById(R.id.tv_description); 82 | this.rl_progress = (RelativeLayout) fragmentView.findViewById(R.id.rl_progress); 83 | this.rl_retry = (RelativeLayout) fragmentView.findViewById(R.id.rl_retry); 84 | this.bt_retry = (Button) fragmentView.findViewById(R.id.bt_retry); 85 | this.bt_retry.setOnClickListener(this.retryOnClickListener); 86 | 87 | return fragmentView; 88 | } 89 | 90 | @Override public void onActivityCreated(Bundle savedInstanceState) { 91 | super.onActivityCreated(savedInstanceState); 92 | this.userDetailsPresenter.initialize(this.userId); 93 | } 94 | 95 | @Override public void onResume() { 96 | super.onResume(); 97 | this.userDetailsPresenter.resume(); 98 | } 99 | 100 | @Override public void onPause() { 101 | super.onPause(); 102 | this.userDetailsPresenter.pause(); 103 | } 104 | 105 | @Override void initializePresenter() { 106 | // All these dependency initialization could have been avoided using a 107 | // dependency injection framework. But in this case are used this way for 108 | // LEARNING EXAMPLE PURPOSE. 109 | ThreadExecutor threadExecutor = JobExecutor.getInstance(); 110 | PostExecutionThread postExecutionThread = UIThread.getInstance(); 111 | 112 | JsonSerializer userCacheSerializer = new JsonSerializer(); 113 | UserCache userCache = UserCacheImpl.getInstance(getActivity(), userCacheSerializer, 114 | FileManager.getInstance(), threadExecutor); 115 | UserDataStoreFactory userDataStoreFactory = 116 | new UserDataStoreFactory(this.getContext(), userCache); 117 | UserEntityDataMapper userEntityDataMapper = new UserEntityDataMapper(); 118 | UserRepository userRepository = UserDataRepository.getInstance(userDataStoreFactory, 119 | userEntityDataMapper); 120 | 121 | GetUserDetailsUseCase getUserDetailsUseCase = new GetUserDetailsUseCaseImpl(userRepository, 122 | threadExecutor, postExecutionThread); 123 | UserModelDataMapper userModelDataMapper = new UserModelDataMapper(); 124 | 125 | this.userDetailsPresenter = 126 | new UserDetailsPresenter(this, getUserDetailsUseCase, userModelDataMapper); 127 | } 128 | 129 | @Override public void renderUser(UserModel user) { 130 | if (user != null) { 131 | this.iv_cover.setImageUrl(user.getCoverUrl()); 132 | this.tv_fullname.setText(user.getFullName()); 133 | this.tv_email.setText(user.getEmail()); 134 | this.tv_followers.setText(String.valueOf(user.getFollowers())); 135 | this.tv_description.setText(user.getDescription()); 136 | } 137 | } 138 | 139 | @Override public void showLoading() { 140 | this.rl_progress.setVisibility(View.VISIBLE); 141 | this.getActivity().setProgressBarIndeterminateVisibility(true); 142 | } 143 | 144 | @Override public void hideLoading() { 145 | this.rl_progress.setVisibility(View.GONE); 146 | this.getActivity().setProgressBarIndeterminateVisibility(false); 147 | } 148 | 149 | @Override public void showRetry() { 150 | this.rl_retry.setVisibility(View.VISIBLE); 151 | } 152 | 153 | @Override public void hideRetry() { 154 | this.rl_retry.setVisibility(View.GONE); 155 | } 156 | 157 | @Override public void showError(String message) { 158 | this.showToastMessage(message); 159 | } 160 | 161 | @Override public Context getContext() { 162 | return getActivity().getApplicationContext(); 163 | } 164 | 165 | /** 166 | * Initializes fragment's private members. 167 | */ 168 | private void initialize() { 169 | this.userId = getArguments().getInt(ARGUMENT_KEY_USER_ID); 170 | } 171 | 172 | /** 173 | * Loads all users. 174 | */ 175 | private void loadUserDetails() { 176 | if (this.userDetailsPresenter != null) { 177 | this.userDetailsPresenter.initialize(this.userId); 178 | } 179 | } 180 | 181 | private final View.OnClickListener retryOnClickListener = new View.OnClickListener() { 182 | @Override public void onClick(View view) { 183 | UserDetailsFragment.this.loadUserDetails(); 184 | } 185 | }; 186 | } 187 | -------------------------------------------------------------------------------- /Presentation/src/main/java/com/fernandocejas/android10/sample/presentation/view/fragment/UserListFragment.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package com.fernandocejas.android10.sample.presentation.view.fragment; 6 | 7 | import android.app.Activity; 8 | import android.content.Context; 9 | import android.os.Bundle; 10 | import android.view.LayoutInflater; 11 | import android.view.View; 12 | import android.view.ViewGroup; 13 | import android.widget.AdapterView; 14 | import android.widget.Button; 15 | import android.widget.ListView; 16 | import android.widget.RelativeLayout; 17 | import com.fernandocejas.android10.sample.data.cache.FileManager; 18 | import com.fernandocejas.android10.sample.data.cache.UserCache; 19 | import com.fernandocejas.android10.sample.data.cache.UserCacheImpl; 20 | import com.fernandocejas.android10.sample.data.cache.serializer.JsonSerializer; 21 | import com.fernandocejas.android10.sample.data.entity.mapper.UserEntityDataMapper; 22 | import com.fernandocejas.android10.sample.data.executor.JobExecutor; 23 | import com.fernandocejas.android10.sample.data.repository.UserDataRepository; 24 | import com.fernandocejas.android10.sample.data.repository.datasource.UserDataStoreFactory; 25 | import com.fernandocejas.android10.sample.domain.executor.PostExecutionThread; 26 | import com.fernandocejas.android10.sample.domain.executor.ThreadExecutor; 27 | import com.fernandocejas.android10.sample.domain.interactor.GetUserListUseCase; 28 | import com.fernandocejas.android10.sample.domain.interactor.GetUserListUseCaseImpl; 29 | import com.fernandocejas.android10.sample.domain.repository.UserRepository; 30 | import com.fernandocejas.android10.sample.presentation.R; 31 | import com.fernandocejas.android10.sample.presentation.UIThread; 32 | import com.fernandocejas.android10.sample.presentation.mapper.UserModelDataMapper; 33 | import com.fernandocejas.android10.sample.presentation.model.UserModel; 34 | import com.fernandocejas.android10.sample.presentation.presenter.UserListPresenter; 35 | import com.fernandocejas.android10.sample.presentation.view.UserListView; 36 | import com.fernandocejas.android10.sample.presentation.view.adapter.UsersAdapter; 37 | import java.util.Collection; 38 | 39 | /** 40 | * Fragment that shows a list of Users. 41 | */ 42 | public class UserListFragment extends BaseFragment implements UserListView { 43 | 44 | /** 45 | * Interface for listening user list events. 46 | */ 47 | public interface UserListListener { 48 | void onUserClicked(final UserModel userModel); 49 | } 50 | 51 | private UserListPresenter userListPresenter; 52 | 53 | private ListView lv_users; 54 | private RelativeLayout rl_progress; 55 | private RelativeLayout rl_retry; 56 | private Button bt_retry; 57 | 58 | private UsersAdapter usersAdapter; 59 | 60 | private UserListListener userListListener; 61 | 62 | public UserListFragment() { super(); } 63 | 64 | @Override public void onAttach(Activity activity) { 65 | super.onAttach(activity); 66 | if (activity instanceof UserListListener) { 67 | this.userListListener = (UserListListener) activity; 68 | } 69 | } 70 | 71 | @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, 72 | Bundle savedInstanceState) { 73 | 74 | View fragmentView = inflater.inflate(R.layout.fragment_user_list, container, true); 75 | 76 | this.lv_users = (ListView) fragmentView.findViewById(R.id.lv_users); 77 | this.lv_users.setOnItemClickListener(this.userOnItemClickListener); 78 | this.rl_progress = (RelativeLayout) fragmentView.findViewById(R.id.rl_progress); 79 | this.rl_retry = (RelativeLayout) fragmentView.findViewById(R.id.rl_retry); 80 | this.bt_retry = (Button) fragmentView.findViewById(R.id.bt_retry); 81 | this.bt_retry.setOnClickListener(this.retryOnClickListener); 82 | 83 | return fragmentView; 84 | } 85 | 86 | @Override public void onActivityCreated(Bundle savedInstanceState) { 87 | super.onActivityCreated(savedInstanceState); 88 | this.userListPresenter.initialize(); 89 | } 90 | 91 | @Override public void onResume() { 92 | super.onResume(); 93 | this.userListPresenter.resume(); 94 | } 95 | 96 | @Override public void onPause() { 97 | super.onPause(); 98 | this.userListPresenter.pause(); 99 | } 100 | 101 | @Override protected void initializePresenter() { 102 | // All these dependency initialization could have been avoided using a 103 | // dependency injection framework. But in this case are used this way for 104 | // LEARNING EXAMPLE PURPOSE. 105 | ThreadExecutor threadExecutor = JobExecutor.getInstance(); 106 | PostExecutionThread postExecutionThread = UIThread.getInstance(); 107 | 108 | JsonSerializer userCacheSerializer = new JsonSerializer(); 109 | UserCache userCache = UserCacheImpl.getInstance(getActivity(), userCacheSerializer, 110 | FileManager.getInstance(), threadExecutor); 111 | UserDataStoreFactory userDataStoreFactory = 112 | new UserDataStoreFactory(this.getContext(), userCache); 113 | UserEntityDataMapper userEntityDataMapper = new UserEntityDataMapper(); 114 | UserRepository userRepository = UserDataRepository.getInstance(userDataStoreFactory, 115 | userEntityDataMapper); 116 | 117 | GetUserListUseCase getUserListUseCase = new GetUserListUseCaseImpl(userRepository, 118 | threadExecutor, postExecutionThread); 119 | UserModelDataMapper userModelDataMapper = new UserModelDataMapper(); 120 | 121 | this.userListPresenter = new UserListPresenter(this, getUserListUseCase, userModelDataMapper); 122 | } 123 | 124 | @Override public void showLoading() { 125 | this.rl_progress.setVisibility(View.VISIBLE); 126 | this.getActivity().setProgressBarIndeterminateVisibility(true); 127 | } 128 | 129 | @Override public void hideLoading() { 130 | this.rl_progress.setVisibility(View.GONE); 131 | this.getActivity().setProgressBarIndeterminateVisibility(false); 132 | } 133 | 134 | @Override public void showRetry() { 135 | this.rl_retry.setVisibility(View.VISIBLE); 136 | } 137 | 138 | @Override public void hideRetry() { 139 | this.rl_retry.setVisibility(View.GONE); 140 | } 141 | 142 | @Override public void renderUserList(Collection userModelCollection) { 143 | if (userModelCollection != null) { 144 | if (this.usersAdapter == null) { 145 | this.usersAdapter = new UsersAdapter(getActivity(), userModelCollection); 146 | } else { 147 | this.usersAdapter.setUsersCollection(userModelCollection); 148 | } 149 | this.lv_users.setAdapter(usersAdapter); 150 | } 151 | } 152 | 153 | @Override public void viewUser(UserModel userModel) { 154 | if (this.userListListener != null) { 155 | this.userListListener.onUserClicked(userModel); 156 | } 157 | } 158 | 159 | @Override public void showError(String message) { 160 | this.showToastMessage(message); 161 | } 162 | 163 | @Override public Context getContext() { 164 | return this.getActivity().getApplicationContext(); 165 | } 166 | 167 | /** 168 | * Loads all users. 169 | */ 170 | private void loadUserList() { 171 | if (this.userListPresenter != null) { 172 | this.userListPresenter.initialize(); 173 | } 174 | } 175 | 176 | /** 177 | * Views a {@link UserModel} when is clicked. 178 | * Uses the presenter via composition to achieve this. 179 | * 180 | * @param userModel {@link UserModel} to show. 181 | */ 182 | private void onUserClicked(UserModel userModel) { 183 | if (this.userListPresenter != null) { 184 | this.userListPresenter.onUserClicked(userModel); 185 | } 186 | } 187 | 188 | private final View.OnClickListener retryOnClickListener = new View.OnClickListener() { 189 | @Override public void onClick(View view) { 190 | UserListFragment.this.loadUserList(); 191 | } 192 | }; 193 | 194 | private final AdapterView.OnItemClickListener userOnItemClickListener = 195 | new AdapterView.OnItemClickListener() { 196 | @Override public void onItemClick(AdapterView parent, View view, int position, long id) { 197 | UserModel userModel = (UserModel) UserListFragment.this.usersAdapter.getItem(position); 198 | UserListFragment.this.onUserClicked(userModel); 199 | } 200 | }; 201 | } 202 | -------------------------------------------------------------------------------- /Presentation/src/main/project.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system edit 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | # 10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): 11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt 12 | 13 | # Project target. 14 | target=android-19 15 | android.library.reference.1=../../../data/src/main 16 | -------------------------------------------------------------------------------- /Presentation/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxy1228/MVP/ed2b4ee61abb5b2df57dc71e8d55e4349f9cfb5a/Presentation/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /Presentation/src/main/res/drawable-hdpi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxy1228/MVP/ed2b4ee61abb5b2df57dc71e8d55e4349f9cfb5a/Presentation/src/main/res/drawable-hdpi/logo.png -------------------------------------------------------------------------------- /Presentation/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxy1228/MVP/ed2b4ee61abb5b2df57dc71e8d55e4349f9cfb5a/Presentation/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /Presentation/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxy1228/MVP/ed2b4ee61abb5b2df57dc71e8d55e4349f9cfb5a/Presentation/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /Presentation/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxy1228/MVP/ed2b4ee61abb5b2df57dc71e8d55e4349f9cfb5a/Presentation/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /Presentation/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 12 | 13 | 19 | 20 | 26 | 27 |