diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5edb4ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..0161652 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,73 @@ +apply plugin: 'com.android.application' +apply plugin: 'io.fabric' + +repositories { + maven { url 'https://maven.fabric.io/public' } +} + + +android { + compileSdkVersion 28 + defaultConfig { + applicationId "com.khammami.imerolium" + minSdkVersion 16 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + + vectorDrawables.useSupportLibrary = true + + multiDexEnabled true + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + api project(':colorpicker') + implementation 'com.android.support:multidex:1.0.3' + implementation 'com.android.support:appcompat-v7:28.0.0-alpha3' + implementation 'com.android.support:preference-v7:28.0.0-alpha3' + implementation 'com.android.support:design:28.0.0-alpha3' + implementation 'com.android.support:support-v4:28.0.0-alpha3' + implementation 'com.android.support.constraint:constraint-layout:1.1.2' + + implementation 'com.google.android.gms:play-services-auth:15.0.1' + implementation 'com.google.firebase:firebase-core:16.0.1' + implementation 'com.google.firebase:firebase-auth:16.0.2' + implementation 'com.google.firebase:firebase-firestore:17.0.2' + + implementation "android.arch.lifecycle:runtime:1.1.1" + implementation "android.arch.lifecycle:extensions:1.1.1" + annotationProcessor "android.arch.lifecycle:compiler:1.1.1" + implementation "android.arch.persistence.room:runtime:1.1.1" + annotationProcessor "android.arch.persistence.room:compiler:1.1.1" + + api('com.google.api-client:google-api-client-android:1.23.0') { + exclude group: 'com.google.code.findbugs' + exclude group: 'org.apache.httpcomponents' + exclude group: 'com.google.guava' + } + api('com.google.apis:google-api-services-people:v1-rev168-1.23.0') { + exclude group: 'com.google.code.findbugs' + exclude group: 'org.apache.httpcomponents' + exclude group: 'com.google.guava' + } + + implementation 'com.squareup.picasso:picasso:2.71828' + + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + implementation('com.crashlytics.sdk.android:crashlytics:2.9.4@aar') { + transitive = true + } +} + +apply plugin: 'com.google.gms.google-services' diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/src/androidTest/java/com/khammami/imerolium/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/khammami/imerolium/ExampleInstrumentedTest.java new file mode 100644 index 0000000..48bdc2f --- /dev/null +++ b/app/src/androidTest/java/com/khammami/imerolium/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.khammami.imerolium; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("com.khammami.imerolium", appContext.getPackageName()); + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..2db7461 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/khammami/imerolium/AppExecutors.java b/app/src/main/java/com/khammami/imerolium/AppExecutors.java new file mode 100644 index 0000000..6609d47 --- /dev/null +++ b/app/src/main/java/com/khammami/imerolium/AppExecutors.java @@ -0,0 +1,56 @@ +package com.khammami.imerolium; + +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.NonNull; + +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +public class AppExecutors { + + // For Singleton instantiation + private static final Object LOCK = new Object(); + private static AppExecutors sInstance; + private final Executor diskIO; + private final Executor mainThread; + private final Executor networkIO; + + private AppExecutors(Executor diskIO, Executor networkIO, Executor mainThread) { + this.diskIO = diskIO; + this.networkIO = networkIO; + this.mainThread = mainThread; + } + + public static AppExecutors getInstance() { + if (sInstance == null) { + synchronized (LOCK) { + sInstance = new AppExecutors(Executors.newSingleThreadExecutor(), + Executors.newFixedThreadPool(3), + new MainThreadExecutor()); + } + } + return sInstance; + } + + public Executor diskIO() { + return diskIO; + } + + public Executor mainThread() { + return mainThread; + } + + public Executor networkIO() { + return networkIO; + } + + private static class MainThreadExecutor implements Executor { + private Handler mainThreadHandler = new Handler(Looper.getMainLooper()); + + @Override + public void execute(@NonNull Runnable command) { + mainThreadHandler.post(command); + } + } +} diff --git a/app/src/main/java/com/khammami/imerolium/BaseActivity.java b/app/src/main/java/com/khammami/imerolium/BaseActivity.java new file mode 100644 index 0000000..c61a2a0 --- /dev/null +++ b/app/src/main/java/com/khammami/imerolium/BaseActivity.java @@ -0,0 +1,40 @@ +package com.khammami.imerolium; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.support.v7.app.AppCompatActivity; + +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseUser; +import com.khammami.imerolium.ui.LoginActivity; +import com.khammami.imerolium.ui.MainActivity; + +public class BaseActivity extends AppCompatActivity { + + @VisibleForTesting + public FirebaseAuth mAuth; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mAuth = FirebaseAuth.getInstance(); + checkAuth(); + } + + public void checkAuth() { + FirebaseUser currentUser = mAuth.getCurrentUser(); + if (!(this instanceof LoginActivity) && currentUser == null) { + Intent signInIntent = new Intent(this, LoginActivity.class); + startActivity(signInIntent); + finish(); + }else if((this instanceof LoginActivity) && currentUser != null){ + Intent mainIntent = new Intent(this, MainActivity.class); + startActivity(mainIntent); + finish(); + } + } +} diff --git a/app/src/main/java/com/khammami/imerolium/BasicApplication.java b/app/src/main/java/com/khammami/imerolium/BasicApplication.java new file mode 100644 index 0000000..b2363c1 --- /dev/null +++ b/app/src/main/java/com/khammami/imerolium/BasicApplication.java @@ -0,0 +1,43 @@ +package com.khammami.imerolium; + +import android.app.Application; +import android.support.multidex.MultiDexApplication; + +import com.crashlytics.android.Crashlytics; +import com.google.firebase.firestore.FirebaseFirestore; +import com.google.firebase.firestore.FirebaseFirestoreSettings; +import com.khammami.imerolium.data.AppRepository; +import com.khammami.imerolium.data.db.AppDatabase; +import com.khammami.imerolium.data.net.AppNetworkDataSource; + +import io.fabric.sdk.android.Fabric; + +public class BasicApplication extends MultiDexApplication { + private AppExecutors mAppExecutors; + + @Override + public void onCreate() { + super.onCreate(); + + //init fabric + final Fabric fabric = new Fabric.Builder(this) + .kits(new Crashlytics()) + .debuggable(true) + .build(); + Fabric.with(fabric); + + mAppExecutors = AppExecutors.getInstance(); + } + + public AppDatabase getDatabase() { + return AppDatabase.getInstance(this); + } + + public AppNetworkDataSource getNetworkResouce() { + return AppNetworkDataSource.getInstance(this, mAppExecutors); + } + + public AppRepository getRepository() { + return AppRepository.getInstance(getDatabase(), getNetworkResouce(), mAppExecutors); + } +} diff --git a/app/src/main/java/com/khammami/imerolium/data/AppPreferences.java b/app/src/main/java/com/khammami/imerolium/data/AppPreferences.java new file mode 100644 index 0000000..5b451e0 --- /dev/null +++ b/app/src/main/java/com/khammami/imerolium/data/AppPreferences.java @@ -0,0 +1,55 @@ +package com.khammami.imerolium.data; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.util.Log; + +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseUser; +import com.khammami.imerolium.R; + +public class AppPreferences { + private static final String TAG = AppPreferences.class.getSimpleName(); + + public static final String AGENDA_VIEW = "agenda"; + public static final String QUILT_VIEW = "quilt"; + + public static void setPostListViewType(Context context, String viewType) { + SharedPreferences sp = getUserSharePreferences(context); + SharedPreferences.Editor editor = sp.edit(); + + editor.putString(context.getString(R.string.pref_post_list_view_type_key), viewType); + editor.apply(); + } + + public static String getPostListViewType(Context context) { + SharedPreferences sp = getUserSharePreferences(context); + return sp.getString(context.getString(R.string.pref_post_list_view_type_key), + QUILT_VIEW); + } + + private static SharedPreferences getUserSharePreferences(Context context){ + String userId = FirebaseAuth.getInstance().getUid(); + return context.getSharedPreferences(userId, Context.MODE_PRIVATE); + + } + + public static void setUserPhotoCover(Context context, String photoUrl) { + SharedPreferences sp = getUserSharePreferences(context); + SharedPreferences.Editor editor = sp.edit(); + + editor.putString(context.getString(R.string.pref_user_cover_key), photoUrl); + editor.apply(); + } + + public static String getUserPhotoCover(Context context) { + SharedPreferences sp = getUserSharePreferences(context); + return sp.getString(context.getString(R.string.pref_user_cover_key), ""); + } + + public static boolean isTimeFormat24(Context context) { + SharedPreferences sp = getUserSharePreferences(context); + return sp.getBoolean(context.getString(R.string.pref_time_format_24h_key), false); + } +} diff --git a/app/src/main/java/com/khammami/imerolium/data/AppRepository.java b/app/src/main/java/com/khammami/imerolium/data/AppRepository.java new file mode 100644 index 0000000..53b2025 --- /dev/null +++ b/app/src/main/java/com/khammami/imerolium/data/AppRepository.java @@ -0,0 +1,226 @@ +package com.khammami.imerolium.data; + +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.MutableLiveData; +import android.arch.lifecycle.Observer; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; + +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.OnFailureListener; +import com.google.android.gms.tasks.Task; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.firestore.CollectionReference; +import com.google.firebase.firestore.DocumentReference; +import com.google.firebase.firestore.FirebaseFirestore; +import com.google.firebase.firestore.QuerySnapshot; +import com.google.firebase.firestore.WriteBatch; +import com.khammami.imerolium.AppExecutors; +import com.khammami.imerolium.data.db.AppDatabase; +import com.khammami.imerolium.data.net.AppNetworkDataSource; +import com.khammami.imerolium.model.Post; +import com.khammami.imerolium.model.SyncPostTask; +import com.khammami.imerolium.utilities.AppUtils; + +import java.util.ArrayList; +import java.util.List; + +public class AppRepository { + private static final String TAG = AppRepository.class.getSimpleName(); + private static final String USERS_PATH = "users"; + private static final String POSTS_PATH = "posts"; + + private static final Object LOCK = new Object(); + private static AppRepository sInstance; + private final AppExecutors mExecutors; + private final AppDatabase db; + private final AppNetworkDataSource networkResource; + private final FirebaseFirestore mFirestoreDb; + + private final MutableLiveData> remotePosts = new MutableLiveData<>(); + + private AppRepository(AppDatabase database, AppNetworkDataSource networkResource, AppExecutors executors) { + this.db = database; + this.networkResource = networkResource; + this.mExecutors = executors; + + //firestore init + this.mFirestoreDb = FirebaseFirestore.getInstance(); +} + + public synchronized static AppRepository getInstance( + AppDatabase database, AppNetworkDataSource networkSource, AppExecutors executors) { + Log.d(TAG, "Getting the repository"); + if (sInstance == null) { + synchronized (LOCK) { + sInstance = new AppRepository(database, networkSource, executors); + Log.d(TAG, "Made new repository"); + } + } + return sInstance; + } + + public LiveData> getUserPosts() { + return db.postDao().getUserPostList(getCurrentUserId()); + } + + public void insertPost(final Post post){ + + mExecutors.diskIO().execute(new Runnable() { + @Override + public void run() { + //set post id from firestore + String postId = getNewFireStoreId(post); + post.setId(postId); + + //local + db.postDao().insertPost(post); + + //remote + mFirestoreDb.collection(USERS_PATH).document(post.getUserId()) + .collection(POSTS_PATH).document(postId).set(post) + .addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception e) { + e.printStackTrace(); + db.syncPostDao().insertSyncPostTask( + AppUtils.createSyncPostTask(post, SyncPostTask.DELETE_ACTION)); + } + }); + } + }); + } + + public LiveData getPost(String postId) { + return db.postDao().getPost(postId); + } + + public void updatePost(final Post post) { + mExecutors.diskIO().execute(new Runnable() { + @Override + public void run() { + //local + db.postDao().updatePost(post); + + //remote + getFirestorePostsPath(post.getUserId()).document(post.getId()) + .set(post).addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception e) { + e.printStackTrace(); + db.syncPostDao().insertSyncPostTask( + AppUtils.createSyncPostTask(post, SyncPostTask.SET_ACTION)); + } + }); + } + }); + + } + + public void deletePost(final Post post) { + mExecutors.diskIO().execute(new Runnable() { + @Override + public void run() { + db.postDao().deletePost(post.getId()); + + getFirestorePostsPath(post.getUserId()).document(post.getId()).delete() + .addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception e) { + e.printStackTrace(); + db.syncPostDao().insertSyncPostTask( + AppUtils.createSyncPostTask(post, SyncPostTask.DELETE_ACTION)); + } + }); + } + }); + + } + + public LiveData> fetchPostListFromFirestore(){ + if (getCurrentUserId() != null) { + getFirestorePostsPath(getCurrentUserId()).get() + .addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if (task.isSuccessful()) { + List posts = task.getResult().toObjects(Post.class); + remotePosts.postValue(posts); + } else { + remotePosts.postValue(null); + Log.d(TAG, "Error getting documents: ", task.getException()); + } + } + }); + } + + return remotePosts; + } + + public void updateLocalPost(final List remotePosts) { + mExecutors.diskIO().execute(new Runnable() { + @Override + public void run() { + List localPosts = db.postDao().getPostList(getCurrentUserId()); + //local db empty & remote db contain data + if (remotePosts != null && remotePosts.size() > 0 && localPosts.size() == 0) { + db.postDao().insertPosts(remotePosts); + } + + //remote db empty & local db contain data + if (remotePosts != null && remotePosts.size() == 0 && localPosts.size() > 0) { + ceateFirestoreBatchWrite(localPosts).commit(); + } + + // + if(remotePosts != null && remotePosts.size() > 0 && localPosts.size() > 0){ + //List syncTasks = db.syncPostDao().getSyncPostTasks(getCurrentUserId()); + + List rTmp = new ArrayList<>(remotePosts); + rTmp.removeAll(localPosts); + + //add and update local posts + for (Post p : rTmp){ + Post tmpPost = db.postDao().getPost(p.getId()).getValue(); + if (tmpPost == null){ + db.postDao().insertPost(p); + } else if (p.getUpdatedAt().after(tmpPost.getUpdatedAt())){ + db.postDao().updatePost(p); + } + } + + //batch posts to firestore & firestore rules will take care of add/update data + localPosts.removeAll(remotePosts); + ceateFirestoreBatchWrite(localPosts).commit(); + } + + } + }); + } + + private String getNewFireStoreId(Post post){ + return mFirestoreDb.collection(USERS_PATH).document(post.getUserId()) + .collection(POSTS_PATH) + .document().getId(); + } + + private CollectionReference getFirestorePostsPath(String userId){ + return mFirestoreDb.collection(USERS_PATH).document(userId) + .collection(POSTS_PATH); + } + + private String getCurrentUserId(){ + return FirebaseAuth.getInstance().getUid(); + } + + private WriteBatch ceateFirestoreBatchWrite(List posts){ + WriteBatch batch = mFirestoreDb.batch(); + for (Post p: posts){ + DocumentReference pRef = getFirestorePostsPath(getCurrentUserId()) + .document(p.getId()); + batch.set(pRef, p); + } + return batch; + } +} diff --git a/app/src/main/java/com/khammami/imerolium/data/db/AppDatabase.java b/app/src/main/java/com/khammami/imerolium/data/db/AppDatabase.java new file mode 100644 index 0000000..d11e16e --- /dev/null +++ b/app/src/main/java/com/khammami/imerolium/data/db/AppDatabase.java @@ -0,0 +1,42 @@ +package com.khammami.imerolium.data.db; + +import android.arch.persistence.room.Database; +import android.arch.persistence.room.Room; +import android.arch.persistence.room.RoomDatabase; +import android.arch.persistence.room.TypeConverters; +import android.content.Context; +import android.util.Log; + +import com.khammami.imerolium.model.Label; +import com.khammami.imerolium.model.Post; +import com.khammami.imerolium.model.SyncPostTask; + +@Database(entities = {Post.class, Label.class, SyncPostTask.class}, version = 1, exportSchema = false) +@TypeConverters(DateConverter.class) +public abstract class AppDatabase extends RoomDatabase { + + private static final String LOG_TAG = AppDatabase.class.getSimpleName(); + private static final String DATABASE_NAME = "journal_app"; + + // For Singleton instantiation + private static final Object LOCK = new Object(); + private static AppDatabase sInstance; + + public static AppDatabase getInstance(Context context) { + Log.d(LOG_TAG, "Getting the database"); + if (sInstance == null) { + synchronized (LOCK) { + sInstance = Room.databaseBuilder(context.getApplicationContext(), + AppDatabase.class, AppDatabase.DATABASE_NAME).build(); + Log.d(LOG_TAG, "Made new database"); + } + } + return sInstance; + } + + public abstract PostDao postDao(); + + public abstract LabelDao labelDao(); + + public abstract SyncPostTaskDao syncPostDao(); +} diff --git a/app/src/main/java/com/khammami/imerolium/data/db/DateConverter.java b/app/src/main/java/com/khammami/imerolium/data/db/DateConverter.java new file mode 100644 index 0000000..411e7ea --- /dev/null +++ b/app/src/main/java/com/khammami/imerolium/data/db/DateConverter.java @@ -0,0 +1,17 @@ +package com.khammami.imerolium.data.db; + +import android.arch.persistence.room.TypeConverter; + +import java.util.Date; + +public class DateConverter { + @TypeConverter + public static Date toDate(Long timestamp) { + return timestamp == null ? null : new Date(timestamp); + } + + @TypeConverter + public static Long toTimestamp(Date date) { + return date == null ? null : date.getTime(); + } +} diff --git a/app/src/main/java/com/khammami/imerolium/data/db/LabelDao.java b/app/src/main/java/com/khammami/imerolium/data/db/LabelDao.java new file mode 100644 index 0000000..047e4a1 --- /dev/null +++ b/app/src/main/java/com/khammami/imerolium/data/db/LabelDao.java @@ -0,0 +1,30 @@ +package com.khammami.imerolium.data.db; + +import android.arch.lifecycle.LiveData; +import android.arch.persistence.room.Dao; +import android.arch.persistence.room.Insert; +import android.arch.persistence.room.OnConflictStrategy; +import android.arch.persistence.room.Query; +import android.arch.persistence.room.Update; + +import com.khammami.imerolium.model.Label; + +import java.util.List; + +@Dao +public interface LabelDao { + @Query("SELECT * FROM labels WHERE userId = :userId") + LiveData> getUserLabelList(String userId); + + @Query("SELECT * FROM labels WHERE id = :labelId") + LiveData