diff --git a/app/build.gradle b/app/build.gradle index 49d9651a..b74c53c2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -133,7 +133,8 @@ dependencies { compile "com.android.support:cardview-v7:$SUPPORT_LIBRARY_VERSION" compile "com.android.support:support-annotations:$SUPPORT_LIBRARY_VERSION" compile "com.android.support:design:$SUPPORT_LIBRARY_VERSION" - compile 'com.android.support.constraint:constraint-layout:1.0.2' + compile "com.android.support:customtabs:$SUPPORT_LIBRARY_VERSION" + compile 'com.android.support.constraint:constraint-layout:1.1.0-beta5' compile "com.squareup.retrofit2:retrofit:$RETROFIT_VERSION" compile "com.squareup.retrofit2:converter-gson:$RETROFIT_VERSION" diff --git a/app/src/commonTest/java/org/schulcloud/mobile/test/common/injection/module/ApplicationTestModule.java b/app/src/commonTest/java/org/schulcloud/mobile/test/common/injection/module/ApplicationTestModule.java index 1ba83ca4..d378d897 100644 --- a/app/src/commonTest/java/org/schulcloud/mobile/test/common/injection/module/ApplicationTestModule.java +++ b/app/src/commonTest/java/org/schulcloud/mobile/test/common/injection/module/ApplicationTestModule.java @@ -3,7 +3,7 @@ import android.app.Application; import android.content.Context; -import javax.inject.Singleton; +import com.google.gson.Gson; import org.schulcloud.mobile.data.datamanagers.CourseDataManager; import org.schulcloud.mobile.data.datamanagers.EventDataManager; @@ -17,6 +17,9 @@ import org.schulcloud.mobile.data.datamanagers.UserDataManager; import org.schulcloud.mobile.data.remote.RestService; import org.schulcloud.mobile.injection.ApplicationContext; + +import javax.inject.Singleton; + import dagger.Module; import dagger.Provides; import io.realm.Realm; @@ -54,6 +57,12 @@ Realm provideRealm() { /************* MOCKS *************/ + @Provides + @Singleton + Gson provideGson() { + return mock(Gson.class); + } + @Provides @Singleton UserDataManager provideUserDataManager() { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d002e069..c3b6d67e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ - @@ -33,7 +34,34 @@ + android:label="@string/app_name" + android:launchMode="singleTask"> + + + + + + + + + + + + + + + + + + + + + + + + + + syncCourses() { return mRestService.getCourses(userDataManager.getAccessToken()) - .concatMap(new Func1, Observable>() { - @Override - public Observable call(FeathersResponse courses) { - mDatabaseHelper.clearTable(Course.class); - return mDatabaseHelper.setCourses(courses.data); - } + .concatMap(courses -> { + mDatabaseHelper.clearTable(Course.class); + return mDatabaseHelper.setCourses(courses.data); }) .doOnError(Throwable::printStackTrace); } public Observable> getCourses() { - return mDatabaseHelper.getCourses().distinct(); + return mDatabaseHelper.getCourses(); } public Course getCourseForId(String courseId) { diff --git a/app/src/main/java/org/schulcloud/mobile/data/datamanagers/FileDataManager.java b/app/src/main/java/org/schulcloud/mobile/data/datamanagers/FileDataManager.java index 79ca2e64..e8122fcd 100644 --- a/app/src/main/java/org/schulcloud/mobile/data/datamanagers/FileDataManager.java +++ b/app/src/main/java/org/schulcloud/mobile/data/datamanagers/FileDataManager.java @@ -1,18 +1,22 @@ package org.schulcloud.mobile.data.datamanagers; import android.support.annotation.NonNull; -import org.schulcloud.mobile.data.local.FileStorageDatabasehelper; +import android.support.annotation.Nullable; +import android.util.Log; + +import org.schulcloud.mobile.data.local.FileStorageDatabaseHelper; import org.schulcloud.mobile.data.local.PreferencesHelper; import org.schulcloud.mobile.data.model.Directory; import org.schulcloud.mobile.data.model.File; import org.schulcloud.mobile.data.model.requestBodies.CreateDirectoryRequest; import org.schulcloud.mobile.data.model.requestBodies.SignedUrlRequest; -import org.schulcloud.mobile.data.model.responseBodies.FilesResponse; import org.schulcloud.mobile.data.model.responseBodies.SignedUrlResponse; import org.schulcloud.mobile.data.remote.RestService; import org.schulcloud.mobile.util.PathUtil; +import org.schulcloud.mobile.util.WebUtil; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import javax.inject.Inject; @@ -22,114 +26,79 @@ import okhttp3.RequestBody; import okhttp3.ResponseBody; import rx.Observable; -import rx.functions.Func1; @Singleton public class FileDataManager { - private final RestService mRestService; - private final FileStorageDatabasehelper mDatabaseHelper; + private static final String TAG = FileDataManager.class.getSimpleName(); - public static final String FILES_CONTEXT_MY = "users"; - public static final String FILES_CONTEXT_COURSES = "courses"; + private final RestService mRestService; + private final FileStorageDatabaseHelper mDatabaseHelper; + private final PreferencesHelper mPreferencesHelper; + private final UserDataManager mUserDataManager; - @Inject - PreferencesHelper mPreferencesHelper; - @Inject - UserDataManager userDataManager; + public static final String CONTEXT_MY = WebUtil.PATH_INTERNAL_FILES_MY; + private static final String CONTEXT_MY_API = "users"; + public static final String CONTEXT_COURSES = "courses"; @Inject public FileDataManager(RestService restService, PreferencesHelper preferencesHelper, - FileStorageDatabasehelper databaseHelper) { + FileStorageDatabaseHelper databaseHelper, UserDataManager userDataManager) { mRestService = restService; mPreferencesHelper = preferencesHelper; mDatabaseHelper = databaseHelper; + mUserDataManager = userDataManager; } - public PreferencesHelper getPreferencesHelper() { - return mPreferencesHelper; - } - - public Observable syncFiles(String path) { - return mRestService.getFiles(userDataManager.getAccessToken(), path) - .concatMap(new Func1>() { - @Override - public Observable call(FilesResponse filesResponse) { - // clear old files - mDatabaseHelper.clearTable(File.class); - - List files = new ArrayList<>(); - - // set fullPath for every file - for (File file : filesResponse.files) { - file.fullPath = file.key.substring(0, file.key.lastIndexOf(java.io.File.separator)); - files.add(file); - } - - return mDatabaseHelper.setFiles(files); - } - }).doOnError(Throwable::printStackTrace); - } - - public Observable> getFiles() { - return mDatabaseHelper.getFiles().distinct().concatMap(files -> { - List filteredFiles = new ArrayList(); - String currentContext = getCurrentStorageContext(); - // remove last trailing slash - if (!currentContext.equals("/") && currentContext.endsWith("/")) { - currentContext = currentContext.substring(0, currentContext.length() - 1); - } - - for (File f : files) { - if (f.fullPath.equals(currentContext)) filteredFiles.add(f); - } - return Observable.just(filteredFiles); - }); - } - - public Observable syncDirectories(String path) { - return mRestService.getFiles(userDataManager.getAccessToken(), path) - .concatMap(new Func1>() { - @Override - public Observable call(FilesResponse filesResponse) { - // clear old directories - mDatabaseHelper.clearTable(Directory.class); - - List improvedDirs = new ArrayList(); - for(Directory d : filesResponse.directories) { - d.path = getCurrentStorageContext(); - improvedDirs.add(d); - } - - return mDatabaseHelper.setDirectories(improvedDirs); + // Files + @NonNull + public Observable syncFiles(@NonNull String path) { + return mRestService.getFiles(mUserDataManager.getAccessToken(), path) + .concatMap(filesResponse -> { + // clear old files + mDatabaseHelper.clearTable(File.class); + + List files = new ArrayList<>(); + + // set fullPath for every file + for (File file : filesResponse.files) { + file.fullPath = + file.key.substring(0, file.key.lastIndexOf(java.io.File.separator)); + files.add(file); } - }).doOnError(Throwable::printStackTrace); + return mDatabaseHelper.setFiles(files); + }).doOnError(throwable -> Log.w(TAG, "Error syncing files", throwable)); } + @NonNull + public Observable> getFiles() { + return mDatabaseHelper.getFiles() + .map(files -> { + List filteredFiles = new ArrayList<>(); + String currentContext = PathUtil.trimTrailingSlash(getStorageContext()); + for (File f : files) + if (f.fullPath.equals(currentContext)) + filteredFiles.add(f); - public Observable> getDirectories() { - return mDatabaseHelper.getDirectories().distinct().concatMap(directories -> { - List filteredDirectories = new ArrayList(); - for (Directory d : directories) { - if (d.path.equals(getCurrentStorageContext())) filteredDirectories.add(d); - } - return Observable.just(filteredDirectories); - }); - } - - public Observable deleteDirectory(String path) { - return mRestService.deleteDirectory(userDataManager.getAccessToken(), path); + Collections.sort(filteredFiles, (o1, o2) -> + o1.name == null ? (o2.name == null ? 0 : -1) + : o1.name.compareTo(o2.name)); + return filteredFiles; + }); } - public Observable getFileUrl(SignedUrlRequest signedUrlRequest) { - return mRestService.generateSignedUrl(userDataManager.getAccessToken(), signedUrlRequest); + @NonNull + public Observable getFileUrl(@NonNull SignedUrlRequest signedUrlRequest) { + return mRestService.generateSignedUrl(mUserDataManager.getAccessToken(), signedUrlRequest); } - - public Observable downloadFile(String url) { + @NonNull + public Observable downloadFile(@NonNull String url) { return mRestService.downloadFile(url); } - public Observable uploadFile(java.io.File file, SignedUrlResponse signedUrlResponse) { - RequestBody requestBody = RequestBody.create(MediaType.parse("file/*"), file); + @NonNull + public Observable uploadFile(@NonNull java.io.File file, + @NonNull SignedUrlResponse signedUrlResponse) { + RequestBody requestBody = RequestBody.create(MediaType.parse("file/*"), file); return mRestService.uploadFile( signedUrlResponse.url, signedUrlResponse.header.getContentType(), @@ -140,24 +109,9 @@ public Observable uploadFile(java.io.File file, SignedUrlResponse requestBody ); } - - public Observable deleteFile(String path) { - return mRestService.deleteFile(userDataManager.getAccessToken(), path); - } - - public String getCurrentStorageContext() { - String storageContext = mPreferencesHelper.getCurrentStorageContext(); - // personal files are default - return storageContext.equals("null") ? "users/" + userDataManager.getCurrentUserId() + "/" : storageContext + "/"; - } - - public void setCurrentStorageContext(String newStorageContext) { - mPreferencesHelper.saveCurrentStorageContext(newStorageContext); - } - @NonNull public Observable persistFile(@NonNull SignedUrlResponse signedUrl, - @NonNull String fileName, @NonNull String fileType, long fileSize) { + @NonNull String fileName, @NonNull String fileType, long fileSize) { File newFile = new File(); newFile.key = PathUtil.combine(signedUrl.header.getMetaPath(), fileName); newFile.path = PathUtil.ensureTrailingSlash(signedUrl.header.getMetaPath()); @@ -166,22 +120,93 @@ public Observable persistFile(@NonNull SignedUrlResponse signedUrl newFile.size = "" + fileSize; newFile.flatFileName = signedUrl.header.getMetaFlatName(); newFile.thumbnail = signedUrl.header.getMetaThumbnail(); - return mRestService.persistFile(userDataManager.getAccessToken(), newFile); + return mRestService.persistFile(mUserDataManager.getAccessToken(), newFile); } - public void setCurrentStorageContextToRoot() { - setCurrentStorageContext(""); + @NonNull + public Observable deleteFile(@NonNull String path) { + return mRestService.deleteFile(mUserDataManager.getAccessToken(), path); } - public void setCurrentStorageContextToMy() { - setCurrentStorageContext(FILES_CONTEXT_MY + "/" + userDataManager.getCurrentUserId()); + + // Directories + @NonNull + public Observable syncDirectories(@NonNull String path) { + return mRestService.getFiles(mUserDataManager.getAccessToken(), path) + .concatMap(filesResponse -> { + // clear old directories + mDatabaseHelper.clearTable(Directory.class); + + List improvedDirs = new ArrayList<>(); + for (Directory d : filesResponse.directories) { + d.path = getStorageContext(); + improvedDirs.add(d); + } + + return mDatabaseHelper.setDirectories(improvedDirs); + }).doOnError(throwable -> Log.w(TAG, "Error syncing directories", throwable)); } - public void setCurrentStorageContextToCourse(@NonNull String courseId) { - setCurrentStorageContext(FILES_CONTEXT_COURSES + "/" + courseId); + @NonNull + public Observable> getDirectories() { + return mDatabaseHelper.getDirectories() + .map(directories -> { + List filteredDirectories = new ArrayList<>(); + String currentContext = getStorageContext(); + for (Directory d : directories) + if (d.path.equals(currentContext)) + filteredDirectories.add(d); + + Collections.sort(filteredDirectories, (o1, o2) -> + o1.name == null ? (o2.name == null ? 0 : -1) + : o1.name.compareTo(o2.name)); + return filteredDirectories; + }); } @NonNull public Observable createDirectory( @NonNull CreateDirectoryRequest createDirectoryRequest) { - return mRestService.createDirectory(userDataManager.getAccessToken(), createDirectoryRequest); + return mRestService + .createDirectory(mUserDataManager.getAccessToken(), createDirectoryRequest); + } + + @NonNull + public Observable deleteDirectory(@NonNull String path) { + return mRestService.deleteDirectory(mUserDataManager.getAccessToken(), path); + } + + // Storage context + @NonNull + public String getStorageContext() { + String storageContext = mPreferencesHelper.getCurrentStorageContext(); + // personal files are default + return PathUtil.ensureTrailingSlash(storageContext.equals("null") + ? PathUtil.combine("users", mUserDataManager.getCurrentUserId()) + : storageContext); + } + public void setStorageContext(@NonNull String storageContext) { + storageContext = PathUtil.trimSlashes(storageContext); + if (storageContext.startsWith(CONTEXT_MY)) + storageContext = PathUtil.combine(CONTEXT_MY_API, mUserDataManager.getCurrentUserId(), + storageContext.substring(CONTEXT_MY.length())); + mPreferencesHelper.saveCurrentStorageContext(storageContext); + } + public void setStorageContextToRoot() { + setStorageContext(""); + } + public boolean isStorageContextMy() { + return getStorageContext().startsWith(CONTEXT_MY_API); + } + public void setStorageContextToMy() { + setStorageContext(CONTEXT_MY); + } + @Nullable + public String isStorageContextCourse() { + String storageContext = getStorageContext(); + if (!storageContext.startsWith(CONTEXT_COURSES)) + return null; + return PathUtil.getAllParts(storageContext, 3)[1]; + } + public void setCurrentStorageContextToCourse(@NonNull String courseId) { + setStorageContext(PathUtil.combine(CONTEXT_COURSES, courseId)); } } diff --git a/app/src/main/java/org/schulcloud/mobile/data/datamanagers/TopicDataManager.java b/app/src/main/java/org/schulcloud/mobile/data/datamanagers/TopicDataManager.java index ff28e3b3..81021a3c 100644 --- a/app/src/main/java/org/schulcloud/mobile/data/datamanagers/TopicDataManager.java +++ b/app/src/main/java/org/schulcloud/mobile/data/datamanagers/TopicDataManager.java @@ -1,10 +1,11 @@ package org.schulcloud.mobile.data.datamanagers; import android.support.annotation.NonNull; +import android.util.Log; + import org.schulcloud.mobile.data.local.PreferencesHelper; import org.schulcloud.mobile.data.local.TopicsDatabaseHelper; import org.schulcloud.mobile.data.model.Topic; -import org.schulcloud.mobile.data.model.responseBodies.FeathersResponse; import org.schulcloud.mobile.data.remote.RestService; import java.util.List; @@ -13,47 +14,50 @@ import javax.inject.Singleton; import rx.Observable; -import rx.functions.Func1; +import rx.Single; @Singleton public class TopicDataManager { + private static final String TAG = TopicDataManager.class.getSimpleName(); + private final RestService mRestService; private final TopicsDatabaseHelper mDatabaseHelper; - - @Inject - PreferencesHelper mPreferencesHelper; - @Inject - UserDataManager userDataManager; + private final PreferencesHelper mPreferencesHelper; + private final UserDataManager mUserDataManager; @Inject public TopicDataManager(RestService restService, PreferencesHelper preferencesHelper, - TopicsDatabaseHelper databaseHelper) { + TopicsDatabaseHelper databaseHelper, UserDataManager userDataManager) { mRestService = restService; mPreferencesHelper = preferencesHelper; mDatabaseHelper = databaseHelper; + mUserDataManager = userDataManager; } + @NonNull public PreferencesHelper getPreferencesHelper() { return mPreferencesHelper; } - public Observable syncTopics(String courseId) { - return mRestService.getTopics(userDataManager.getAccessToken(), courseId) - .concatMap(new Func1, Observable>() { - @Override - public Observable call(FeathersResponse topics) { - mDatabaseHelper.clearTable(Topic.class); - return mDatabaseHelper.setTopics(topics.data); - } - }) - .doOnError(Throwable::printStackTrace); + @NonNull + public Observable syncTopics(@NonNull String courseId) { + return mRestService.getTopics(mUserDataManager.getAccessToken(), courseId) + .concatMap(topics -> mDatabaseHelper.setTopics(courseId, topics.data)) + .doOnError(throwable -> + Log.w(TAG, "Error syncing topics for course " + courseId, throwable)); } - public Observable> getTopics() { - return mDatabaseHelper.getTopics().distinctUntilChanged(); + @NonNull + public Observable> getTopics(@NonNull String courseId) { + return mDatabaseHelper.getTopics(courseId); } - - public Topic getTopicForId(@NonNull String topicId) { - return mDatabaseHelper.getTopicForId(topicId); + @NonNull + public Single getTopic(@NonNull String topicId) { + Topic topic = mDatabaseHelper.getTopicForId(topicId); + if (topic != null) + return Single.just(topic); + + return mRestService.getTopic(mUserDataManager.getAccessToken(), topicId) + .doOnSuccess(mDatabaseHelper::setTopic); } } diff --git a/app/src/main/java/org/schulcloud/mobile/data/local/BaseDatabaseHelper.java b/app/src/main/java/org/schulcloud/mobile/data/local/BaseDatabaseHelper.java index 27885fe6..8116eaa7 100644 --- a/app/src/main/java/org/schulcloud/mobile/data/local/BaseDatabaseHelper.java +++ b/app/src/main/java/org/schulcloud/mobile/data/local/BaseDatabaseHelper.java @@ -1,10 +1,13 @@ package org.schulcloud.mobile.data.local; +import android.support.annotation.NonNull; + import javax.inject.Inject; import javax.inject.Provider; import javax.inject.Singleton; import io.realm.Realm; +import io.realm.RealmModel; @Singleton public class BaseDatabaseHelper { @@ -15,9 +18,8 @@ public class BaseDatabaseHelper { mRealmProvider = realmProvider; } - public void clearTable(Class table) { - final Realm realm = mRealmProvider.get(); - realm.executeTransaction(realm1 -> realm1.delete(table)); + public void clearTable(@NonNull Class table) { + mRealmProvider.get().executeTransaction(realm -> realm.delete(table)); } public void clearAll() { mRealmProvider.get().executeTransaction(realm -> realm.deleteAll()); diff --git a/app/src/main/java/org/schulcloud/mobile/data/local/CourseDatabaseHelper.java b/app/src/main/java/org/schulcloud/mobile/data/local/CourseDatabaseHelper.java index a5804763..38f96c5d 100644 --- a/app/src/main/java/org/schulcloud/mobile/data/local/CourseDatabaseHelper.java +++ b/app/src/main/java/org/schulcloud/mobile/data/local/CourseDatabaseHelper.java @@ -1,5 +1,7 @@ package org.schulcloud.mobile.data.local; +import android.support.annotation.NonNull; + import org.schulcloud.mobile.data.model.Course; import java.util.Collection; @@ -10,13 +12,16 @@ import javax.inject.Singleton; import io.realm.Realm; +import io.realm.RealmResults; import rx.Observable; import timber.log.Timber; @Singleton public class CourseDatabaseHelper extends BaseDatabaseHelper { @Inject - CourseDatabaseHelper(Provider realmProvider) {super(realmProvider);} + CourseDatabaseHelper(Provider realmProvider) { + super(realmProvider); + } public Observable setCourses(final Collection newCourse) { return Observable.create(subscriber -> { @@ -38,15 +43,16 @@ public Observable setCourses(final Collection newCourse) { }); } + @NonNull public Observable> getCourses() { - final Realm realm = mRealmProvider.get(); + Realm realm = mRealmProvider.get(); return realm.where(Course.class).findAllAsync().asObservable() - .filter(course -> course.isLoaded()) - .map(course -> realm.copyFromRealm(course)); + .filter(RealmResults::isLoaded) + .map(realm::copyFromRealm); } public Course getCourseForId(String courseId) { - final Realm realm = mRealmProvider.get(); - return realm.where(Course.class).equalTo("_id", courseId).findFirst(); + Realm realm = mRealmProvider.get(); + return realm.copyFromRealm(realm.where(Course.class).equalTo("_id", courseId).findFirst()); } } diff --git a/app/src/main/java/org/schulcloud/mobile/data/local/FileStorageDatabasehelper.java b/app/src/main/java/org/schulcloud/mobile/data/local/FileStorageDatabaseHelper.java similarity index 72% rename from app/src/main/java/org/schulcloud/mobile/data/local/FileStorageDatabasehelper.java rename to app/src/main/java/org/schulcloud/mobile/data/local/FileStorageDatabaseHelper.java index 3e195271..3073f5ce 100644 --- a/app/src/main/java/org/schulcloud/mobile/data/local/FileStorageDatabasehelper.java +++ b/app/src/main/java/org/schulcloud/mobile/data/local/FileStorageDatabaseHelper.java @@ -1,5 +1,7 @@ package org.schulcloud.mobile.data.local; +import android.support.annotation.NonNull; + import org.schulcloud.mobile.data.model.Directory; import org.schulcloud.mobile.data.model.File; @@ -10,15 +12,26 @@ import javax.inject.Provider; import io.realm.Realm; +import io.realm.RealmResults; import rx.Observable; import timber.log.Timber; -public class FileStorageDatabasehelper extends BaseDatabaseHelper{ +public class FileStorageDatabaseHelper extends BaseDatabaseHelper { @Inject - FileStorageDatabasehelper(Provider realmProvider) {super(realmProvider);} + FileStorageDatabaseHelper(Provider realmProvider) { + super(realmProvider); + } - public Observable setFiles(final Collection files) { + @NonNull + public Observable> getFiles() { + Realm realm = mRealmProvider.get(); + return realm.where(File.class).findAllAsync().asObservable() + .filter(RealmResults::isLoaded) + .map(realm::copyFromRealm); + } + @NonNull + public Observable setFiles(@NonNull Collection files) { return Observable.create(subscriber -> { if (subscriber.isUnsubscribed()) return; @@ -39,14 +52,15 @@ public Observable setFiles(final Collection files) { }); } - public Observable> getFiles() { - final Realm realm = mRealmProvider.get(); - return realm.where(File.class).findAllAsync().asObservable() - .filter(files -> files.isLoaded()) - .map(files -> realm.copyFromRealm(files)); + @NonNull + public Observable> getDirectories() { + Realm realm = mRealmProvider.get(); + return realm.where(Directory.class).findAllAsync().asObservable() + .filter(RealmResults::isLoaded) + .map(realm::copyFromRealm); } - - public Observable setDirectories(final Collection directories) { + @NonNull + public Observable setDirectories(@NonNull Collection directories) { return Observable.create(subscriber -> { if (subscriber.isUnsubscribed()) return; @@ -66,11 +80,4 @@ public Observable setDirectories(final Collection director } }); } - - public Observable> getDirectories() { - final Realm realm = mRealmProvider.get(); - return realm.where(Directory.class).findAllAsync().asObservable() - .filter(directories -> directories.isLoaded()) - .map(directories -> realm.copyFromRealm(directories)); - } } diff --git a/app/src/main/java/org/schulcloud/mobile/data/local/TopicsDatabaseHelper.java b/app/src/main/java/org/schulcloud/mobile/data/local/TopicsDatabaseHelper.java index 3ec663b2..7870843d 100644 --- a/app/src/main/java/org/schulcloud/mobile/data/local/TopicsDatabaseHelper.java +++ b/app/src/main/java/org/schulcloud/mobile/data/local/TopicsDatabaseHelper.java @@ -1,5 +1,7 @@ package org.schulcloud.mobile.data.local; +import android.support.annotation.NonNull; + import org.schulcloud.mobile.data.model.Topic; import java.util.Collection; @@ -10,15 +12,20 @@ import javax.inject.Singleton; import io.realm.Realm; +import io.realm.RealmResults; import rx.Observable; import timber.log.Timber; @Singleton -public class TopicsDatabaseHelper extends BaseDatabaseHelper { +public class TopicsDatabaseHelper extends BaseDatabaseHelper { @Inject - TopicsDatabaseHelper(Provider realmProvider) {super(realmProvider);} + TopicsDatabaseHelper(Provider realmProvider) { + super(realmProvider); + } - public Observable setTopics(final Collection newTopic) { + @NonNull + public Observable setTopics(@NonNull String courseId, + @NonNull Collection newTopics) { return Observable.create(subscriber -> { if (subscriber.isUnsubscribed()) return; @@ -26,32 +33,49 @@ public Observable setTopics(final Collection newTopic) { try { realm = mRealmProvider.get(); - realm.executeTransaction(realm1 -> realm1.copyToRealmOrUpdate(newTopic)); + realm.executeTransaction(realm1 -> { + realm1.where(Topic.class).equalTo("courseId", courseId).findAll() + .deleteAllFromRealm(); + realm1.copyToRealmOrUpdate(newTopics); + }); } catch (Exception e) { Timber.e(e, "There was an error while adding in Realm."); subscriber.onError(e); } finally { - if (realm != null) { + if (realm != null) realm.close(); - } } }); } + @NonNull + public Observable setTopic(@NonNull Topic topic) { + return Observable.create(subscriber -> { + if (subscriber.isUnsubscribed()) + return; + Realm realm = null; - public Observable> getTopics() { - final Realm realm = mRealmProvider.get(); - return realm.where(Topic.class).findAllAsync().asObservable() - .filter(topic -> topic.isLoaded()) - .map(topic -> realm.copyFromRealm(topic)); + try { + realm = mRealmProvider.get(); + realm.executeTransaction(realm1 -> realm1.copyToRealm(topic)); + } catch (Exception e) { + Timber.e(e, "There was an error while adding in Realm."); + subscriber.onError(e); + } finally { + if (realm != null) + realm.close(); + } + }); } - public Topic getContents(String topicId) { - final Realm realm = mRealmProvider.get(); - return realm.where(Topic.class).equalTo("_id", topicId).findFirst(); + @NonNull + public Observable> getTopics(@NonNull String courseId) { + Realm realm = mRealmProvider.get(); + return realm.where(Topic.class).equalTo("courseId", courseId).findAllAsync().asObservable() + .filter(RealmResults::isLoaded) + .map(realm::copyFromRealm); } - public Topic getTopicForId(String topicId) { - final Realm realm = mRealmProvider.get(); - return realm.where(Topic.class).equalTo("_id", topicId).findFirst(); + public Topic getTopicForId(@NonNull String topicId) { + return mRealmProvider.get().where(Topic.class).equalTo("_id", topicId).findFirst(); } } diff --git a/app/src/main/java/org/schulcloud/mobile/data/model/Content.java b/app/src/main/java/org/schulcloud/mobile/data/model/Content.java index b2fef14e..29034716 100644 --- a/app/src/main/java/org/schulcloud/mobile/data/model/Content.java +++ b/app/src/main/java/org/schulcloud/mobile/data/model/Content.java @@ -1,8 +1,20 @@ package org.schulcloud.mobile.data.model; +import io.realm.RealmList; import io.realm.RealmObject; public class Content extends RealmObject{ + // text public String text; + + // resources + public RealmList resources; + + // geogebra public String materialId; + + // etherpad, nexboard + public String title; + public String description; + public String url; } diff --git a/app/src/main/java/org/schulcloud/mobile/data/model/Contents.java b/app/src/main/java/org/schulcloud/mobile/data/model/Contents.java index cf2f6ad8..d6f74bf4 100644 --- a/app/src/main/java/org/schulcloud/mobile/data/model/Contents.java +++ b/app/src/main/java/org/schulcloud/mobile/data/model/Contents.java @@ -3,6 +3,12 @@ import io.realm.RealmObject; public class Contents extends RealmObject { + public static final String COMPONENT_TEXT = "text"; + public static final String COMPONENT_RESOURCES = "resources"; + public static final String COMPONENT_GEOGEBRA = "geoGebra"; + public static final String COMPONENT_ETHERPAD = "Etherpad"; + public static final String COMPONENT_NEXBOARD = "neXboard"; + public String component; public String title; public Boolean hidden; diff --git a/app/src/main/java/org/schulcloud/mobile/data/model/Resource.java b/app/src/main/java/org/schulcloud/mobile/data/model/Resource.java new file mode 100644 index 00000000..a4bef4a9 --- /dev/null +++ b/app/src/main/java/org/schulcloud/mobile/data/model/Resource.java @@ -0,0 +1,13 @@ +package org.schulcloud.mobile.data.model; + +import io.realm.RealmObject; + +/** + * Date: 2/17/2018 + */ +public class Resource extends RealmObject { + public String url; + public String client; + public String title; + public String description; +} diff --git a/app/src/main/java/org/schulcloud/mobile/data/model/responseBodies/FeathersResponse.java b/app/src/main/java/org/schulcloud/mobile/data/model/responseBodies/FeathersResponse.java index 1dc3d6d6..0b5e520f 100644 --- a/app/src/main/java/org/schulcloud/mobile/data/model/responseBodies/FeathersResponse.java +++ b/app/src/main/java/org/schulcloud/mobile/data/model/responseBodies/FeathersResponse.java @@ -2,6 +2,6 @@ import java.util.List; -public class FeathersResponse { +public class FeathersResponse { public List data; } diff --git a/app/src/main/java/org/schulcloud/mobile/data/model/responseBodies/GeogebraResponse.java b/app/src/main/java/org/schulcloud/mobile/data/model/responseBodies/GeogebraResponse.java new file mode 100644 index 00000000..9d57e370 --- /dev/null +++ b/app/src/main/java/org/schulcloud/mobile/data/model/responseBodies/GeogebraResponse.java @@ -0,0 +1,20 @@ +package org.schulcloud.mobile.data.model.responseBodies; + +/** + * Date: 2/18/2018 + */ +public class GeogebraResponse { + public Responses responses; + + public static class Responses { + public Response response; + + public static class Response { + public Item item; + + public static class Item { + public String previewUrl; + } + } + } +} diff --git a/app/src/main/java/org/schulcloud/mobile/data/remote/RestService.java b/app/src/main/java/org/schulcloud/mobile/data/remote/RestService.java index d7ecb01d..de0f25dc 100644 --- a/app/src/main/java/org/schulcloud/mobile/data/remote/RestService.java +++ b/app/src/main/java/org/schulcloud/mobile/data/remote/RestService.java @@ -41,6 +41,7 @@ import retrofit2.http.Query; import retrofit2.http.Url; import rx.Observable; +import rx.Single; public interface RestService { @@ -116,6 +117,8 @@ Observable uploadFile( @GET("lessons") Observable> getTopics(@Header("Authorization") String accessToken, @Query("courseId") String courseId); + @GET("lessons/{id}") + Single getTopic(@Header("Authorization") String accessToken, @Path("id") String topicId); @POST("mails") Observable sendFeedback(@Header("Authorization") String accessToken, @Body FeedbackRequest feedbackRequest); diff --git a/app/src/main/java/org/schulcloud/mobile/data/sync/DirectorySyncService.java b/app/src/main/java/org/schulcloud/mobile/data/sync/DirectorySyncService.java index b56dafa7..44ca9ff5 100644 --- a/app/src/main/java/org/schulcloud/mobile/data/sync/DirectorySyncService.java +++ b/app/src/main/java/org/schulcloud/mobile/data/sync/DirectorySyncService.java @@ -53,7 +53,7 @@ public int onStartCommand(Intent intent, int flags, final int startId) { } RxUtil.unsubscribe(mSubscription); - mSubscription = mFileDataManager.syncDirectories(mFileDataManager.getCurrentStorageContext()) + mSubscription = mFileDataManager.syncDirectories(mFileDataManager.getStorageContext()) .subscribeOn(Schedulers.io()) .subscribe(new Observer() { @Override diff --git a/app/src/main/java/org/schulcloud/mobile/data/sync/FileSyncService.java b/app/src/main/java/org/schulcloud/mobile/data/sync/FileSyncService.java index 205d2be5..ffc2f3ec 100644 --- a/app/src/main/java/org/schulcloud/mobile/data/sync/FileSyncService.java +++ b/app/src/main/java/org/schulcloud/mobile/data/sync/FileSyncService.java @@ -53,7 +53,7 @@ public int onStartCommand(Intent intent, int flags, final int startId) { } RxUtil.unsubscribe(mSubscription); - mSubscription = mFileDataManager.syncFiles(mFileDataManager.getCurrentStorageContext()) + mSubscription = mFileDataManager.syncFiles(mFileDataManager.getStorageContext()) .subscribeOn(Schedulers.io()) .subscribe(new Observer() { @Override diff --git a/app/src/main/java/org/schulcloud/mobile/injection/component/ActivityComponent.java b/app/src/main/java/org/schulcloud/mobile/injection/component/ActivityComponent.java index bef8c7f8..6284f6f4 100644 --- a/app/src/main/java/org/schulcloud/mobile/injection/component/ActivityComponent.java +++ b/app/src/main/java/org/schulcloud/mobile/injection/component/ActivityComponent.java @@ -4,6 +4,7 @@ import org.schulcloud.mobile.injection.scope.PerActivity; import org.schulcloud.mobile.ui.courses.CourseFragment; import org.schulcloud.mobile.ui.courses.detailed.DetailedCourseFragment; +import org.schulcloud.mobile.ui.courses.topic.ContentAdapter; import org.schulcloud.mobile.ui.courses.topic.TopicFragment; import org.schulcloud.mobile.ui.dashboard.DashboardFragment; import org.schulcloud.mobile.ui.feedback.FeedbackDialog; @@ -42,26 +43,22 @@ public interface ActivityComponent { // News void inject(NewsFragment newsFragment); - void inject(DetailedNewsFragment detailedNewsFragment); // Course void inject(CourseFragment courseFragment); - void inject(DetailedCourseFragment detailedCourseFragment); - void inject(TopicFragment topicFragment); + void inject(ContentAdapter.ResourcesViewHolder resourcesViewHolder); + // Homework void inject(HomeworkFragment homeworkFragment); - void inject(DetailedHomeworkFragment detailedHomeworkFragment); - void inject(AddHomeworkFragment addHomeworkFragment); // File void inject(FileOverviewFragment fileOverviewFragment); - void inject(FilesFragment filesFragment); // Settings diff --git a/app/src/main/java/org/schulcloud/mobile/injection/component/ApplicationComponent.java b/app/src/main/java/org/schulcloud/mobile/injection/component/ApplicationComponent.java index 53424a5e..8051ca8d 100644 --- a/app/src/main/java/org/schulcloud/mobile/injection/component/ApplicationComponent.java +++ b/app/src/main/java/org/schulcloud/mobile/injection/component/ApplicationComponent.java @@ -3,6 +3,8 @@ import android.app.Application; import android.content.Context; +import com.google.gson.Gson; + import org.schulcloud.mobile.data.datamanagers.CourseDataManager; import org.schulcloud.mobile.data.datamanagers.EventDataManager; import org.schulcloud.mobile.data.datamanagers.FeedbackDataManager; @@ -16,7 +18,7 @@ import org.schulcloud.mobile.data.local.BaseDatabaseHelper; import org.schulcloud.mobile.data.local.CourseDatabaseHelper; import org.schulcloud.mobile.data.local.EventsDatabaseHelper; -import org.schulcloud.mobile.data.local.FileStorageDatabasehelper; +import org.schulcloud.mobile.data.local.FileStorageDatabaseHelper; import org.schulcloud.mobile.data.local.HomeworkDatabaseHelper; import org.schulcloud.mobile.data.local.NewsDatabaseHelper; import org.schulcloud.mobile.data.local.NotificationsDatabaseHelper; @@ -77,6 +79,8 @@ public interface ApplicationComponent { Application application(); + Gson gson(); + RestService restService(); PreferencesHelper preferencesHelper(); @@ -87,7 +91,7 @@ public interface ApplicationComponent { EventsDatabaseHelper eventsDatabaseHelper(); - FileStorageDatabasehelper fileStorageDatabaseHelper(); + FileStorageDatabaseHelper fileStorageDatabaseHelper(); HomeworkDatabaseHelper homeworkDatabseHelper(); diff --git a/app/src/main/java/org/schulcloud/mobile/injection/module/ApplicationModule.java b/app/src/main/java/org/schulcloud/mobile/injection/module/ApplicationModule.java index ced2e3a8..5f5e846b 100644 --- a/app/src/main/java/org/schulcloud/mobile/injection/module/ApplicationModule.java +++ b/app/src/main/java/org/schulcloud/mobile/injection/module/ApplicationModule.java @@ -38,8 +38,8 @@ Context provideContext() { @Singleton RealmConfiguration provideRealmConfiguration(@ApplicationContext Context context) { Realm.init(context); - RealmConfiguration.Builder builder = new RealmConfiguration.Builder(). - deleteRealmIfMigrationNeeded(); + RealmConfiguration.Builder builder = new RealmConfiguration.Builder() + .deleteRealmIfMigrationNeeded(); builder.name("default.realm"); return builder.build(); } diff --git a/app/src/main/java/org/schulcloud/mobile/ui/base/BaseActivity.java b/app/src/main/java/org/schulcloud/mobile/ui/base/BaseActivity.java index dbf8e5b6..1bb4d6be 100644 --- a/app/src/main/java/org/schulcloud/mobile/ui/base/BaseActivity.java +++ b/app/src/main/java/org/schulcloud/mobile/ui/base/BaseActivity.java @@ -40,7 +40,8 @@ public abstract class BaseActivity extends AppCompatActivity implements MvpView { private static final String KEY_ACTIVITY_ID = "KEY_ACTIVITY_ID"; private static final AtomicInteger sNextId = new AtomicInteger(0); - private static final SparseArray sComponentsMap = new SparseArray<>(); + private static final SparseArray sComponentsMap = + new SparseArray<>(); @Inject PreferencesHelper mPreferencesHelper; @@ -66,13 +67,15 @@ protected void onCreate(Bundle savedInstanceState) { : sNextId.getAndIncrement(); ConfigPersistentComponent configPersistentComponent; if (null == sComponentsMap.get(mActivityId)) { - Timber.i("Creating new ConfigPersistentComponent id=%d", mActivityId); + Timber.i("Creating new ConfigPersistentComponent id=%d, class=%s", mActivityId, + getClass().getSimpleName()); configPersistentComponent = DaggerConfigPersistentComponent.builder() .applicationComponent(SchulCloudApplication.get(this).getComponent()) .build(); sComponentsMap.put(mActivityId, configPersistentComponent); } else { - Timber.i("Reusing ConfigPersistentComponent id=%d", mActivityId); + Timber.i("Reusing ConfigPersistentComponent id=%d, class=%s", mActivityId, + getClass().getSimpleName()); configPersistentComponent = sComponentsMap.get(mActivityId); } mActivityComponent = configPersistentComponent.activityComponent(new ActivityModule(this)); diff --git a/app/src/main/java/org/schulcloud/mobile/ui/base/BaseAdapter.java b/app/src/main/java/org/schulcloud/mobile/ui/base/BaseAdapter.java new file mode 100644 index 00000000..d410272c --- /dev/null +++ b/app/src/main/java/org/schulcloud/mobile/ui/base/BaseAdapter.java @@ -0,0 +1,38 @@ +package org.schulcloud.mobile.ui.base; + +import android.support.v7.widget.RecyclerView; + +import org.schulcloud.mobile.ui.main.MainActivity; + +/** + * Date: 2/19/2018 + */ +public abstract class BaseAdapter + extends RecyclerView.Adapter { + private MainActivity mMainActivity; + private RecyclerView mRecyclerView; + + @Override + public void onAttachedToRecyclerView(RecyclerView recyclerView) { + super.onAttachedToRecyclerView(recyclerView); + + mRecyclerView = recyclerView; + if (!(mRecyclerView.getContext() instanceof MainActivity)) + throw new IllegalStateException("BaseAdapter may only be used in MainActivity"); + mMainActivity = (MainActivity) mRecyclerView.getContext(); + } + @Override + public void onDetachedFromRecyclerView(RecyclerView recyclerView) { + super.onDetachedFromRecyclerView(recyclerView); + + mRecyclerView = null; + mMainActivity = null; + } + + public RecyclerView getRecyclerView() { + return mRecyclerView; + } + public MainActivity getMainActivity() { + return mMainActivity; + } +} diff --git a/app/src/main/java/org/schulcloud/mobile/ui/base/BaseDialog.java b/app/src/main/java/org/schulcloud/mobile/ui/base/BaseDialog.java index 2f5a1a97..21938832 100644 --- a/app/src/main/java/org/schulcloud/mobile/ui/base/BaseDialog.java +++ b/app/src/main/java/org/schulcloud/mobile/ui/base/BaseDialog.java @@ -42,15 +42,18 @@ public void onCreate(@Nullable Bundle savedInstanceState) { ConfigPersistentComponent configPersistentComponent = sComponents.get(mActivityId); if (configPersistentComponent == null) { - Timber.i("Creating new ConfigPersistentComponent id=%d", mActivityId); + Timber.i("Creating new ConfigPersistentComponent id=%d, class=%s", mActivityId, + getClass().getSimpleName()); configPersistentComponent = DaggerConfigPersistentComponent.builder() .applicationComponent(SchulCloudApplication.get(getActivity()).getComponent()) .build(); sComponents.put(mActivityId, configPersistentComponent); } else - Timber.i("Reusing ConfigPersistentComponent id=%d", mActivityId); + Timber.i("Reusing ConfigPersistentComponent id=%d, class=%s", mActivityId, + getClass().getSimpleName()); mActivityComponent = configPersistentComponent - .activityComponent(new ActivityModule((BaseActivity) getActivity())); + .activityComponent(new ActivityModule( + (BaseActivity) getActivity())); } protected final void readArguments(@Nullable Bundle savedInstanceState) { if (savedInstanceState == null) diff --git a/app/src/main/java/org/schulcloud/mobile/ui/base/BaseFragment.java b/app/src/main/java/org/schulcloud/mobile/ui/base/BaseFragment.java index db658b8f..cd266eee 100644 --- a/app/src/main/java/org/schulcloud/mobile/ui/base/BaseFragment.java +++ b/app/src/main/java/org/schulcloud/mobile/ui/base/BaseFragment.java @@ -43,15 +43,18 @@ public void onCreate(@Nullable Bundle savedInstanceState) { ConfigPersistentComponent configPersistentComponent = sComponents.get(mActivityId); if (configPersistentComponent == null) { - Timber.i("Creating new ConfigPersistentComponent id=%d", mActivityId); + Timber.i("Creating new ConfigPersistentComponent id=%d, class=%s", mActivityId, + getClass().getSimpleName()); configPersistentComponent = DaggerConfigPersistentComponent.builder() .applicationComponent(SchulCloudApplication.get(getActivity()).getComponent()) .build(); sComponents.put(mActivityId, configPersistentComponent); } else - Timber.i("Reusing ConfigPersistentComponent id=%d", mActivityId); + Timber.i("Reusing ConfigPersistentComponent id=%d, class=%s", mActivityId, + getClass().getSimpleName()); mActivityComponent = configPersistentComponent - .activityComponent(new ActivityModule((BaseActivity) getActivity())); + .activityComponent(new ActivityModule( + (BaseActivity) getActivity())); } protected final void readArguments(@Nullable Bundle savedInstanceState) { if (savedInstanceState == null) diff --git a/app/src/main/java/org/schulcloud/mobile/ui/courses/detailed/DetailedCoursePresenter.java b/app/src/main/java/org/schulcloud/mobile/ui/courses/detailed/DetailedCoursePresenter.java index eec0fe9e..9004ac1c 100644 --- a/app/src/main/java/org/schulcloud/mobile/ui/courses/detailed/DetailedCoursePresenter.java +++ b/app/src/main/java/org/schulcloud/mobile/ui/courses/detailed/DetailedCoursePresenter.java @@ -50,7 +50,7 @@ public void loadCourse(@NonNull String courseId) { mCourse = mCourseDataManager.getCourseForId(courseId); showName(); RxUtil.unsubscribe(mSubscription); - mSubscription = mTopicDataManager.getTopics() + mSubscription = mTopicDataManager.getTopics(courseId) .observeOn(AndroidSchedulers.mainThread()) .subscribe( topics -> sendToView(v -> v.showTopics(topics)), diff --git a/app/src/main/java/org/schulcloud/mobile/ui/courses/detailed/TopicsAdapter.java b/app/src/main/java/org/schulcloud/mobile/ui/courses/detailed/TopicsAdapter.java index db9356b2..0f3e76b6 100644 --- a/app/src/main/java/org/schulcloud/mobile/ui/courses/detailed/TopicsAdapter.java +++ b/app/src/main/java/org/schulcloud/mobile/ui/courses/detailed/TopicsAdapter.java @@ -44,7 +44,6 @@ public TopicsViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { .inflate(R.layout.item_topic, parent, false); return new TopicsViewHolder(itemView); } - @Override public void onBindViewHolder(TopicsViewHolder holder, int position) { Topic topic = mTopics.get(position); @@ -54,7 +53,6 @@ public void onBindViewHolder(TopicsViewHolder holder, int position) { holder.cardView.setOnClickListener(v -> mDetailedCoursePresenter.showTopicDetail(topic._id)); } - @Override public int getItemCount() { return mTopics.size(); diff --git a/app/src/main/java/org/schulcloud/mobile/ui/courses/topic/BaseViewHolder.java b/app/src/main/java/org/schulcloud/mobile/ui/courses/topic/BaseViewHolder.java new file mode 100644 index 00000000..89f323f9 --- /dev/null +++ b/app/src/main/java/org/schulcloud/mobile/ui/courses/topic/BaseViewHolder.java @@ -0,0 +1,31 @@ +package org.schulcloud.mobile.ui.courses.topic; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.v7.widget.RecyclerView; +import android.view.View; + +public abstract class BaseViewHolder extends RecyclerView.ViewHolder { + private T mItem; + + BaseViewHolder(View itemView) { + super(itemView); + } + + @NonNull + public Context getContext() { + return itemView.getContext(); + } + + void setItem(@NonNull T item) { + mItem = item; + //boolean hidden = item.hidden != null ? item.hidden : false; + //ViewUtil.setVisibility(itemView, !hidden); + onItemSet(item); + } + abstract void onItemSet(@NonNull T item); + @NonNull + final T getItem() { + return mItem; + } +} diff --git a/app/src/main/java/org/schulcloud/mobile/ui/courses/topic/ContentAdapter.java b/app/src/main/java/org/schulcloud/mobile/ui/courses/topic/ContentAdapter.java index 16bb185c..54d7a9a4 100644 --- a/app/src/main/java/org/schulcloud/mobile/ui/courses/topic/ContentAdapter.java +++ b/app/src/main/java/org/schulcloud/mobile/ui/courses/topic/ContentAdapter.java @@ -1,20 +1,42 @@ package org.schulcloud.mobile.ui.courses.topic; -import android.content.Context; +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.graphics.Color; +import android.net.Uri; +import android.os.Build; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.constraint.ConstraintLayout; +import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; -import android.text.Html; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.webkit.JavascriptInterface; +import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ProgressBar; import android.widget.TextView; +import com.google.gson.Gson; +import com.squareup.picasso.Picasso; + import org.schulcloud.mobile.R; import org.schulcloud.mobile.data.datamanagers.UserDataManager; import org.schulcloud.mobile.data.model.Contents; +import org.schulcloud.mobile.data.model.responseBodies.GeogebraResponse; import org.schulcloud.mobile.injection.ConfigPersistent; +import org.schulcloud.mobile.ui.base.BaseActivity; +import org.schulcloud.mobile.ui.base.BaseAdapter; import org.schulcloud.mobile.ui.courses.detailed.DetailedCoursePresenter; -import org.schulcloud.mobile.util.PicassoImageGetter; +import org.schulcloud.mobile.util.ViewUtil; +import org.schulcloud.mobile.util.WebUtil; import java.util.ArrayList; import java.util.List; @@ -23,69 +45,396 @@ import butterknife.BindView; import butterknife.ButterKnife; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import rx.Single; +import rx.android.schedulers.AndroidSchedulers; +import rx.schedulers.Schedulers; @ConfigPersistent -public class ContentAdapter extends RecyclerView.Adapter { +public class ContentAdapter extends BaseAdapter> { + private static final String[] CONTENT_TYPES = { + Contents.COMPONENT_TEXT, + Contents.COMPONENT_RESOURCES, + Contents.COMPONENT_GEOGEBRA, + Contents.COMPONENT_ETHERPAD, + Contents.COMPONENT_NEXBOARD}; - private List mContent; + private List mContents; @Inject DetailedCoursePresenter mDetailedCoursePresenter; - @Inject UserDataManager mUserDataManger; + @Inject + Gson mGson; @Inject public ContentAdapter() { - mContent = new ArrayList<>(); + mContents = new ArrayList<>(); } - public void setContent(@NonNull List content) { - mContent = content; + public void setContents(@NonNull List contents) { + mContents = contents; notifyDataSetChanged(); } @Override - public ContentViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View itemView = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_content, parent, false); - return new ContentViewHolder(itemView); - } + public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + switch (viewType) { + case 0: + return new TextViewHolder(mUserDataManger, + inflater.inflate(R.layout.item_content_text, parent, false)); + case 1: + return new ResourcesViewHolder( + inflater.inflate(R.layout.item_content_resources, parent, false)); + case 2: + return new GeogebraViewHolder(mUserDataManger, + inflater.inflate(R.layout.item_content_geogebra, parent, false)); + case 3: + return new EtherpadViewHolder(mUserDataManger, + inflater.inflate(R.layout.item_content_etherpad, parent, false)); + case 4: + return new NexboardViewHolder(mUserDataManger, + inflater.inflate(R.layout.item_content_nexboard, parent, false)); + default: + return new UnsupportedViewHolder( + inflater.inflate(R.layout.item_content_unsupported, parent, false)); + } + } @Override - public void onBindViewHolder(ContentViewHolder holder, int position) { - Context context = holder.itemView.getContext(); - Contents contents = mContent.get(position); - - holder.nameTextView.setText(contents.title); - - PicassoImageGetter imageGetter = new PicassoImageGetter(holder.descriptionTextView, context, - mUserDataManger.getAccessToken()); - - if (contents.component.equals("text")) - holder.descriptionTextView - .setText(Html.fromHtml(contents.content.text, imageGetter, null)); - else - holder.descriptionTextView.setText( - context.getString(R.string.courses_content_error_notSupported, - contents.component)); + public void onBindViewHolder(BaseViewHolder holder, int position) { + holder.setItem(mContents.get(position)); } - @Override public int getItemCount() { - return mContent.size(); + return mContents.size(); + } + @Override + public int getItemViewType(int position) { + String component = mContents.get(position).component.toLowerCase(); + for (int i = 0; i < CONTENT_TYPES.length; i++) + if (component.equalsIgnoreCase(CONTENT_TYPES[i])) + return i; + return -1; + } + + private static boolean isHidden(@NonNull Contents content) { + return content.hidden != null ? content.hidden : false; + } + + + class UnsupportedViewHolder extends BaseViewHolder { + + @BindView(R.id.contentUnsupported_ll_wrapper) + LinearLayout vLl_wrapper; + @BindView(R.id.contentUnsupported_tv_title) + TextView vTv_title; + @BindView(R.id.contentUnsupported_tv_message) + TextView vTv_message; + + UnsupportedViewHolder(View itemView) { + super(itemView); + ButterKnife.bind(this, itemView); + } + @Override + void onItemSet(@NonNull Contents item) { + boolean isVisible = !isHidden(item); + ViewUtil.setVisibility(vLl_wrapper, isVisible); + if (!isVisible) + return; + + ViewUtil.setText(vTv_title, item.title); + vTv_title.setText(item.title); + vTv_message.setText(getContext() + .getString(R.string.courses_contentUnsupported_message, item.component)); + } } + class TextViewHolder extends WebViewHolder { - class ContentViewHolder extends RecyclerView.ViewHolder { + @BindView(R.id.contentText_ll_wrapper) + LinearLayout vLl_wrapper; + @BindView(R.id.contentText_tv_title) + TextView vTv_title; + @BindView(R.id.contentText_wv_content) + WebView vWv_content; - @BindView(R.id.text_name) - TextView nameTextView; - @BindView(R.id.text_description) - TextView descriptionTextView; + @SuppressLint("SetJavaScriptEnabled") + TextViewHolder(@NonNull UserDataManager userDataManager, @NonNull View itemView) { + super(userDataManager, itemView); + ButterKnife.bind(this, itemView); + + vWv_content.getSettings().setJavaScriptEnabled(true); + vWv_content.setWebViewClient(new WebViewClient() { + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + openUrl(Uri.parse(url)); + return true; + } + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + openUrl(request.getUrl()); + return true; + } + private void openUrl(@NonNull Uri url) { + WebUtil.openUrl(getMainActivity(), url); + } + + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, String url) { + return handleRequest(url); + } + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, + WebResourceRequest request) { + return handleRequest(request.getUrl().toString()); + } + @Nullable + private WebResourceResponse handleRequest(@NonNull String url) { + try { + OkHttpClient okHttpClient; + if (url.startsWith(WebUtil.URL_BASE)) + okHttpClient = CLIENT_INTERNAL; + else + okHttpClient = CLIENT_EXTERNAL; + + Response response = + okHttpClient.newCall(new Request.Builder().url(url).build()) + .execute(); + return new WebResourceResponse( + response.header(WebUtil.HEADER_CONTENT_TYPE, + WebUtil.MIME_TEXT_PLAIN), + response.header(WebUtil.HEADER_CONTENT_TYPE, + WebUtil.ENCODING_UTF_8), + response.body().byteStream() + ); + } catch (Exception e) { + return null; + } + } + }); + vWv_content.setBackgroundColor(Color.TRANSPARENT); + } + @Override + void onItemSet(@NonNull Contents item) { + boolean isVisible = !isHidden(item); + ViewUtil.setVisibility(vLl_wrapper, isVisible); + if (!isVisible) + return; - public ContentViewHolder(View itemView) { + ViewUtil.setText(vTv_title, item.title); + + vWv_content + .loadDataWithBaseURL(null, null, WebUtil.MIME_TEXT_HTML, WebUtil.ENCODING_UTF_8, + null); + vWv_content.loadDataWithBaseURL(WebUtil.URL_BASE, + CONTENT_TEXT_PREFIX + (item.content.text != null ? item.content.text : "") + + CONTENT_TEXT_SUFFIX, WebUtil.MIME_TEXT_HTML, WebUtil.ENCODING_UTF_8, + null); + } + } + public class ResourcesViewHolder extends BaseViewHolder { + + @Inject + ResourcesAdapter mResourcesAdapter; + + @BindView(R.id.contentResources_ll_wrapper) + LinearLayout vLl_wrapper; + @BindView(R.id.contentResources_tv_title) + TextView vTv_title; + @BindView(R.id.contentResources_rv) + RecyclerView vRv; + + ResourcesViewHolder(View itemView) { super(itemView); ButterKnife.bind(this, itemView); + ((BaseActivity) itemView.getContext()).activityComponent().inject(this); + + vRv.setAdapter(mResourcesAdapter); + vRv.setLayoutManager(new LinearLayoutManager(itemView.getContext()) { + @Override + public boolean canScrollVertically() { + return false; + } + }); + } + @Override + void onItemSet(@NonNull Contents item) { + boolean isVisible = !isHidden(item); + ViewUtil.setVisibility(vLl_wrapper, isVisible); + if (!isVisible) + return; + + ViewUtil.setText(vTv_title, item.title); + + mResourcesAdapter.setResources(item.content.resources); + } + } + class GeogebraViewHolder extends WebViewHolder { + private final String TAG = GeogebraViewHolder.class.getSimpleName(); + + private static final String GEOGEBRA = "https://www.geogebra.org/m/"; + private static final String GEOGEBRA_API = "http://www.geogebra.org/api/json.php"; + // language=json + private static final String GEOGEBRA_REQUEST_PREFIX = "{ \"request\": {\n" + + " \"-api\": \"1.0.0\",\n" + + " \"task\": {\n" + + " \"-type\": \"fetch\",\n" + + " \"fields\": {\n" + + " \"field\": [\n" + + " { \"-name\": \"preview_url\" }\n" + + " ]\n" + + " },\n" + + " \"filters\" : {\n" + + " \"field\": [\n" + + " { \"-name\":\"id\", \"#text\":\""; + // language=json + private static final String GEOGEBRA_REQUEST_SUFFIX = "\" }\n" + + " ]\n" + + " },\n" + + " \"limit\": { \"-num\": \"1\" }\n" + + " }\n" + + "}\n" + + "}"; + + @BindView(R.id.contentGeogebra_cl_wrapper) + ConstraintLayout vCl_wrapper; + @BindView(R.id.contentGeogebra_tv_title) + TextView vTv_title; + @BindView(R.id.contentGeogebra_iv_preview) + ImageView vIv_preview; + + GeogebraViewHolder(@NonNull UserDataManager userDataManager, @NonNull View itemView) { + super(userDataManager, itemView); + ButterKnife.bind(this, itemView); + + vCl_wrapper.setOnClickListener(v -> WebUtil + .openUrl(getMainActivity(), + Uri.parse(GEOGEBRA + getItem().content.materialId))); + } + @Override + void onItemSet(@NonNull Contents item) { + boolean isVisible = !isHidden(item); + ViewUtil.setVisibility(vCl_wrapper, isVisible); + if (!isVisible) + return; + + vTv_title.setText(item.title); + + loadPreviewImage(); + } + + private void loadPreviewImage() { + ViewUtil.setVisibility(vIv_preview, true); + Single.just(getItem().content.materialId) + .flatMap(materialId -> Single.create(subscriber -> { + try { + Response responseRaw = CLIENT_EXTERNAL.newCall(new Request.Builder() + .url(GEOGEBRA_API) + .post(RequestBody + .create(MediaType.parse(WebUtil.MIME_APPLICATION_JSON), + GEOGEBRA_REQUEST_PREFIX + materialId + + GEOGEBRA_REQUEST_SUFFIX)) + .build()).execute(); + GeogebraResponse response = + mGson.fromJson(responseRaw.body().charStream(), + GeogebraResponse.class); + subscriber.onSuccess(response.responses.response.item.previewUrl); + } catch (Exception e) { + Log.w(TAG, "Error retrieving preview URL", e); + subscriber.onError(e); + } + })) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(uri -> Picasso.with(getContext()).load(uri).into(vIv_preview), + throwable -> {}); + } + } + class EtherpadViewHolder extends WebViewHolder { + + @BindView(R.id.contentEtherpad_cl_wrapper) + ConstraintLayout vCl_wrapper; + @BindView(R.id.contentEtherpad_tv_title) + TextView vTv_title; + @BindView(R.id.contentEtherpad_tv_description) + TextView vTv_description; + @BindView(R.id.contentEtherpad_iv_open) + ImageView vIv_open; + @BindView(R.id.contentEtherpad_wv_content) + WebView vWv_content; + + @SuppressLint("SetJavaScriptEnabled") + EtherpadViewHolder(@NonNull UserDataManager userDataManager, @NonNull View itemView) { + super(userDataManager, itemView); + ButterKnife.bind(this, itemView); + + vIv_open.setOnClickListener(v -> + WebUtil.openUrl(getMainActivity(), Uri.parse(getItem().content.url))); + + vWv_content.getSettings().setJavaScriptEnabled(true); + } + @Override + void onItemSet(@NonNull Contents item) { + boolean isVisible = !isHidden(item); + ViewUtil.setVisibility(vCl_wrapper, isVisible); + if (!isVisible) + return; + + vTv_title.setText(item.title); + ViewUtil.setText(vTv_description, item.content.description); + + vWv_content + .loadDataWithBaseURL(null, null, WebUtil.MIME_TEXT_HTML, WebUtil.ENCODING_UTF_8, + null); + vWv_content.loadUrl(getItem().content.url); + } + } + class NexboardViewHolder extends WebViewHolder { + private static final String URL_SUFFIX = "?username=Test&stickypad=false"; + + @BindView(R.id.contentNexboard_cl_wrapper) + ConstraintLayout vCl_wrapper; + @BindView(R.id.contentNexboard_tv_title) + TextView vTv_title; + @BindView(R.id.contentNexboard_tv_description) + TextView vTv_description; + @BindView(R.id.contentNexboard_iv_open) + ImageView vIv_open; + @BindView(R.id.contentNexboard_wv_content) + WebView vWv_content; + + @SuppressLint("SetJavaScriptEnabled") + NexboardViewHolder(@NonNull UserDataManager userDataManager, @NonNull View itemView) { + super(userDataManager, itemView); + ButterKnife.bind(this, itemView); + + vIv_open.setOnClickListener(v -> WebUtil + .openUrl(getMainActivity(), Uri.parse(getItem().content.url + URL_SUFFIX))); + + vWv_content.getSettings().setJavaScriptEnabled(true); + } + @Override + void onItemSet(@NonNull Contents item) { + boolean isVisible = !isHidden(item); + ViewUtil.setVisibility(vCl_wrapper, isVisible); + if (!isVisible) + return; + + vTv_title.setText(item.title); + ViewUtil.setText(vTv_description, item.content.description); + + vWv_content + .loadDataWithBaseURL(null, null, WebUtil.MIME_TEXT_HTML, WebUtil.ENCODING_UTF_8, + null); + vWv_content.loadUrl(getItem().content.url + URL_SUFFIX); } } } diff --git a/app/src/main/java/org/schulcloud/mobile/ui/courses/topic/PassiveWebView.java b/app/src/main/java/org/schulcloud/mobile/ui/courses/topic/PassiveWebView.java new file mode 100644 index 00000000..b56e1e1f --- /dev/null +++ b/app/src/main/java/org/schulcloud/mobile/ui/courses/topic/PassiveWebView.java @@ -0,0 +1,41 @@ +package org.schulcloud.mobile.ui.courses.topic; + +import android.content.Context; +import android.support.annotation.RequiresApi; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.webkit.WebView; + +/** + * A {@link WebView} that does not consume touch events. + *

+ * Date: 2/22/2018 + */ +public class PassiveWebView extends WebView { + public PassiveWebView(Context context) { + super(context); + init(); + } + public PassiveWebView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + public PassiveWebView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + @RequiresApi(21) + public PassiveWebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + private void init() { + setClickable(false); + setFocusable(false); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + return false; + } +} diff --git a/app/src/main/java/org/schulcloud/mobile/ui/courses/topic/ResourcesAdapter.java b/app/src/main/java/org/schulcloud/mobile/ui/courses/topic/ResourcesAdapter.java new file mode 100644 index 00000000..b3287edd --- /dev/null +++ b/app/src/main/java/org/schulcloud/mobile/ui/courses/topic/ResourcesAdapter.java @@ -0,0 +1,132 @@ +package org.schulcloud.mobile.ui.courses.topic; + +import android.annotation.TargetApi; +import android.net.Uri; +import android.os.Build; +import android.support.annotation.NonNull; +import android.support.v7.widget.CardView; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.WebResourceRequest; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.TextView; + +import org.schulcloud.mobile.R; +import org.schulcloud.mobile.data.datamanagers.UserDataManager; +import org.schulcloud.mobile.data.model.Resource; +import org.schulcloud.mobile.ui.base.BaseAdapter; +import org.schulcloud.mobile.util.ViewUtil; +import org.schulcloud.mobile.util.WebUtil; +import org.schulcloud.mobile.util.dialogs.DialogFactory; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import butterknife.BindView; +import butterknife.ButterKnife; +import rx.android.schedulers.AndroidSchedulers; +import rx.schedulers.Schedulers; + +import static org.schulcloud.mobile.ui.courses.topic.ContentAdapter.TextViewHolder.CONTENT_TEXT_PREFIX; +import static org.schulcloud.mobile.ui.courses.topic.ContentAdapter.TextViewHolder.CONTENT_TEXT_SUFFIX; + +/** + * Date: 2/17/2018 + */ +public class ResourcesAdapter extends BaseAdapter { + private static final String TAG = ResourcesAdapter.class.getSimpleName(); + + private final UserDataManager mUserDataManager; + + private List mResources; + + @Inject + public ResourcesAdapter(UserDataManager userDataManager) { + mUserDataManager = userDataManager; + mResources = new ArrayList<>(); + } + + public void setResources(@NonNull List resources) { + mResources = resources; + notifyDataSetChanged(); + } + + @Override + public ResourceViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View itemView = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_resource, parent, false); + return new ResourceViewHolder(mUserDataManager, itemView); + } + @Override + public void onBindViewHolder(ResourceViewHolder holder, int position) { + holder.setItem(mResources.get(position)); + } + @Override + public int getItemCount() { + return mResources.size(); + } + + class ResourceViewHolder extends WebViewHolder { + + @BindView(R.id.resource_card) + CardView vCard; + @BindView(R.id.resource_tv_title) + TextView vTv_title; + @BindView(R.id.resource_pwv_description) + PassiveWebView vPwv_description; + @BindView(R.id.resource_tv_client) + TextView vTv_client; + + public ResourceViewHolder(@NonNull UserDataManager userDataManager, + @NonNull View itemView) { + super(userDataManager, itemView); + ButterKnife.bind(this, itemView); + + vCard.setOnClickListener(v -> + WebUtil.resolveRedirect(getItem().url, mUserDataManager.getAccessToken()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(url -> WebUtil.openUrl(getMainActivity(), url), + throwable -> { + Log.e(TAG, "onBindViewHolder: ", throwable); + DialogFactory.createGenericErrorDialog(getContext(), + R.string.courses_resources_error).show(); + })); + vPwv_description.setOnTouchListener(null); + } + + @Override + void onItemSet(@NonNull Resource item) { + vTv_title.setText(item.title); + ViewUtil.setText(vTv_title, item.title); + + vPwv_description.getSettings().setJavaScriptEnabled(true); + vPwv_description.setWebViewClient(new WebViewClient() { + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + openUrl(Uri.parse(url)); + return true; + } + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + openUrl(request.getUrl()); + return true; + } + private void openUrl(@NonNull Uri url) { + WebUtil.openUrl(getMainActivity(), url); + } + }); + vPwv_description.loadDataWithBaseURL(WebUtil.URL_BASE, + CONTENT_TEXT_PREFIX + (item.description != null ? item.description : "") + + CONTENT_TEXT_SUFFIX, WebUtil.MIME_TEXT_HTML, WebUtil.ENCODING_UTF_8, null); + vTv_client + .setText(getContext().getString(R.string.courses_resource_client, item.client)); + } + } +} diff --git a/app/src/main/java/org/schulcloud/mobile/ui/courses/topic/TopicFragment.java b/app/src/main/java/org/schulcloud/mobile/ui/courses/topic/TopicFragment.java index 08c32ba2..4f4a3aeb 100644 --- a/app/src/main/java/org/schulcloud/mobile/ui/courses/topic/TopicFragment.java +++ b/app/src/main/java/org/schulcloud/mobile/ui/courses/topic/TopicFragment.java @@ -3,8 +3,9 @@ import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.StaggeredGridLayoutManager; +import android.util.DisplayMetrics; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -13,6 +14,8 @@ import org.schulcloud.mobile.R; import org.schulcloud.mobile.data.model.Contents; import org.schulcloud.mobile.ui.main.MainFragment; +import org.schulcloud.mobile.util.ViewUtil; +import org.schulcloud.mobile.util.dialogs.DialogFactory; import java.util.List; @@ -25,23 +28,21 @@ public class TopicFragment extends MainFragment implements TopicMvpView { private static final String ARGUMENT_TOPIC_ID = "ARGUMENT_TOPIC_ID"; - private String mTopicId = null; - @Inject TopicPresenter mTopicPresenter; @Inject ContentAdapter mContentAdapter; - @BindView(R.id.topicName) - TextView topicName; - @BindView(R.id.topicRecycler) - RecyclerView recyclerView; + @BindView(R.id.topic_tv_title) + TextView vTv_title; + @BindView(R.id.topic_rv) + RecyclerView vRv; /** * Creates a new instance of this fragment. * - * @param topicId The ID of the topic that should be shown + * @param topicId The ID of the topic that should be shown * @return The new instance */ @NonNull @@ -73,8 +74,20 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, ButterKnife.bind(this, view); setTitle(R.string.courses_topic_title); - recyclerView.setAdapter(mContentAdapter); - recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); + vRv.setAdapter(mContentAdapter); + DisplayMetrics metrics = new DisplayMetrics(); + getActivity().getWindowManager().getDefaultDisplay().getMetrics(metrics); + int spans = Math.max(1, metrics.widthPixels / ViewUtil.dpToPx(440)); + StaggeredGridLayoutManager layoutManager = + new StaggeredGridLayoutManager(spans, StaggeredGridLayoutManager.VERTICAL) { + @Override + public boolean canScrollVertically() { + return false; + } + }; + layoutManager + .setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS); + vRv.setLayoutManager(layoutManager); return view; } @@ -83,10 +96,15 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, /***** MVP View methods implementation *****/ @Override public void showName(@NonNull String name) { - topicName.setText(name); + vTv_title.setText(name); } @Override public void showContent(@NonNull List contents) { - mContentAdapter.setContent(contents); + mContentAdapter.setContents(contents); + } + @Override + public void showError() { + DialogFactory.createGenericErrorDialog(getContext(), R.string.courses_topic_error).show(); + finish(); } } diff --git a/app/src/main/java/org/schulcloud/mobile/ui/courses/topic/TopicMvpView.java b/app/src/main/java/org/schulcloud/mobile/ui/courses/topic/TopicMvpView.java index 38caa794..71f2940b 100644 --- a/app/src/main/java/org/schulcloud/mobile/ui/courses/topic/TopicMvpView.java +++ b/app/src/main/java/org/schulcloud/mobile/ui/courses/topic/TopicMvpView.java @@ -11,5 +11,6 @@ public interface TopicMvpView extends MvpView { void showName(@NonNull String name); void showContent(@NonNull List contents); + void showError(); } diff --git a/app/src/main/java/org/schulcloud/mobile/ui/courses/topic/TopicPresenter.java b/app/src/main/java/org/schulcloud/mobile/ui/courses/topic/TopicPresenter.java index 652d76bf..55b0e5a0 100644 --- a/app/src/main/java/org/schulcloud/mobile/ui/courses/topic/TopicPresenter.java +++ b/app/src/main/java/org/schulcloud/mobile/ui/courses/topic/TopicPresenter.java @@ -1,16 +1,22 @@ package org.schulcloud.mobile.ui.courses.topic; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; -import org.schulcloud.mobile.data.model.Topic; import org.schulcloud.mobile.data.datamanagers.TopicDataManager; +import org.schulcloud.mobile.data.model.Topic; import org.schulcloud.mobile.injection.ConfigPersistent; import org.schulcloud.mobile.ui.base.BasePresenter; import javax.inject.Inject; +import rx.android.schedulers.AndroidSchedulers; +import rx.schedulers.Schedulers; + @ConfigPersistent public class TopicPresenter extends BasePresenter { + private static final String TAG = TopicPresenter.class.getSimpleName(); private final TopicDataManager mTopicDataManager; private Topic mTopic; @@ -25,10 +31,26 @@ public void onViewAttached(@NonNull TopicMvpView view) { showName(); } - public void loadContents(@NonNull String topicId) { - mTopic = mTopicDataManager.getTopicForId(topicId); - showName(); - sendToView(v -> v.showContent(mTopic.contents)); + public void loadContents(@Nullable String topicId) { + if (topicId == null) { + sendToView(TopicMvpView::showError); + return; + } + + mTopicDataManager.getTopic(topicId) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(topic -> { + mTopic = topic; + if (topic == null) + return; + + showName(); + sendToView(v -> v.showContent(mTopic.contents)); + }, throwable -> { + Log.w(TAG, "Error loading topic " + topicId, throwable); + sendToView(TopicMvpView::showError); + }); } private void showName() { diff --git a/app/src/main/java/org/schulcloud/mobile/ui/courses/topic/WebViewHolder.java b/app/src/main/java/org/schulcloud/mobile/ui/courses/topic/WebViewHolder.java new file mode 100644 index 00000000..3427f996 --- /dev/null +++ b/app/src/main/java/org/schulcloud/mobile/ui/courses/topic/WebViewHolder.java @@ -0,0 +1,77 @@ +package org.schulcloud.mobile.ui.courses.topic; + +import android.os.Build; +import android.support.annotation.NonNull; +import android.view.View; +import android.webkit.WebView; + +import org.schulcloud.mobile.BuildConfig; +import org.schulcloud.mobile.data.datamanagers.UserDataManager; +import org.schulcloud.mobile.util.WebUtil; + +import javax.inject.Inject; + +import okhttp3.OkHttpClient; + +public abstract class WebViewHolder extends BaseViewHolder { + // language=HTML + public static final String CONTENT_TEXT_PREFIX = "\n" + + "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + "\n" + + ""; + // language=HTML + public static final String CONTENT_TEXT_SUFFIX = "\n" + + "\n" + + "\n"; + + protected final OkHttpClient CLIENT_INTERNAL; + protected final OkHttpClient CLIENT_EXTERNAL; + + @Inject + UserDataManager mUserDataManager; + + WebViewHolder(@NonNull UserDataManager userDataManager, @NonNull View itemView) { + super(itemView); + mUserDataManager = userDataManager; + + if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) + WebView.setWebContentsDebuggingEnabled(true); + + CLIENT_INTERNAL = new OkHttpClient().newBuilder().addInterceptor(chain -> + chain.proceed(chain.request().newBuilder() + .addHeader(WebUtil.HEADER_COOKIE, + "jwt=" + mUserDataManager.getAccessToken()) + .build())) + .build(); + CLIENT_EXTERNAL = new OkHttpClient(); + } +} diff --git a/app/src/main/java/org/schulcloud/mobile/ui/files/FilesFragment.java b/app/src/main/java/org/schulcloud/mobile/ui/files/FilesFragment.java index fe8e29d4..2d8b214f 100644 --- a/app/src/main/java/org/schulcloud/mobile/ui/files/FilesFragment.java +++ b/app/src/main/java/org/schulcloud/mobile/ui/files/FilesFragment.java @@ -54,9 +54,11 @@ public class FilesFragment extends MainFragment implements FilesMvpView { private static final String ARGUMENT_TRIGGER_SYNC = "ARGUMENT_TRIGGER_SYNC"; + private static final String ARGUMENT_PATH = "ARGUMENT_PATH"; + private static final String ARGUMENT_FILE = "ARGUMENT_FILE"; @Inject - FilesPresenter mFilesPresenter; + FilesPresenter mPresenter; @Inject InternalFilesUtil mFilesUtil; @@ -89,7 +91,11 @@ public class FilesFragment extends MainFragment @NonNull public static FilesFragment newInstance() { - return newInstance(true); + return newInstance(true, null, null); + } + @NonNull + public static FilesFragment newInstance(@NonNull String path, String file) { + return newInstance(true, path, file); } /** * Creates a new instance of this fragment. @@ -99,11 +105,14 @@ public static FilesFragment newInstance() { * @return The new instance */ @NonNull - public static FilesFragment newInstance(boolean triggerDataSyncOnCreate) { + public static FilesFragment newInstance(boolean triggerDataSyncOnCreate, + @Nullable String path, @Nullable String file) { FilesFragment filesFragment = new FilesFragment(); Bundle args = new Bundle(); args.putBoolean(ARGUMENT_TRIGGER_SYNC, triggerDataSyncOnCreate); + args.putString(ARGUMENT_PATH, path); + args.putString(ARGUMENT_FILE, file); filesFragment.setArguments(args); return filesFragment; @@ -113,7 +122,7 @@ public static FilesFragment newInstance(boolean triggerDataSyncOnCreate) { public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); activityComponent().inject(this); - setPresenter(mFilesPresenter); + setPresenter(mPresenter); readArguments(savedInstanceState); setHasOptionsMenu(true); @@ -125,6 +134,8 @@ public void onReadArguments(Bundle args) { restartService(FileSyncService.getStartIntent(getContext())); restartService(DirectorySyncService.getStartIntent(getContext())); } + mPresenter.onDirectorySelected(args.getString(ARGUMENT_PATH), + args.getString(ARGUMENT_FILE)); } @Nullable @Override @@ -187,7 +198,7 @@ public boolean onOptionsItemSelected(MenuItem item) { @Override public boolean onBackPressed() { - return mFilesPresenter.onBackSelected(); + return mPresenter.onBackSelected(); } private void updateEmptyText() { @@ -197,11 +208,12 @@ private void updateEmptyText() { && (files == null || files.isEmpty())); } - /***** MVP View methods implementation *****/ + /* MVP View methods implementation */ @Override public void showBreadcrumbs(@NonNull String path, @Nullable Course course) { String[] folders = PathUtil.getAllParts(path); - final StringBuilder currentPath = new StringBuilder(folders[0] + "/" + folders[1]); + final StringBuilder currentPath = + new StringBuilder(PathUtil.combine(folders[0], folders[1])); SpannableStringBuilder builder = new SpannableStringBuilder(); // Top-level directory ("Persönliche Dateien" or name and color of the course) @@ -238,7 +250,11 @@ private class BreadcrumbClickableSpan extends ClickableSpan { @Override public void onClick(View widget) { - mFilesPresenter.onDirectorySelected(mPath); + mPresenter.onDirectorySelected(mPath, null); + + // Realm doesn't trigger a notification if a table was and stays empty, so in case of a + // manual refresh in an empty directory the refresh indicator would never terminate. + new Handler().postDelayed(() -> swipeRefresh.setRefreshing(false), 3000); } @Override public void updateDrawState(TextPaint ds) { @@ -295,6 +311,11 @@ public void showFile(@NonNull String url, @NonNull String mimeType, @NonNull Str .show(); } @Override + public void showFileError_notFound(@NonNull String fileName) { + DialogFactory.createGenericErrorDialog(getContext(), + getString(R.string.files_fileLoad_error_notFound, fileName)).show(); + } + @Override public void saveFile(@NonNull String fileName, @NonNull ResponseBody body) { DialogUtil.cancel(mFileDownloadProgressDialog); @@ -321,7 +342,7 @@ void startFileUploadChoosing() { permissionsDeniedToError(requestPermissions(Manifest.permission.READ_EXTERNAL_STORAGE)) .flatMap(results -> mFilesUtil.openFileChooser()) .subscribe( - file -> mFilesPresenter.onFileUploadSelected(file), + file -> mPresenter.onFileUploadSelected(file), throwable -> DialogFactory.createGenericErrorDialog(getContext(), R.string.files_fileUpload_error_readPermissionDenied) ); @@ -393,7 +414,7 @@ private void createDirectory() { getString(R.string.files_directoryCreate_title), getString(R.string.dialog_action_ok), getString(R.string.dialog_action_cancel)) .subscribe( - s -> mFilesPresenter.onDirectoryCreateSelected(s), + s -> mPresenter.onDirectoryCreateSelected(s), throwable -> {} // Abort if cancel was selected ); } diff --git a/app/src/main/java/org/schulcloud/mobile/ui/files/FilesMvpView.java b/app/src/main/java/org/schulcloud/mobile/ui/files/FilesMvpView.java index 689f1ab3..ecd6ac8c 100644 --- a/app/src/main/java/org/schulcloud/mobile/ui/files/FilesMvpView.java +++ b/app/src/main/java/org/schulcloud/mobile/ui/files/FilesMvpView.java @@ -31,6 +31,8 @@ public interface FilesMvpView extends MvpView { void showFile(@NonNull String url, @NonNull String mimeType, @NonNull String extension); + void showFileError_notFound(@NonNull String fileName); + void saveFile(@NonNull String fileName, @NonNull ResponseBody body); /* File upload */ diff --git a/app/src/main/java/org/schulcloud/mobile/ui/files/FilesPresenter.java b/app/src/main/java/org/schulcloud/mobile/ui/files/FilesPresenter.java index 14967a4b..cb68ce8a 100644 --- a/app/src/main/java/org/schulcloud/mobile/ui/files/FilesPresenter.java +++ b/app/src/main/java/org/schulcloud/mobile/ui/files/FilesPresenter.java @@ -1,6 +1,7 @@ package org.schulcloud.mobile.ui.files; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.Log; import com.ipaulpro.afilechooser.utils.FileUtils; @@ -8,7 +9,6 @@ import org.schulcloud.mobile.data.datamanagers.CourseDataManager; import org.schulcloud.mobile.data.datamanagers.FileDataManager; import org.schulcloud.mobile.data.datamanagers.UserDataManager; -import org.schulcloud.mobile.data.model.Course; import org.schulcloud.mobile.data.model.CurrentUser; import org.schulcloud.mobile.data.model.Directory; import org.schulcloud.mobile.data.model.File; @@ -45,9 +45,11 @@ public class FilesPresenter extends BasePresenter { private Subscription mDirectoryCreateSubscription; private Subscription mDirectoryDeleteSubscription; + private String mFileToOpen = null; + @Inject FilesPresenter(FileDataManager fileDataManager, UserDataManager userDataManager, - CourseDataManager courseDataManager) { + CourseDataManager courseDataManager) { mFileDataManager = fileDataManager; mUserDataManager = userDataManager; mCourseDataManager = courseDataManager; @@ -81,25 +83,26 @@ public void onDestroy() { private void loadPermissions() { RxUtil.unsubscribe(mCurrentUserSubscription); mCurrentUserSubscription = mUserDataManager.getCurrentUser() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(currentUser -> sendToView(v -> { - v.showCanUploadFile(currentUser.hasPermission( - CurrentUser.PERMISSION_FILE_CREATE)); - v.showCanDeleteFiles(currentUser.hasPermission( - CurrentUser.PERMISSION_FILE_DELETE)); - v.showCanCreateDirectories(currentUser.hasPermission( - CurrentUser.PERMISSION_FOLDER_CREATE)); - v.showCanDeleteDirectories(currentUser.hasPermission( - CurrentUser.PERMISSION_FOLDER_DELETE)); - })); + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(currentUser -> sendToView(v -> { + v.showCanUploadFile(currentUser.hasPermission( + CurrentUser.PERMISSION_FILE_CREATE)); + v.showCanDeleteFiles(currentUser.hasPermission( + CurrentUser.PERMISSION_FILE_DELETE)); + v.showCanCreateDirectories(currentUser.hasPermission( + CurrentUser.PERMISSION_FOLDER_CREATE)); + v.showCanDeleteDirectories(currentUser.hasPermission( + CurrentUser.PERMISSION_FOLDER_DELETE)); + })); } private void loadBreadcrumbs() { - String path = mFileDataManager.getCurrentStorageContext(); - if (path.startsWith(FileDataManager.FILES_CONTEXT_MY)) + String path = mFileDataManager.getStorageContext(); + String courseId = mFileDataManager.isStorageContextCourse(); + if (mFileDataManager.isStorageContextMy()) sendToView(view -> view.showBreadcrumbs(path, null)); - else if (path.startsWith(FileDataManager.FILES_CONTEXT_COURSES)) - sendToView(view -> view.showBreadcrumbs(path, - mCourseDataManager.getCourseForId(path.split("/", 3)[1]))); + else if (courseId != null) + sendToView(view -> + view.showBreadcrumbs(path, mCourseDataManager.getCourseForId(courseId))); } private void loadFiles() { @@ -107,7 +110,21 @@ private void loadFiles() { mFileSubscription = mFileDataManager.getFiles() .observeOn(AndroidSchedulers.mainThread()) .subscribe( - files -> sendToView(view -> view.showFiles(files)), + files -> { + sendToView(view -> view.showFiles(files)); + + if (mFileToOpen == null) + return; + for (File file : files) + if (file.name.equals(mFileToOpen)) { + onFileSelected(file); + mFileToOpen = null; + return; + } + + sendToView(v -> v.showFileError_notFound(mFileToOpen)); + mFileToOpen = null; + }, error -> { Timber.e(error, "There was an error loading the files."); sendToView(FilesMvpView::showFilesLoadError); @@ -175,7 +192,7 @@ private void downloadFile(@NonNull String url, @NonNull String fileName) { public void onFileUploadSelected(@NonNull java.io.File file) { sendToView(FilesMvpView::showFileUploadStarted); String uploadPath = PathUtil - .combine(mFileDataManager.getCurrentStorageContext(), file.getName()); + .combine(mFileDataManager.getStorageContext(), file.getName()); SignedUrlRequest signedUrlRequest = new SignedUrlRequest( SignedUrlRequest.ACTION_PUT, @@ -202,8 +219,9 @@ private void uploadFile(@NonNull java.io.File file, @NonNull SignedUrlResponse signedUrlResponse) { RxUtil.unsubscribe(mFileUploadSubscription); mFileUploadSubscription = mFileDataManager.uploadFile(file, signedUrlResponse) - .flatMap(responseBody -> mFileDataManager.persistFile(signedUrlResponse, file.getName(), - FileUtils.getMimeType(file), file.length())) + .flatMap(responseBody -> mFileDataManager + .persistFile(signedUrlResponse, file.getName(), + FileUtils.getMimeType(file), file.length())) .observeOn(AndroidSchedulers.mainThread()) .subscribe( responseBody -> sendToView(view -> { @@ -258,10 +276,15 @@ private void loadDirectories() { * @param directory The selected directory */ public void onDirectorySelected(@NonNull Directory directory) { - onDirectorySelected(PathUtil.combine(directory.path, directory.name)); + onDirectorySelected(PathUtil.combine(directory.path, directory.name), null); } - public void onDirectorySelected(@NonNull String path) { - mFileDataManager.setCurrentStorageContext(path); + public void onDirectorySelected(@Nullable String path, @Nullable String file) { + if (path == null) + return; + + mFileToOpen = file; + + mFileDataManager.setStorageContext(path); loadBreadcrumbs(); sendToView(view -> { view.reloadFiles(); @@ -274,7 +297,7 @@ public void onDirectoryCreateSelected(@NonNull String name) { RxUtil.unsubscribe(mDirectoryCreateSubscription); mDirectoryCreateSubscription = mFileDataManager .createDirectory(new CreateDirectoryRequest( - PathUtil.combine(mFileDataManager.getCurrentStorageContext(), name))) + PathUtil.combine(mFileDataManager.getStorageContext(), name))) .observeOn(AndroidSchedulers.mainThread()) .subscribe( directory -> sendToView(view -> { @@ -316,11 +339,11 @@ public void onDirectoryDeleteSelected(@NonNull Directory directory) { * @return True if stepping back was successful, false if we are already in the root directory. */ public boolean onBackSelected() { - String storageContext = mFileDataManager.getCurrentStorageContext(); + String storageContext = mFileDataManager.getStorageContext(); // first two parts are meta - if (storageContext.split("/", 3).length > 2) { - onDirectorySelected(PathUtil.parent(storageContext)); + if (storageContext.split("/", 4).length > 3) { + onDirectorySelected(PathUtil.parent(storageContext), null); return true; } return false; diff --git a/app/src/main/java/org/schulcloud/mobile/ui/files/overview/FileOverviewFragment.java b/app/src/main/java/org/schulcloud/mobile/ui/files/overview/FileOverviewFragment.java index b9a0a71e..22ee6cf1 100644 --- a/app/src/main/java/org/schulcloud/mobile/ui/files/overview/FileOverviewFragment.java +++ b/app/src/main/java/org/schulcloud/mobile/ui/files/overview/FileOverviewFragment.java @@ -31,6 +31,7 @@ public class FileOverviewFragment extends MainFragment implements FileOverviewMvpView { private static final String ARGUMENT_TRIGGER_SYNC = "ARGUMENT_TRIGGER_SYNC"; + private static final String ARGUMENT_SWITCH_DIR = "ARGUMENT_SWITCH_DIR"; @Inject FileOverviewPresenter mFileOverviewPresenter; @@ -51,7 +52,11 @@ public class FileOverviewFragment extends MainFragment { - startService(CourseSyncService.getStartIntent(getContext())); - - new Handler().postDelayed(() -> { - mFileOverviewPresenter.load(); + vSwipeRefresh.setOnRefreshListener(() -> { + startService(CourseSyncService.getStartIntent(getContext())); + new Handler().postDelayed(() -> { + mFileOverviewPresenter.load(); - vSwipeRefresh.setRefreshing(false); - }, 3000); - } - ); + vSwipeRefresh.setRefreshing(false); + }, 3000); + }); vC_myWrapper.setOnClickListener(v -> mFileOverviewPresenter.showMyFiles()); vRv_coursesList.setAdapter(mCourseDirectoryAdapter); - vRv_coursesList.setLayoutManager(new LinearLayoutManager(getContext())); + vRv_coursesList.setLayoutManager(new LinearLayoutManager(getContext()) { + @Override + public boolean canScrollVertically() { + return false; + } + }); return view; } diff --git a/app/src/main/java/org/schulcloud/mobile/ui/files/overview/FileOverviewPresenter.java b/app/src/main/java/org/schulcloud/mobile/ui/files/overview/FileOverviewPresenter.java index 44eae327..29279cf1 100644 --- a/app/src/main/java/org/schulcloud/mobile/ui/files/overview/FileOverviewPresenter.java +++ b/app/src/main/java/org/schulcloud/mobile/ui/files/overview/FileOverviewPresenter.java @@ -4,7 +4,6 @@ import org.schulcloud.mobile.data.datamanagers.CourseDataManager; import org.schulcloud.mobile.data.datamanagers.FileDataManager; -import org.schulcloud.mobile.data.model.Course; import org.schulcloud.mobile.injection.ConfigPersistent; import org.schulcloud.mobile.ui.base.BasePresenter; import org.schulcloud.mobile.util.RxUtil; @@ -21,30 +20,31 @@ public class FileOverviewPresenter extends BasePresenter { private final CourseDataManager mCourseDataManager; private Subscription mCoursesSubscription; private boolean mIsFirstLoad = true; + private boolean mSwitchDirectoryOnCreate; @Inject - public FileOverviewPresenter(FileDataManager fileDataManager, CourseDataManager courseDataManager) { + public FileOverviewPresenter(FileDataManager fileDataManager, + CourseDataManager courseDataManager) { mFileDataManager = fileDataManager; mCourseDataManager = courseDataManager; sendToView(v -> load()); } - + @Override + public void onViewAttached(@NonNull FileOverviewMvpView view) { + super.onViewAttached(view); + if (!mIsFirstLoad) + mFileDataManager.setStorageContextToRoot(); + } @Override public void onDestroy() { super.onDestroy(); RxUtil.unsubscribe(mCoursesSubscription); } + public void switchDirectoryOnCreate(boolean switchDir) { + mSwitchDirectoryOnCreate = switchDir; + } public void load() { - // Open folder directly if it is already set (e.g. from a previous session) - if (mIsFirstLoad - && mFileDataManager.getCurrentStorageContext().split("/", 3).length >= 2) { - mIsFirstLoad = false; - getViewOrThrow().showDirectory(); - return; - } - - mFileDataManager.setCurrentStorageContextToRoot(); RxUtil.unsubscribe(mCoursesSubscription); mCoursesSubscription = mCourseDataManager.getCourses() .observeOn(AndroidSchedulers.mainThread()) @@ -52,10 +52,20 @@ public void load() { courses -> sendToView(view -> view.showCourses(courses)), throwable -> sendToView(FileOverviewMvpView::showCoursesError) ); + + // Open folder directly if it is already set (e.g. from a previous session) + if (mIsFirstLoad && mSwitchDirectoryOnCreate + && mFileDataManager.getStorageContext().split("/", 3).length > 2) { + mIsFirstLoad = false; + getViewOrThrow().showDirectory(); + return; + } + + mFileDataManager.setStorageContextToRoot(); } public void showMyFiles() { - mFileDataManager.setCurrentStorageContextToMy(); + mFileDataManager.setStorageContextToMy(); getViewOrThrow().showDirectory(); } public void showCourseDirectory(@NonNull String courseId) { diff --git a/app/src/main/java/org/schulcloud/mobile/ui/main/MainActivity.java b/app/src/main/java/org/schulcloud/mobile/ui/main/MainActivity.java index d4b9ea3b..726f4483 100644 --- a/app/src/main/java/org/schulcloud/mobile/ui/main/MainActivity.java +++ b/app/src/main/java/org/schulcloud/mobile/ui/main/MainActivity.java @@ -1,6 +1,7 @@ package org.schulcloud.mobile.ui.main; import android.content.Intent; +import android.net.Uri; import android.os.Bundle; import android.support.annotation.IdRes; import android.support.annotation.NonNull; @@ -23,6 +24,7 @@ import org.schulcloud.mobile.ui.news.NewsFragment; import org.schulcloud.mobile.ui.settings.SettingsActivity; import org.schulcloud.mobile.util.NetworkUtil; +import org.schulcloud.mobile.util.WebUtil; import java.util.List; @@ -46,14 +48,16 @@ public final class MainActivity // private static final int TAB_ADMINISTRATION = R.id.main_navigation_administration; @Inject - MainPresenter mMainPresenter; + MainPresenter mPresenter; + + boolean mIsTopLevelTransaction = true; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); activityComponent().inject(this); - setPresenter(mMainPresenter); - mMainPresenter.checkSignedIn(this); + setPresenter(mPresenter); + mPresenter.checkSignedIn(this); setContentView(R.layout.activity_main); ButterKnife.bind(this); @@ -66,9 +70,11 @@ protected void onCreate(Bundle savedInstanceState) { ((BottomNavigationView) findViewById(R.id.navigation)).setOnNavigationItemSelectedListener( item -> { - mMainPresenter.onTabSelected(getTabIndexById(item.getItemId())); + mPresenter.onTabSelected(getTabIndexById(item.getItemId())); return true; }); + + mPresenter.setStartUrl(getIntent().getData()); } private int getTabIndexById(@IdRes int tabId) { switch (tabId) { @@ -87,6 +93,12 @@ private int getTabIndexById(@IdRes int tabId) { } } + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + if (intent.getData() != null) + WebUtil.openUrl(this, intent.getData()); + } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.activity_main, menu); @@ -112,16 +124,21 @@ public boolean onOptionsItemSelected(MenuItem item) { @NonNull @Override protected String provideDetailedFeedbackContext() { - int currentViewId = mMainPresenter.getCurrentViewId(); + int currentViewId = mPresenter.getCurrentViewId(); return currentViewId + ", " + findFragment(currentViewId).getClass().getSimpleName(); } @Override public void onBackPressed() { - mMainPresenter.onBackPressed(); + mPresenter.onBackPressed(); } - /***** MVP View methods implementation *****/ + /* MVP View methods implementation */ + @Override + public void loadViewForUrl(@NonNull Uri url) { + WebUtil.openUrl(this, url); + } + @Override public int getTabCount() { return 5; @@ -154,9 +171,11 @@ public Pair createInitialView(int tabIndex) { } @Override - public void showView(int oldViewId, int newViewId, @Nullable MainFragment newView, + public synchronized void showView(int oldViewId, int newViewId, @Nullable MainFragment newView, int oldTabIndex, int newTabIndex) { FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + boolean isTopLevelTransaction = mIsTopLevelTransaction; + mIsTopLevelTransaction = false; if (newTabIndex > oldTabIndex) transaction.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left); @@ -172,6 +191,10 @@ else if (newTabIndex < oldTabIndex) transaction.attach(findFragment(newViewId)); transaction.commit(); + if (isTopLevelTransaction) { + getSupportFragmentManager().executePendingTransactions(); + mIsTopLevelTransaction = true; + } } @Override public void removeViews(@NonNull List viewIds) { @@ -188,11 +211,21 @@ public boolean currentViewHandlesBack(int viewId) { private MainFragment findFragment(int viewId) { return (MainFragment) getSupportFragmentManager().findFragmentByTag("" + viewId); } + @NonNull + public MainFragment getCurrentFragment() { + return findFragment(mPresenter.getCurrentViewId()); + } + public void addFragment(@NonNull MainFragment child) { + mPresenter.addView(child.getActivityId(), child); + } public void addFragment(@NonNull MainFragment parent, @NonNull MainFragment child) { - mMainPresenter.addView(parent.getActivityId(), child.getActivityId(), child); + mPresenter.addView(parent.getActivityId(), child.getActivityId(), child); + } + public void navigateToFragment(int tabIndex, int level) { + mPresenter.navigateToView(tabIndex, level); } public void removeFragment(@NonNull MainFragment fragment) { - mMainPresenter.removeView(fragment.getActivityId()); + mPresenter.removeView(fragment.getActivityId()); } } diff --git a/app/src/main/java/org/schulcloud/mobile/ui/main/MainMvpView.java b/app/src/main/java/org/schulcloud/mobile/ui/main/MainMvpView.java index 915c3643..6bb28438 100644 --- a/app/src/main/java/org/schulcloud/mobile/ui/main/MainMvpView.java +++ b/app/src/main/java/org/schulcloud/mobile/ui/main/MainMvpView.java @@ -1,5 +1,6 @@ package org.schulcloud.mobile.ui.main; +import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.util.Pair; @@ -10,6 +11,8 @@ public interface MainMvpView extends MvpView { + void loadViewForUrl(@NonNull Uri url); + int getTabCount(); Pair createInitialView(int tabIndex); diff --git a/app/src/main/java/org/schulcloud/mobile/ui/main/MainPresenter.java b/app/src/main/java/org/schulcloud/mobile/ui/main/MainPresenter.java index 8a64830b..432798d9 100644 --- a/app/src/main/java/org/schulcloud/mobile/ui/main/MainPresenter.java +++ b/app/src/main/java/org/schulcloud/mobile/ui/main/MainPresenter.java @@ -1,6 +1,7 @@ package org.schulcloud.mobile.ui.main; import android.content.Context; +import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.util.Pair; @@ -28,11 +29,13 @@ public class MainPresenter extends BasePresenter> { private static final int TAB_LEVEL_LAST = -1; private static final int TAB_LEVEL_ONE_BACK = -2; - private Subscription mCurrentUserSubscription; private final UserDataManager mUserDataManager; + private Subscription mCurrentUserSubscription; + + private Uri mStartUrl = null; private Stack[] mViewIds; - private Integer mCurrentViewId; + private int mCurrentViewId; private int mCurrentTabIndex; private int mCurrentLevel; @@ -48,9 +51,15 @@ public MainPresenter(UserDataManager userDataManager) { mViewIds[i] = new Stack<>(); showView(0, TAB_LEVEL_TOP, null, false); + if (mStartUrl != null) + v.loadViewForUrl(mStartUrl); }); } + public void setStartUrl(@Nullable Uri startUrl) { + mStartUrl = startUrl; + } + /** * Checks whether there is already a logged-in user, if not so go to sign-in screen */ @@ -86,6 +95,9 @@ public void onTabSelected(int tabIndex) { else // If the user selects the same tab again, navigate back to the first view of the stack showView(tabIndex, TAB_LEVEL_TOP, null, false); } + public void addView(int childId, @NonNull V child) { + addView(getCurrentViewId(), childId, child); + } /** * Adds the view as the child of parent, and shows it. If parent already has a child (and * possibly sub-children), those are removed. @@ -106,6 +118,9 @@ public void addView(int parentId, int childId, @NonNull V child) { break; } } + public void navigateToView(int tabIndex, int level) { + showView(tabIndex, level, null, false); + } /** * Removes the specified view from the hierarchy. Any child views will be removed too. * @@ -138,7 +153,8 @@ public boolean removeView(int viewId) { * @param level The level of the view. {@link #TAB_LEVEL_TOP}, {@link #TAB_LEVEL_LAST} * and {@link #TAB_LEVEL_ONE_BACK} are allowed. * @param closeIfEmpty If set to true and the other parameters would lead to the top level view - * being removed, the app will be closed. Handy for back navigation, but not + * being removed, the app will be closed. Handy for back navigation, but + * not * if a view tries to remove itself. * @return True if the view identified by {@code tabIndex} and {@code level} is now displayed, * false otherwise (e.g., if that would have closed the app and that isn't permitted). @@ -167,17 +183,21 @@ else if (level == TAB_LEVEL_ONE_BACK) popTabStack(tabStack, level + 1); - int viewId = tabStack.get(level); - V newViewFinal = newView; - sendToView(v -> - v.showView(mCurrentViewId, viewId, newViewFinal, mCurrentTabIndex, tabIndex)); + final int oldViewId = mCurrentViewId; + final int viewId = tabStack.get(level); + final V newViewFinal = newView; mCurrentViewId = viewId; mCurrentTabIndex = tabIndex; mCurrentLevel = level; + + sendToView(v -> v.showView(oldViewId, viewId, newViewFinal, mCurrentTabIndex, tabIndex)); return true; } private void popTabStack(@NonNull Stack tabStack, int endSize) { + if (endSize >= tabStack.size()) + return; + List viewIds = new LinkedList<>(); while (tabStack.size() > endSize) viewIds.add(tabStack.pop()); diff --git a/app/src/main/java/org/schulcloud/mobile/ui/news/NewsFragment.java b/app/src/main/java/org/schulcloud/mobile/ui/news/NewsFragment.java index 699662a8..97da2279 100644 --- a/app/src/main/java/org/schulcloud/mobile/ui/news/NewsFragment.java +++ b/app/src/main/java/org/schulcloud/mobile/ui/news/NewsFragment.java @@ -72,7 +72,7 @@ public void onCreate(Bundle savedInstanceState) { } @Override public void onReadArguments(Bundle args) { - if (getArguments().getBoolean(ARGUMENT_TRIGGER_SYNC, true)) + if (args.getBoolean(ARGUMENT_TRIGGER_SYNC, true)) startService(NewsSyncService.getStartIntent(getContext())); } @Nullable diff --git a/app/src/main/java/org/schulcloud/mobile/util/Function.java b/app/src/main/java/org/schulcloud/mobile/util/Function.java new file mode 100644 index 00000000..f17002df --- /dev/null +++ b/app/src/main/java/org/schulcloud/mobile/util/Function.java @@ -0,0 +1,13 @@ +package org.schulcloud.mobile.util; + +import android.support.annotation.NonNull; + +/** + * Date: 2/20/2018 + */ +public interface Function { + + @NonNull + R apply(@NonNull T t); + +} diff --git a/app/src/main/java/org/schulcloud/mobile/util/ListUtils.java b/app/src/main/java/org/schulcloud/mobile/util/ListUtils.java new file mode 100644 index 00000000..bd8e1c7b --- /dev/null +++ b/app/src/main/java/org/schulcloud/mobile/util/ListUtils.java @@ -0,0 +1,19 @@ +package org.schulcloud.mobile.util; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +/** + * Date: 2/19/2018 + */ +public final class ListUtils { + public static boolean contains(@NonNull T[] array, @Nullable T element) { + for (T item : array) + if (equals(item, element)) + return true; + return false; + } + public static boolean equals(@Nullable Object a, @Nullable Object b) { + return (a == b) || (a != null && a.equals(b)); + } +} diff --git a/app/src/main/java/org/schulcloud/mobile/util/PathUtil.java b/app/src/main/java/org/schulcloud/mobile/util/PathUtil.java index eb2abea6..68c25d11 100644 --- a/app/src/main/java/org/schulcloud/mobile/util/PathUtil.java +++ b/app/src/main/java/org/schulcloud/mobile/util/PathUtil.java @@ -1,6 +1,7 @@ package org.schulcloud.mobile.util; import android.support.annotation.NonNull; +import android.text.TextUtils; import java.io.File; @@ -22,6 +23,12 @@ public static String trimSlashes(@NonNull String path) { return trimTrailingSlash(trimLeadingSlash(path)); } @NonNull + public static String ensureLeadingSlash(@NonNull String path) { + if (path.length() == 0 || path.charAt(0) != File.separatorChar) + return File.separator + path; + return path; + } + @NonNull public static String ensureTrailingSlash(@NonNull String path) { if (path.length() == 0 || path.charAt(path.length() - 1) != File.separatorChar) return path + File.separator; @@ -33,9 +40,16 @@ public static String[] getAllParts(@NonNull String path) { return path.split(File.separator); } @NonNull + public static String[] getAllParts(@NonNull String path, int limit) { + return path.split(File.separator, limit); + } + @NonNull public static String combine(@NonNull String... parts) { StringBuilder builder = new StringBuilder(parts[0]); for (int i = 1; i < parts.length; i++) { + if (TextUtils.isEmpty(parts[i])) + continue; + boolean endsWithSeparator = builder.length() > 0 && builder.charAt(builder.length() - 1) == File.separatorChar; boolean beginsWithSeparator = parts[i].length() > 0 diff --git a/app/src/main/java/org/schulcloud/mobile/util/PicassoImageGetter.java b/app/src/main/java/org/schulcloud/mobile/util/PicassoImageGetter.java deleted file mode 100644 index bfe2f3c6..00000000 --- a/app/src/main/java/org/schulcloud/mobile/util/PicassoImageGetter.java +++ /dev/null @@ -1,108 +0,0 @@ -package org.schulcloud.mobile.util; - - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.text.Html; -import android.widget.TextView; - -import com.jakewharton.picasso.OkHttp3Downloader; -import com.squareup.picasso.Picasso; -import com.squareup.picasso.Target; - -import org.schulcloud.mobile.R; - -import java.io.IOException; - -import okhttp3.Interceptor; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; - -public class PicassoImageGetter implements Html.ImageGetter { - - private TextView textView = null; - private Context context = null; - private String accessToken = null; - - public PicassoImageGetter() { - - } - - public PicassoImageGetter(TextView target, Context context, String accessToken) { - textView = target; - this.context = context; - this.accessToken = accessToken; - } - - @Override - public Drawable getDrawable(String source) { - BitmapDrawablePlaceHolder drawable = new BitmapDrawablePlaceHolder(); - - if (!source.contains(context.getString(R.string.web_protocol_http)) - && !source.contains(context.getString(R.string.web_protocol_https))) - source = context.getString(R.string.website) + source; - - OkHttpClient okHttpClient = new OkHttpClient().newBuilder() - .addInterceptor(new Interceptor() { - @Override - public Response intercept(Chain chain) throws IOException { - final Request original = chain.request(); - - final Request authorized = original.newBuilder() - .addHeader("Cookie", "jwt=" + accessToken) - .build(); - - return chain.proceed(authorized); - } - }) - .build(); - - Picasso picasso = new Picasso.Builder(context) - .downloader(new OkHttp3Downloader(okHttpClient)) - .build(); - picasso.load(source).into(drawable); - return drawable; - } - - private class BitmapDrawablePlaceHolder extends BitmapDrawable implements Target { - - protected Drawable drawable; - - @Override - public void draw(final Canvas canvas) { - if (drawable != null) { - drawable.draw(canvas); - } - } - - public void setDrawable(Drawable drawable) { - this.drawable = drawable; - int width = drawable.getIntrinsicWidth(); - int height = drawable.getIntrinsicHeight(); - drawable.setBounds(0, 0, width, height); - setBounds(0, 0, width, height); - if (textView != null) { - textView.setText(textView.getText()); - } - } - - @Override - public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) { - setDrawable(new BitmapDrawable(context.getResources(), bitmap)); - } - - @Override - public void onBitmapFailed(Drawable errorDrawable) { - } - - @Override - public void onPrepareLoad(Drawable placeHolderDrawable) { - - } - - } -} diff --git a/app/src/main/java/org/schulcloud/mobile/util/ViewUtil.java b/app/src/main/java/org/schulcloud/mobile/util/ViewUtil.java index 964a9ac6..e47a56c7 100644 --- a/app/src/main/java/org/schulcloud/mobile/util/ViewUtil.java +++ b/app/src/main/java/org/schulcloud/mobile/util/ViewUtil.java @@ -4,10 +4,13 @@ import android.content.Context; import android.content.res.Resources; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v4.content.res.ResourcesCompat; import android.support.v4.widget.SwipeRefreshLayout; +import android.text.TextUtils; import android.view.View; import android.view.inputmethod.InputMethodManager; +import android.widget.TextView; import org.schulcloud.mobile.R; @@ -32,6 +35,10 @@ public static int boolToVisibility(boolean visible) { public static void setVisibility(@NonNull View view, boolean visible) { view.setVisibility(visible ? View.VISIBLE : View.GONE); } + public static void setText(@NonNull TextView view, @Nullable String content) { + view.setText(content); + view.setVisibility(TextUtils.isEmpty(content) ? View.GONE : View.VISIBLE); + } public static float pxToDp(float px) { float densityDpi = Resources.getSystem().getDisplayMetrics().densityDpi; diff --git a/app/src/main/java/org/schulcloud/mobile/util/WebUtil.java b/app/src/main/java/org/schulcloud/mobile/util/WebUtil.java new file mode 100644 index 00000000..1ff81c9d --- /dev/null +++ b/app/src/main/java/org/schulcloud/mobile/util/WebUtil.java @@ -0,0 +1,260 @@ +package org.schulcloud.mobile.util; + +import android.content.Context; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.customtabs.CustomTabsIntent; +import android.support.v4.content.ContextCompat; +import android.text.TextUtils; +import android.util.Log; +import android.widget.Toast; + +import org.schulcloud.mobile.BuildConfig; +import org.schulcloud.mobile.R; +import org.schulcloud.mobile.data.datamanagers.FileDataManager; +import org.schulcloud.mobile.ui.courses.CourseFragment; +import org.schulcloud.mobile.ui.courses.detailed.DetailedCourseFragment; +import org.schulcloud.mobile.ui.courses.topic.TopicFragment; +import org.schulcloud.mobile.ui.dashboard.DashboardFragment; +import org.schulcloud.mobile.ui.files.FilesFragment; +import org.schulcloud.mobile.ui.files.overview.FileOverviewFragment; +import org.schulcloud.mobile.ui.homework.HomeworkFragment; +import org.schulcloud.mobile.ui.homework.detailed.DetailedHomeworkFragment; +import org.schulcloud.mobile.ui.main.MainActivity; +import org.schulcloud.mobile.ui.main.MainFragment; +import org.schulcloud.mobile.ui.news.NewsFragment; +import org.schulcloud.mobile.ui.news.detailed.DetailedNewsFragment; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import rx.Single; + +/** + * Date: 2/17/2018 + */ +public final class WebUtil { + private static final String TAG = WebUtil.class.getSimpleName(); + + public static final String HEADER_COOKIE = "cookie"; + public static final String HEADER_CONTENT_TYPE = "content-type"; + public static final String HEADER_CONTENT_ENCODING = "content-encoding"; + + public static final String MIME_TEXT_PLAIN = "text/plain"; + public static final String MIME_TEXT_HTML = "text/html"; + public static final String MIME_APPLICATION_JSON = "application/json"; + + public static final String ENCODING_UTF_8 = "utf-8"; + + public static final String SCHEME_HTTP = "http"; + public static final String SCHEME_HTTPS = "https"; + + public static final String HOST_SCHULCLOUD_ORG = "schulcloud.org"; + public static final String HOST_SCHUL_CLOUD_ORG = "schul-cloud.org"; + + public static final String URL_BASE_API = BuildConfig.URL_ENDPOINT; + public static final String URL_BASE = SCHEME_HTTPS + "://" + HOST_SCHUL_CLOUD_ORG; + + // Internal paths + public static final String PATH_INTERNAL_DASHBOARD = "dashboard"; + + public static final String PATH_INTERNAL_NEWS = "news"; + + public static final String PATH_INTERNAL_COURSES = "courses"; + public static final String PATH_INTERNAL_COURSES_TOPICS = "topics"; + public static final String PATH_INTERNAL_COURSES_TOOLS = "tools"; + public static final String PATH_INTERNAL_COURSES_GROUPS = "groups"; + + public static final String PATH_INTERNAL_HOMEWORK = "homework"; + public static final String PATH_INTERNAL_HOMEWORK_ASKED = "asked"; + public static final String PATH_INTERNAL_HOMEWORK_PRIVATE = "private"; + public static final String PATH_INTERNAL_HOMEWORK_ARCHIVE = "archive"; + public static final String[] PATHS_INTERNAL_HOMEWORK = { + PATH_INTERNAL_HOMEWORK_ASKED, + PATH_INTERNAL_HOMEWORK_PRIVATE, + PATH_INTERNAL_HOMEWORK_ARCHIVE}; + + public static final String PATH_INTERNAL_FILES = "files"; + public static final String PATH_INTERNAL_FILES_PARAM_DIR = "dir"; + public static final String PATH_INTERNAL_FILES_PARAM_PATH = "path"; + public static final String PATH_INTERNAL_FILES_PARAM_FILE = "file"; + public static final String PATH_INTERNAL_FILES_MY = "my"; + public static final String PATH_INTERNAL_FILES_COURSES = "courses"; + public static final String PATH_INTERNAL_FILES_SHARED = "shared"; + public static final String PATH_INTERNAL_FILES_FILE = "file"; + public static final String[] PATHS_INTERNAL_FILES = { + PATH_INTERNAL_FILES_MY, + PATH_INTERNAL_FILES_COURSES, + PATH_INTERNAL_FILES_SHARED, + PATH_INTERNAL_FILES_FILE}; + + public static final String[] PATHS_INTERNAL = { + PATH_INTERNAL_DASHBOARD, + PATH_INTERNAL_NEWS, + PATH_INTERNAL_COURSES, + PATH_INTERNAL_HOMEWORK, + PATH_INTERNAL_FILES}; + + @NonNull + public static Single resolveRedirect(@NonNull String url, @NonNull String accessToken) { + if (url.charAt(0) != '/') + return Single.just(Uri.parse(url)); + + return Single.create(subscriber -> { + OkHttpClient okHttpClient = + new OkHttpClient().newBuilder().addInterceptor(chain -> chain + .proceed(chain.request().newBuilder() + .addHeader(HEADER_COOKIE, "jwt=" + accessToken).build())) + .build(); + try { + Response response = okHttpClient + .newCall(new Request.Builder().url(PathUtil.combine(URL_BASE_API, url)) + .build()) + .execute(); + subscriber.onSuccess(Uri.parse(response.request().url().toString())); + } catch (IOException e) { + Log.w(TAG, "Error resolving internal redirect", e); + subscriber.onError(e); + } + }); + } + + @NonNull + public static CustomTabsIntent newCustomTab(@NonNull Context context) { + CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder(); + builder.setToolbarColor(ContextCompat.getColor(context, R.color.hpiRed)); + builder.setStartAnimations(context, R.anim.slide_in_right, R.anim.slide_out_left); + builder.setExitAnimations(context, R.anim.slide_in_left, R.anim.slide_out_right); + builder.addDefaultShareMenuItem(); + return builder.build(); + } + + public static void openUrl(@NonNull MainActivity mainActivity, @NonNull Uri url) { + openUrl(mainActivity, null, url); + } + public static void openUrl(@NonNull MainActivity mainActivity, @Nullable MainFragment fragment, + @NonNull Uri url) { + Log.i(TAG, "Opening url: " + url); + String scheme = url.getScheme().toLowerCase(); + String host = url.getHost().toLowerCase(); + List path = url.getPathSegments(); + String pathPrefix = null; + String pathEnd = null; + if (!path.isEmpty()) { + pathPrefix = path.get(0); + pathEnd = path.get(path.size() - 1).toLowerCase(); + } + + // Internal links can be handled by the app + if ((scheme.equals(SCHEME_HTTP) || scheme.equals(SCHEME_HTTPS)) + && (host.equals(HOST_SCHULCLOUD_ORG) || host.equals(HOST_SCHUL_CLOUD_ORG)) + && path.size() > 0 + && ListUtils.contains(PATHS_INTERNAL, pathPrefix)) { + MainFragment newFragment = null; + assert pathPrefix != null; + switch (pathPrefix) { + case PATH_INTERNAL_DASHBOARD: + if (!(fragment instanceof DashboardFragment)) + newFragment = DashboardFragment.newInstance(); + break; + + case PATH_INTERNAL_NEWS: + if (path.size() == 1) + newFragment = NewsFragment.newInstance(); + else if (path.size() == 2) + newFragment = DetailedNewsFragment.newInstance(pathEnd); + break; + + case PATH_INTERNAL_COURSES: + if (path.size() == 1) + newFragment = CourseFragment.newInstance(); + else if (path.size() == 2 || path.size() == 3) + newFragment = DetailedCourseFragment.newInstance(pathEnd); + else if (path.size() == 4 && path.get(3).equals(PATH_INTERNAL_COURSES_TOPICS)) + newFragment = TopicFragment.newInstance(pathEnd); + break; + + case PATH_INTERNAL_HOMEWORK: + if (path.size() == 1) + newFragment = HomeworkFragment.newInstance(); + else if (path.size() == 2 + && !ListUtils.contains(PATHS_INTERNAL_HOMEWORK, pathEnd)) + newFragment = DetailedHomeworkFragment.newInstance(pathEnd); + break; + + case PATH_INTERNAL_FILES: + if (path.size() == 1) + newFragment = FileOverviewFragment.newInstance(false); + else if (path.size() == 2 + && ListUtils.contains(PATHS_INTERNAL_FILES, pathEnd)) { + String[] filePath = parseUrlPath(url); + if (filePath != null) + newFragment = FilesFragment.newInstance(filePath[0], filePath[1]); + } + break; + } + Log.i(TAG, "Chosen fragment: " + newFragment); + if (newFragment != null) { + if (fragment == null) + mainActivity.addFragment(newFragment); + else + mainActivity.addFragment(fragment, newFragment); + return; + } else { + Toast.makeText(mainActivity, R.string.web_error_linkNotSupported, + Toast.LENGTH_SHORT).show(); + return; + } + } + + newCustomTab(mainActivity).launchUrl(mainActivity, url); + } + @Nullable + private static String[] parseUrlPath(@NonNull Uri url) { + List segments = url.getPathSegments(); + if (!segments.get(0).toLowerCase().equals(PATH_INTERNAL_FILES)) + return null; + + String path; + String file = null; + String dir; + switch (segments.get(1).toLowerCase()) { + case PATH_INTERNAL_FILES_MY: + path = FileDataManager.CONTEXT_MY; + dir = url.getQueryParameter(PATH_INTERNAL_FILES_PARAM_DIR); + if (dir != null) + path = PathUtil.combine(path, dir); + break; + + case PATH_INTERNAL_FILES_COURSES: + path = FileDataManager.CONTEXT_COURSES; + if (segments.size() > 2) { + path = PathUtil.combine(path, segments.get(2)); + dir = url.getQueryParameter(PATH_INTERNAL_FILES_PARAM_DIR); + if (dir != null) + path = PathUtil.combine(path, dir); + } + break; + + case PATH_INTERNAL_FILES_FILE: + path = url.getQueryParameter(PATH_INTERNAL_FILES_PARAM_FILE); + if (TextUtils.isEmpty(path)) + path = url.getQueryParameter(PATH_INTERNAL_FILES_PARAM_PATH); + + String[] pathSegments = PathUtil.getAllParts(path); + file = pathSegments[pathSegments.length - 1]; + path = PathUtil.combine(Arrays.copyOf(pathSegments, pathSegments.length - 1)); + break; + + case PATH_INTERNAL_FILES_SHARED: // Not supported yet + default: + return null; + } + return new String[]{path, file}; + } +} diff --git a/app/src/main/res/layouts/courses/drawable/sc_card_footer.xml b/app/src/main/res/layouts/courses/drawable/sc_card_footer.xml new file mode 100644 index 00000000..9dd6f616 --- /dev/null +++ b/app/src/main/res/layouts/courses/drawable/sc_card_footer.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layouts/courses/layout/fragment_topic.xml b/app/src/main/res/layouts/courses/layout/fragment_topic.xml index 87df2f8a..df493f34 100644 --- a/app/src/main/res/layouts/courses/layout/fragment_topic.xml +++ b/app/src/main/res/layouts/courses/layout/fragment_topic.xml @@ -1,29 +1,31 @@ - - + android:descendantFocusability="blocksDescendants" + android:orientation="vertical"> - + + + + + - + diff --git a/app/src/main/res/layouts/courses/layout/item_content.xml b/app/src/main/res/layouts/courses/layout/item_content.xml deleted file mode 100644 index e15c0945..00000000 --- a/app/src/main/res/layouts/courses/layout/item_content.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/layouts/courses/layout/item_content_etherpad.xml b/app/src/main/res/layouts/courses/layout/item_content_etherpad.xml new file mode 100644 index 00000000..2b8b3435 --- /dev/null +++ b/app/src/main/res/layouts/courses/layout/item_content_etherpad.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layouts/courses/layout/item_content_geogebra.xml b/app/src/main/res/layouts/courses/layout/item_content_geogebra.xml new file mode 100644 index 00000000..0478519d --- /dev/null +++ b/app/src/main/res/layouts/courses/layout/item_content_geogebra.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layouts/courses/layout/item_content_nexboard.xml b/app/src/main/res/layouts/courses/layout/item_content_nexboard.xml new file mode 100644 index 00000000..0400e774 --- /dev/null +++ b/app/src/main/res/layouts/courses/layout/item_content_nexboard.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layouts/courses/layout/item_content_resources.xml b/app/src/main/res/layouts/courses/layout/item_content_resources.xml new file mode 100644 index 00000000..74c8672c --- /dev/null +++ b/app/src/main/res/layouts/courses/layout/item_content_resources.xml @@ -0,0 +1,27 @@ + + + + + + + + diff --git a/app/src/main/res/layouts/courses/layout/item_content_text.xml b/app/src/main/res/layouts/courses/layout/item_content_text.xml new file mode 100644 index 00000000..d1a371d1 --- /dev/null +++ b/app/src/main/res/layouts/courses/layout/item_content_text.xml @@ -0,0 +1,28 @@ + + + + + + + + diff --git a/app/src/main/res/layouts/courses/layout/item_content_unsupported.xml b/app/src/main/res/layouts/courses/layout/item_content_unsupported.xml new file mode 100644 index 00000000..c1a09487 --- /dev/null +++ b/app/src/main/res/layouts/courses/layout/item_content_unsupported.xml @@ -0,0 +1,28 @@ + + + + + + + + diff --git a/app/src/main/res/layouts/courses/layout/item_course.xml b/app/src/main/res/layouts/courses/layout/item_course.xml index 5e94e6f7..a4dfda9b 100644 --- a/app/src/main/res/layouts/courses/layout/item_course.xml +++ b/app/src/main/res/layouts/courses/layout/item_course.xml @@ -7,6 +7,7 @@ style="@style/Material.Card.Margin" android:layout_width="match_parent" android:layout_height="wrap_content" + android:foreground="?android:attr/selectableItemBackground" app:contentPadding="0dp"> diff --git a/app/src/main/res/layouts/courses/layout/item_news.xml b/app/src/main/res/layouts/courses/layout/item_news.xml index 8d7bc5cb..cb8bf254 100644 --- a/app/src/main/res/layouts/courses/layout/item_news.xml +++ b/app/src/main/res/layouts/courses/layout/item_news.xml @@ -20,7 +20,7 @@ android:layout_marginBottom="0dp" android:ellipsize="end" android:maxLines="2" - android:textSize="@dimen/text_large_body" + android:textSize="@dimen/text_body_large" tools:text="Title" /> + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layouts/courses/layout/item_topic.xml b/app/src/main/res/layouts/courses/layout/item_topic.xml index c46435d4..da67923c 100644 --- a/app/src/main/res/layouts/courses/layout/item_topic.xml +++ b/app/src/main/res/layouts/courses/layout/item_topic.xml @@ -5,15 +5,17 @@ android:id="@+id/card_view" style="@style/Material.Card.Margin" android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_height="wrap_content" + android:foreground="?android:attr/selectableItemBackground"> diff --git a/app/src/main/res/layouts/files/layout/fragment_file_overview.xml b/app/src/main/res/layouts/files/layout/fragment_file_overview.xml index 11886935..7a076416 100644 --- a/app/src/main/res/layouts/files/layout/fragment_file_overview.xml +++ b/app/src/main/res/layouts/files/layout/fragment_file_overview.xml @@ -13,15 +13,14 @@ + android:layout_height="fill_parent"> + android:orientation="vertical" + android:paddingBottom="8dp"> diff --git a/app/src/main/res/layouts/homework/layout/item_homework.xml b/app/src/main/res/layouts/homework/layout/item_homework.xml index 669a9c4f..191233a8 100644 --- a/app/src/main/res/layouts/homework/layout/item_homework.xml +++ b/app/src/main/res/layouts/homework/layout/item_homework.xml @@ -38,7 +38,7 @@ android:layout_marginTop="5dp" android:ellipsize="end" android:maxLines="2" - android:textSize="@dimen/text_small_body" + android:textSize="@dimen/text_body_small" tools:text="Description...\n..." /> diff --git a/app/src/main/res/layouts/navigationDrawer/layout/action_bar.xml b/app/src/main/res/layouts/navigationDrawer/layout/action_bar.xml index 8343dab4..bd5b1036 100644 --- a/app/src/main/res/layouts/navigationDrawer/layout/action_bar.xml +++ b/app/src/main/res/layouts/navigationDrawer/layout/action_bar.xml @@ -14,7 +14,7 @@ android:layout_marginLeft="32dp" android:layout_marginStart="32dp" android:text="@string/base_offline" - android:textSize="@dimen/text_small_body" + android:textSize="@dimen/text_body_small" android:visibility="invisible" /> diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 3de1a5dd..8c8fb109 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -5,12 +5,12 @@ 16dp 24sp - 22sp + 22sp 20sp - 18sp + 18sp 16sp - 14sp - 10sp + 14sp + 10sp 16dp 4sp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 37fde1e7..fce76eee 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -60,6 +60,7 @@ Löschen @string/files_file_action_delete Oops! Irgendwas ist beim Laden der Dateien schief gelaufen! + Die Datei \'%1$s\' wurde nicht gefunden Die Datei wird geladen… Datei \'%1$s\' wurde erfolgreich in Downloads gespeichert! Die Datei konnte nicht geladen werden @@ -85,11 +86,14 @@ Kurse - %s wird zurzeit nicht unterstüzt. + %s wird zurzeit nicht unterstützt Leider gab es ein Problem beim Laden der Kurse Kurs Leider gab es ein Problem beim Laden des Kurses + Das Thema konnte nicht geladen werden Thema + via %1$s + Es gab einen Fehler beim Laden des Materials Aufgaben @@ -158,6 +162,7 @@ http:// https:// https://schul-cloud.org + Dieser Link kann aktuell nicht geöffnet werden OK diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index cbdd18ad..f0650952 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -18,19 +18,19 @@ - + + + + + + + - +