diff --git a/app/build.gradle b/app/build.gradle index 614d49ac91c..de7b409fadc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -40,6 +40,7 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + } ext { @@ -99,4 +100,6 @@ dependencies { implementation "com.squareup.okhttp3:okhttp:${okHttpLibVersion}" debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoLibVersion}" + + implementation 'com.google.crypto.tink:tink-android:1.3.0-rc1' } diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index 3ac2d50144e..f193f34b2d9 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -6,9 +6,9 @@ import android.app.NotificationManager; import android.content.Context; import android.os.Build; -import android.preference.PreferenceManager; import android.support.annotation.Nullable; import android.util.Log; +import android.widget.Toast; import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache; import com.nostra13.universalimageloader.core.ImageLoader; @@ -21,9 +21,10 @@ import org.acra.config.ACRAConfigurationException; import org.acra.config.ConfigurationBuilder; import org.acra.sender.ReportSenderFactory; +import org.schabi.newpipe.database.Database; +import org.schabi.newpipe.database.RemoteDatabase; import org.schabi.newpipe.extractor.Downloader; import org.schabi.newpipe.extractor.NewPipe; -import org.schabi.newpipe.extractor.utils.Localization; import org.schabi.newpipe.report.AcraReportSenderFactory; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; @@ -37,6 +38,7 @@ import java.util.Collections; import java.util.List; +import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.annotations.NonNull; import io.reactivex.exceptions.CompositeException; import io.reactivex.exceptions.MissingBackpressureException; @@ -107,6 +109,14 @@ public void onCreate() { // Check for new version new CheckForNewAppVersionTask().execute(); + + Database database = NewPipeDatabase.getInstance(this); + if(database instanceof RemoteDatabase){ + ((RemoteDatabase) database).sync().observeOn(AndroidSchedulers.mainThread()).subscribe(() -> {}, e -> { + Toast.makeText(this, "Failed to sync data from server", Toast.LENGTH_SHORT).show(); + }); + } + } protected Downloader getDownloader() { diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index a9f2e962239..1ac7445ec87 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -49,6 +49,7 @@ import android.widget.ImageView; import android.widget.TextView; +import org.schabi.newpipe.auth.AuthService; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ExtractionException; @@ -529,7 +530,14 @@ private void initFragments() { StateSaver.clearStateFiles(); if (getIntent() != null && getIntent().hasExtra(Constants.KEY_LINK_TYPE)) { handleIntent(getIntent()); - } else NavigationHelper.gotoMainFragment(getSupportFragmentManager()); + } else { + AuthService authService = AuthService.getInstance(getApplicationContext()); + if(!authService.isLoggedIn() && !authService.skipLogin()){ + NavigationHelper.openLoginFragment(getSupportFragmentManager()); + }else{ + NavigationHelper.gotoMainFragment(getSupportFragmentManager()); + } + } } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java index 189e5aeab69..d6f45c33843 100644 --- a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java @@ -4,39 +4,71 @@ import android.content.Context; import android.support.annotation.NonNull; +import org.schabi.newpipe.auth.AuthService; import org.schabi.newpipe.database.AppDatabase; +import org.schabi.newpipe.database.Database; +import org.schabi.newpipe.database.RemoteDatabaseImpl; import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME; import static org.schabi.newpipe.database.Migrations.MIGRATION_11_12; +import static org.schabi.newpipe.database.Migrations.MIGRATION_12_13; public final class NewPipeDatabase { - private static volatile AppDatabase databaseInstance; + private static volatile Database remoteDatabaseInstance; + private static volatile Database localDatabaseInstance; private NewPipeDatabase() { //no instance } - private static AppDatabase getDatabase(Context context) { + private static Database getRemoteDatabase(AppDatabase roomDb, Context context) { + return new RemoteDatabaseImpl(roomDb, context); + } + + private static Database getLocalDatabase(Context context) { return Room .databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME) - .addMigrations(MIGRATION_11_12) + .addMigrations(MIGRATION_11_12, MIGRATION_12_13) .fallbackToDestructiveMigration() .build(); } @NonNull - public static AppDatabase getInstance(@NonNull Context context) { - AppDatabase result = databaseInstance; - if (result == null) { + private static Database getRemoteInstance(@NonNull Context context) { + AppDatabase roomDb = (AppDatabase) getLocalInstance(context); + if (remoteDatabaseInstance == null) { synchronized (NewPipeDatabase.class) { - result = databaseInstance; - if (result == null) { - databaseInstance = (result = getDatabase(context)); + if (remoteDatabaseInstance == null) { + remoteDatabaseInstance = getRemoteDatabase(roomDb, context); } } } - return result; + return remoteDatabaseInstance; } + + @NonNull + private static Database getLocalInstance(@NonNull Context context) { + if (localDatabaseInstance == null) { + synchronized (NewPipeDatabase.class) { + if (localDatabaseInstance == null) { + localDatabaseInstance = getLocalDatabase(context); + } + } + } + + return localDatabaseInstance; + } + + @NonNull + public static Database getInstance(@NonNull Context context) { + boolean loggedIn = AuthService.getInstance(context).isLoggedIn(); + if(loggedIn){ + return getRemoteInstance(context); + }else { + return getLocalInstance(context); + } + } + } diff --git a/app/src/main/java/org/schabi/newpipe/auth/AuthService.java b/app/src/main/java/org/schabi/newpipe/auth/AuthService.java new file mode 100644 index 00000000000..89971408b48 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/auth/AuthService.java @@ -0,0 +1,168 @@ +package org.schabi.newpipe.auth; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.grack.nanojson.JsonBuilder; +import com.grack.nanojson.JsonObject; +import com.grack.nanojson.JsonParser; +import com.grack.nanojson.JsonWriter; + +import org.schabi.newpipe.database.RemoteDatabaseClient; +import org.schabi.newpipe.database.RemoteDatabaseException; + +import java.security.NoSuchAlgorithmException; +import java.util.Objects; + +import io.reactivex.Completable; +import io.reactivex.schedulers.Schedulers; + +public class AuthService implements SharedPreferences.OnSharedPreferenceChangeListener { + + private static volatile AuthService instance; + + public static final String LOGIN_ENDPOINT = "/auth/signin"; + public static final String SIGNUP_ENDPOINT = "/auth/signup"; + + private final Context context; + private final SharedPreferences sharedPreferences; + private final RemoteDatabaseClient client; + + private String username; + private String authToken; + private Boolean loggedIn; + + public static AuthService getInstance(@NonNull Context context) { + if (instance == null) { + synchronized (AuthService.class) { + if (instance == null) { + instance = new AuthService(context); + } + } + } + + return instance; + } + + private AuthService(Context context) { + this.context = context; + this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + this.sharedPreferences.registerOnSharedPreferenceChangeListener(this); + this.client = RemoteDatabaseClient.getInstance(context); + } + + public boolean isLoggedIn(){ + if(null == loggedIn){ + loggedIn = sharedPreferences.getBoolean("logged_in", false); + } + return loggedIn; + } + + public boolean skipLogin(){ + return sharedPreferences.getBoolean("skip_login", false); + } + + public Completable login(String username, String password){ + + return Completable.fromAction(() -> { + String req = buildRequest(username, password); + String response = client.post(LOGIN_ENDPOINT, req); + JsonObject jsonObject = JsonParser.object().from(response); + String tokenType = jsonObject.getString("tokenType"); + String accessToken = jsonObject.getString("accessToken"); + if(null == tokenType || null == accessToken) throw new RemoteDatabaseException("Unable to authenticate"); + + // create encryption key + EncryptionUtils encryptionUtils = EncryptionUtils.getInstance(context); + encryptionUtils.createKey(context, username, password); + + // update shared prefs + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString("username", username); + editor.putString("token", tokenType + " " + accessToken); + editor.putBoolean("logged_in", true); + editor.apply(); + this.username = username; + this.authToken = tokenType + " " + accessToken; + this.loggedIn = true; + }).subscribeOn(Schedulers.io()); + + } + + public Completable logout(){ + + return Completable.fromAction(() -> { + // delete encryption key + EncryptionUtils.getInstance(context).deleteKey(context); + // update shared prefs + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.remove("username"); + editor.remove("token"); + editor.putBoolean("logged_in", false); + editor.apply(); + this.username = null; + this.authToken = null; + this.loggedIn = false; + }).subscribeOn(Schedulers.io()); + + } + + public Completable signup(String username, String password){ + + return Completable.fromAction(() -> { + String req = buildRequest(username, password); + client.post(SIGNUP_ENDPOINT, req); + }).andThen(login(username, password)).subscribeOn(Schedulers.io()); + + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if(key.equals("token")){ + authToken = sharedPreferences.getString(key, null); + }else if(key.equals("logged_in")){ + loggedIn = sharedPreferences.getBoolean(key, false); + }else if(key.equals("username")){ + username = sharedPreferences.getString(key, null); + } + } + + @Nullable + private String getHash(String input){ + String hash = null; + try { + hash = EncryptionUtils.getSHA(input); + } catch (NoSuchAlgorithmException e) { + // ? + } + return hash; + } + + private String buildRequest(String username, String password){ + String usernameHash = getHash(username); + String passwordHash = getHash(password); + Objects.requireNonNull(usernameHash, "failed to calculate username hash"); + Objects.requireNonNull(passwordHash, "failed to calculate password hash"); + JsonBuilder json = JsonObject.builder(); + json.value("username", usernameHash); + json.value("password", passwordHash); + return JsonWriter.string(json.done()); + } + + public String getToken(){ + if(null == authToken){ + authToken = sharedPreferences.getString("token", null); + } + return authToken; + } + + public String getUsername(){ + if(null == username){ + username = sharedPreferences.getString("username", null); + } + return username; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/auth/EncryptionUtils.java b/app/src/main/java/org/schabi/newpipe/auth/EncryptionUtils.java new file mode 100644 index 00000000000..62199a9be2a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/auth/EncryptionUtils.java @@ -0,0 +1,155 @@ +package org.schabi.newpipe.auth; + +import android.content.Context; +import android.util.Log; + +import com.google.crypto.tink.DeterministicAead; +import com.google.crypto.tink.Registry; +import com.google.crypto.tink.daead.DeterministicAeadConfig; +import com.google.crypto.tink.proto.AesSivKey; +import com.google.crypto.tink.subtle.Base64; +import com.google.protobuf.ByteString; + +import org.schabi.newpipe.MainActivity; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; + +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; + +public class EncryptionUtils { + + private EncryptionUtils() { + } + + private static volatile EncryptionUtils instance; + + private DeterministicAead daead; + + public static final String ENC_KEY_FILE = "encKeyFile"; + + private final String TAG = "EncryptionUtils@" + Integer.toHexString(hashCode()); + private final boolean DEBUG = MainActivity.DEBUG; + + public static EncryptionUtils getInstance(Context context) throws GeneralSecurityException, IOException { + + if (instance == null) { + synchronized (EncryptionUtils.class) { + if (instance == null) { + DeterministicAeadConfig.register(); + instance = new EncryptionUtils(); + instance.loadKey(context); + } + } + } + return instance; + } + + private void loadKey(Context context) throws GeneralSecurityException { + try(FileInputStream fis = context.openFileInput(ENC_KEY_FILE)){ + AesSivKey aesSivKey = AesSivKey.parseFrom(fis); + this.daead = Registry.getPrimitive(DeterministicAeadConfig.AES_SIV_TYPE_URL, aesSivKey, DeterministicAead.class); + } catch (IOException e) { + if(DEBUG) Log.d(TAG, "unable to load encKey", e); + } + } + + protected void createKey(Context context, String username, String password) throws IOException, GeneralSecurityException { + SecretKey key = generateKey(password.toCharArray(), username.getBytes(StandardCharsets.UTF_8)); + AesSivKey aesSivKey = AesSivKey.newBuilder().setKeyValue(ByteString.copyFrom(key.getEncoded())).build(); + + try(FileOutputStream fos = context.openFileOutput(ENC_KEY_FILE, Context.MODE_PRIVATE)){ + aesSivKey.writeTo(fos); + } + + this.daead = Registry.getPrimitive(DeterministicAeadConfig.AES_SIV_TYPE_URL, aesSivKey, DeterministicAead.class); + } + + protected void deleteKey(Context context){ + context.deleteFile(EncryptionUtils.ENC_KEY_FILE); + this.daead = null; + } + + private String encrypt(byte[] plainText) throws GeneralSecurityException { + assertDaeadNotNull(); + byte[] cipherText = daead.encryptDeterministically(plainText, "".getBytes()); + return Base64.encode(cipherText); + } + + public String encrypt(long data) throws GeneralSecurityException { + byte[] plainText = BigInteger.valueOf(data).toByteArray(); + return encrypt(plainText); + } + + public String encrypt(String data) throws GeneralSecurityException { + byte[] plainText = data.getBytes(StandardCharsets.UTF_8); + return encrypt(plainText); + } + + private byte[] decrypt(byte[] cipherText) throws GeneralSecurityException { + assertDaeadNotNull(); + byte[] decrypted = daead.decryptDeterministically(cipherText, "".getBytes()); + return decrypted; + } + + public long decryptAsLong(String data) throws GeneralSecurityException { + byte[] decoded = Base64.decode(data); + byte[] decrypted = decrypt(decoded); + BigInteger bigInteger = new BigInteger(decrypted); + return bigInteger.longValue(); + } + + public String decrypt(String data) throws GeneralSecurityException { + byte[] decoded = Base64.decode(data); + byte[] decrypted = decrypt(decoded); + return new String(decrypted, StandardCharsets.UTF_8); + } + + private void assertDaeadNotNull(){ + if(null == daead) throw new IllegalStateException("encryption key not set"); + } + + public SecretKey generateKey(char[] passphraseOrPin, byte[] salt) throws NoSuchAlgorithmException, InvalidKeySpecException { + // Number of PBKDF2 hardening rounds to use. Larger values increase + // computation time. You should select a value that causes computation + // to take >100ms. + final int iterations = 1000; + + // Generate a 512-bit key + final int outputKeyLength = 512; + + SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + KeySpec keySpec = new PBEKeySpec(passphraseOrPin, salt, iterations, outputKeyLength); + SecretKey secretKey = secretKeyFactory.generateSecret(keySpec); + return secretKey; + } + + public static String getSHA(String input) throws NoSuchAlgorithmException + { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] hash = md.digest(input.getBytes(StandardCharsets.UTF_8)); + return toHexString(hash); + } + + public static String toHexString(byte[] hash) + { + BigInteger number = new BigInteger(1, hash); + StringBuilder hexString = new StringBuilder(number.toString(16)); + + while (hexString.length() < 32) { + hexString.insert(0, '0'); + } + + return hexString.toString(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java index 145a77c702f..76a74309474 100644 --- a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java @@ -21,7 +21,7 @@ import org.schabi.newpipe.database.subscription.SubscriptionDAO; import org.schabi.newpipe.database.subscription.SubscriptionEntity; -import static org.schabi.newpipe.database.Migrations.DB_VER_12_0; +import static org.schabi.newpipe.database.Migrations.DB_VER_13_0; @TypeConverters({Converters.class}) @Database( @@ -30,10 +30,10 @@ StreamEntity.class, StreamHistoryEntity.class, StreamStateEntity.class, PlaylistEntity.class, PlaylistStreamEntity.class, PlaylistRemoteEntity.class }, - version = DB_VER_12_0, + version = DB_VER_13_0, exportSchema = false ) -public abstract class AppDatabase extends RoomDatabase { +public abstract class AppDatabase extends RoomDatabase implements org.schabi.newpipe.database.Database { public static final String DATABASE_NAME = "newpipe.db"; diff --git a/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java b/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java index 13117145aeb..071ed7dd940 100644 --- a/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java @@ -43,4 +43,6 @@ public interface BasicDAO { @Update void update(final Collection entities); + + void destroyAndRefill(final Collection entities); } diff --git a/app/src/main/java/org/schabi/newpipe/database/Database.java b/app/src/main/java/org/schabi/newpipe/database/Database.java new file mode 100644 index 00000000000..38cf997f29a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/Database.java @@ -0,0 +1,28 @@ +package org.schabi.newpipe.database; + +import org.schabi.newpipe.database.history.dao.SearchHistoryDAO; +import org.schabi.newpipe.database.history.dao.StreamHistoryDAO; +import org.schabi.newpipe.database.playlist.dao.PlaylistDAO; +import org.schabi.newpipe.database.playlist.dao.PlaylistRemoteDAO; +import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO; +import org.schabi.newpipe.database.stream.dao.StreamDAO; +import org.schabi.newpipe.database.stream.dao.StreamStateDAO; +import org.schabi.newpipe.database.subscription.SubscriptionDAO; + +public interface Database { + SubscriptionDAO subscriptionDAO(); + + SearchHistoryDAO searchHistoryDAO(); + + StreamDAO streamDAO(); + + StreamHistoryDAO streamHistoryDAO(); + + StreamStateDAO streamStateDAO(); + + PlaylistDAO playlistDAO(); + + PlaylistStreamDAO playlistStreamDAO(); + + PlaylistRemoteDAO playlistRemoteDAO(); +} diff --git a/app/src/main/java/org/schabi/newpipe/database/Migrations.java b/app/src/main/java/org/schabi/newpipe/database/Migrations.java index 9d24dbb08ee..07207087486 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Migrations.java +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.java @@ -11,6 +11,7 @@ public class Migrations { public static final int DB_VER_11_0 = 1; public static final int DB_VER_12_0 = 2; + public static final int DB_VER_13_0 = 3; public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release"); private static final String TAG = Migrations.class.getName(); @@ -71,4 +72,63 @@ public void migrate(@NonNull SupportSQLiteDatabase database) { } } }; + + public static final Migration MIGRATION_12_13 = new Migration(DB_VER_12_0, DB_VER_13_0) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + if(DEBUG) { + Log.d(TAG, "Start migrating database"); + } + /* + * Unfortunately these queries must be hardcoded due to the possibility of + * schema and names changing at a later date, thus invalidating the older migration + * scripts if they are not hardcoded. + * */ + + // update stream_state table + database.execSQL("CREATE TABLE IF NOT EXISTS `new_stream_state` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )"); + database.execSQL("CREATE UNIQUE INDEX `index_stream_state_stream_id` ON `new_stream_state` (`stream_id`)"); + + database.execSQL("INSERT INTO new_stream_state (stream_id, progress_time) SELECT stream_id, progress_time FROM stream_state"); + + database.execSQL("DROP TABLE IF EXISTS stream_state"); + database.execSQL("ALTER TABLE new_stream_state RENAME TO stream_state"); + + + // update stream_history table + database.execSQL("CREATE TABLE IF NOT EXISTS `new_stream_history` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )"); + database.execSQL("CREATE UNIQUE INDEX `index_stream_history_stream_id_access_date` ON `new_stream_history` (`stream_id`, `access_date`)"); + + database.execSQL("INSERT INTO new_stream_history (stream_id, access_date, repeat_count) SELECT stream_id, access_date, repeat_count FROM stream_history"); + + database.execSQL("DROP TABLE IF EXISTS stream_history"); + database.execSQL("ALTER TABLE new_stream_history RENAME TO stream_history"); + + // update search_history table + database.execSQL("CREATE TABLE IF NOT EXISTS `new_search_history` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `search` TEXT, `creation_date` INTEGER, `service_id` INTEGER NOT NULL)"); + database.execSQL("DROP INDEX IF EXISTS `index_search_history_search`"); + database.execSQL("CREATE INDEX `index_search_history_search` ON `new_search_history` (`search`)"); + + database.execSQL("INSERT INTO new_search_history (search, creation_date, service_id) SELECT search, creation_date, service_id FROM search_history"); + + database.execSQL("DROP TABLE IF EXISTS search_history"); + database.execSQL("ALTER TABLE new_search_history RENAME TO search_history"); + + // update playlist_stream_join table + database.execSQL("CREATE TABLE IF NOT EXISTS `new_playlist_stream_join` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)"); + database.execSQL("DROP INDEX IF EXISTS `index_playlist_stream_join_playlist_id_join_index`"); + database.execSQL("CREATE UNIQUE INDEX `index_playlist_stream_join_playlist_id_join_index` ON `new_playlist_stream_join` (`playlist_id`, `join_index`)"); + database.execSQL("DROP INDEX IF EXISTS `index_playlist_stream_join_stream_id`"); + database.execSQL("CREATE INDEX `index_playlist_stream_join_stream_id` ON `new_playlist_stream_join` (`stream_id`)"); + + database.execSQL("INSERT INTO new_playlist_stream_join (playlist_id, stream_id, join_index) SELECT playlist_id, stream_id, join_index FROM playlist_stream_join"); + + database.execSQL("DROP TABLE IF EXISTS playlist_stream_join"); + database.execSQL("ALTER TABLE new_playlist_stream_join RENAME TO playlist_stream_join"); + + if(DEBUG) { + Log.d(TAG, "Stop migrating database"); + } + } + }; } diff --git a/app/src/main/java/org/schabi/newpipe/database/RemoteDatabase.java b/app/src/main/java/org/schabi/newpipe/database/RemoteDatabase.java new file mode 100644 index 00000000000..002b0230afe --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/RemoteDatabase.java @@ -0,0 +1,39 @@ +package org.schabi.newpipe.database; + +import org.schabi.newpipe.database.history.dao.SearchHistoryDAO; +import org.schabi.newpipe.database.history.dao.StreamHistoryDAO; +import org.schabi.newpipe.database.playlist.dao.PlaylistDAO; +import org.schabi.newpipe.database.playlist.dao.PlaylistRemoteDAO; +import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO; +import org.schabi.newpipe.database.stream.dao.StreamDAO; +import org.schabi.newpipe.database.stream.dao.StreamStateDAO; +import org.schabi.newpipe.database.subscription.SubscriptionDAO; + +import io.reactivex.Completable; + +public abstract class RemoteDatabase implements Database{ + + public abstract SubscriptionDAO subscriptionDAO(); + + public abstract SearchHistoryDAO searchHistoryDAO(); + + public abstract StreamDAO streamDAO(); + + public abstract StreamHistoryDAO streamHistoryDAO(); + + public abstract StreamStateDAO streamStateDAO(); + + public abstract PlaylistDAO playlistDAO(); + + public abstract PlaylistStreamDAO playlistStreamDAO(); + + public abstract PlaylistRemoteDAO playlistRemoteDAO(); + + public abstract Completable sync(); + + public abstract Completable refreshSubscriptions(); + + public abstract Completable refreshPlaylists(); + + public abstract Completable refreshHistory(); +} diff --git a/app/src/main/java/org/schabi/newpipe/database/RemoteDatabaseClient.java b/app/src/main/java/org/schabi/newpipe/database/RemoteDatabaseClient.java new file mode 100644 index 00000000000..f4160982ed3 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/RemoteDatabaseClient.java @@ -0,0 +1,134 @@ +package org.schabi.newpipe.database; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.support.annotation.NonNull; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.auth.AuthService; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import okhttp3.HttpUrl; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class RemoteDatabaseClient implements SharedPreferences.OnSharedPreferenceChangeListener { + + private static RemoteDatabaseClient INSTANCE; + private static final String AUTHORIZATION = "Authorization"; + private static final String MEDIA_TYPE = "application/json; charset=UTF-8"; + private static final String DEFAULT_URL = "https://65d3af6c-9704-4d26-b936-47b59dda70b0.pub.cloud.scaleway.com/api"; + + private final OkHttpClient client; + private String url; + private final Context context; + + private RemoteDatabaseClient(Context context) { + this.client = new OkHttpClient.Builder() + .readTimeout(30, TimeUnit.SECONDS) + //.cache(new Cache(new File(context.getExternalCacheDir(), "okhttp"), 16 * 1024 * 1024)) + .build(); + this.context = context; + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + preferences.registerOnSharedPreferenceChangeListener(this); + this.url = preferences.getString(context.getString(R.string.sync_server_url_key), DEFAULT_URL); + } + + synchronized public static RemoteDatabaseClient getInstance(Context context) { + if (null == INSTANCE) { + INSTANCE = new RemoteDatabaseClient(context); + } + return INSTANCE; + } + + public String get(String endpoint) { + + Request request = new Request.Builder() + .url(url + endpoint) + .addHeader(AUTHORIZATION, getAuthorization()) + .build(); + + return execute(request); + } + + public String get(String endpoint, Map params) { + HttpUrl.Builder httpBuilder = HttpUrl.get(url + endpoint).newBuilder(); + for (Map.Entry param : params.entrySet()) { + httpBuilder.addQueryParameter(param.getKey(), param.getValue()); + } + Request request = new Request.Builder() + .url(httpBuilder.build()) + .addHeader(AUTHORIZATION, getAuthorization()) + .build(); + + return execute(request); + } + + public String post(String endpoint, String body) { + + RequestBody requestBody = RequestBody.create(MediaType.parse(MEDIA_TYPE), body); + + Request.Builder builder = new Request.Builder() + .url(url + endpoint) + .post(requestBody); + + if (!(endpoint.equals(AuthService.LOGIN_ENDPOINT) || endpoint.equals(AuthService.SIGNUP_ENDPOINT))) { + builder.addHeader(AUTHORIZATION, getAuthorization()); + } + + return execute(builder.build()); + } + + public String put(String endpoint, String body) { + RequestBody requestBody = RequestBody.create(MediaType.parse(MEDIA_TYPE), body); + + Request request = new Request.Builder() + .url(url + endpoint) + .addHeader(AUTHORIZATION, getAuthorization()) + .put(requestBody) + .build(); + + return execute(request); + } + + public String delete(String endpoint) { + Request request = new Request.Builder() + .url(url + endpoint) + .addHeader(AUTHORIZATION, getAuthorization()) + .delete() + .build(); + + return execute(request); + } + + @NonNull + private String execute(Request request) { + try { + Response response = client.newCall(request).execute(); + if (!response.isSuccessful() || response.body() == null) { + throw new RemoteDatabaseException("Error communicating with server"); + } + return response.body().string(); + } catch (IOException e) { + throw new RemoteDatabaseException("Error communicating with server", e); + } + } + + private String getAuthorization() { + return AuthService.getInstance(context).getToken(); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if(key.equals(context.getString(R.string.sync_server_url_key))){ + this.url = sharedPreferences.getString(key, DEFAULT_URL); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/RemoteDatabaseException.java b/app/src/main/java/org/schabi/newpipe/database/RemoteDatabaseException.java new file mode 100644 index 00000000000..14d61aebcec --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/RemoteDatabaseException.java @@ -0,0 +1,13 @@ +package org.schabi.newpipe.database; + +public class RemoteDatabaseException extends RuntimeException { + + public RemoteDatabaseException(String message) { + super(message); + } + + public RemoteDatabaseException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/app/src/main/java/org/schabi/newpipe/database/RemoteDatabaseImpl.java b/app/src/main/java/org/schabi/newpipe/database/RemoteDatabaseImpl.java new file mode 100644 index 00000000000..174f60e5a39 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/RemoteDatabaseImpl.java @@ -0,0 +1,171 @@ +package org.schabi.newpipe.database; + +import android.content.Context; + +import org.schabi.newpipe.database.history.dao.RemoteSearchHistoryDAO; +import org.schabi.newpipe.database.history.dao.RemoteStreamHistoryDAO; +import org.schabi.newpipe.database.history.dao.SearchHistoryDAO; +import org.schabi.newpipe.database.history.dao.StreamHistoryDAO; +import org.schabi.newpipe.database.history.model.SearchHistoryEntry; +import org.schabi.newpipe.database.history.model.StreamHistoryEntity; +import org.schabi.newpipe.database.playlist.dao.PlaylistDAO; +import org.schabi.newpipe.database.playlist.dao.PlaylistRemoteDAO; +import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO; +import org.schabi.newpipe.database.playlist.dao.RemotePlaylistDAO; +import org.schabi.newpipe.database.playlist.dao.RemotePlaylistRemoteDAO; +import org.schabi.newpipe.database.playlist.dao.RemotePlaylistStreamDAO; +import org.schabi.newpipe.database.playlist.model.PlaylistEntity; +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; +import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; +import org.schabi.newpipe.database.stream.dao.RemoteStreamDAO; +import org.schabi.newpipe.database.stream.dao.RemoteStreamStateDAO; +import org.schabi.newpipe.database.stream.dao.StreamDAO; +import org.schabi.newpipe.database.stream.dao.StreamStateDAO; +import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; +import org.schabi.newpipe.database.subscription.RemoteSubscriptionDAO; +import org.schabi.newpipe.database.subscription.SubscriptionDAO; +import org.schabi.newpipe.database.subscription.SubscriptionEntity; + +import java.util.List; + +import io.reactivex.Completable; +import io.reactivex.schedulers.Schedulers; + +public class RemoteDatabaseImpl extends RemoteDatabase { + + private final RemoteSubscriptionDAO subscriptionDAO; + private final RemoteSearchHistoryDAO searchHistoryDAO; + private final RemoteStreamDAO streamDAO; + private final RemoteStreamHistoryDAO streamHistoryDAO; + private final RemoteStreamStateDAO streamStateDAO; + private final RemotePlaylistDAO playlistDAO; + private final RemotePlaylistStreamDAO playlistStreamDAO; + private final RemotePlaylistRemoteDAO playlistRemoteDAO; + + private final AppDatabase roomDb; + + public RemoteDatabaseImpl(AppDatabase roomDb, Context context) { + super(); + this.roomDb = roomDb; + subscriptionDAO = new RemoteSubscriptionDAO(roomDb, context); + searchHistoryDAO = new RemoteSearchHistoryDAO(roomDb, context); + streamDAO = new RemoteStreamDAO(roomDb, context); + streamHistoryDAO = new RemoteStreamHistoryDAO(roomDb, context); + streamStateDAO = new RemoteStreamStateDAO(roomDb, context); + playlistStreamDAO = new RemotePlaylistStreamDAO(roomDb, context); + playlistDAO = new RemotePlaylistDAO(roomDb, context); + playlistRemoteDAO = new RemotePlaylistRemoteDAO(roomDb,context); + } + + + @Override + public SubscriptionDAO subscriptionDAO() { + return subscriptionDAO; + } + + @Override + public SearchHistoryDAO searchHistoryDAO() { + return searchHistoryDAO; + } + + @Override + public StreamDAO streamDAO() { + return streamDAO; + } + + @Override + public StreamHistoryDAO streamHistoryDAO() { + return streamHistoryDAO; + } + + @Override + public StreamStateDAO streamStateDAO() { + return streamStateDAO; + } + + @Override + public PlaylistDAO playlistDAO() { + return playlistDAO; + } + + @Override + public PlaylistStreamDAO playlistStreamDAO() { + return playlistStreamDAO; + } + + @Override + public PlaylistRemoteDAO playlistRemoteDAO() { + return playlistRemoteDAO; + } + + @Override + public Completable sync() { + + return Completable.fromAction(() -> { + //TODO make these calls parallel + List streamEntities = streamDAO.fetchAll(); + List streamHistoryEntities = streamHistoryDAO.fetchAll(); + List streamStateEntities = streamStateDAO.fetchAll(); + List playlistEntities = playlistDAO.fetchAll(); + List playlistStreamEntities = playlistStreamDAO.fetchAll(); + List playlistRemoteEntities = playlistRemoteDAO.fetchAll(); + List searchHistoryEntries = searchHistoryDAO.fetchAll(); + List subscriptionEntities = subscriptionDAO.fetchAll(); + + roomDb.runInTransaction(() -> { + roomDb.streamDAO().destroyAndRefill(streamEntities); + roomDb.streamHistoryDAO().destroyAndRefill(streamHistoryEntities); + roomDb.streamStateDAO().destroyAndRefill(streamStateEntities); + roomDb.playlistDAO().destroyAndRefill(playlistEntities); + roomDb.playlistStreamDAO().destroyAndRefill(playlistStreamEntities); + roomDb.playlistRemoteDAO().destroyAndRefill(playlistRemoteEntities); + roomDb.searchHistoryDAO().destroyAndRefill(searchHistoryEntries); + roomDb.subscriptionDAO().destroyAndRefill(subscriptionEntities); + }); + }).subscribeOn(Schedulers.io()); + } + + @Override + public Completable refreshSubscriptions() { + return Completable.fromAction(() -> { + List subscriptionEntities = subscriptionDAO.fetchAll(); + roomDb.runInTransaction(() -> roomDb.subscriptionDAO().destroyAndRefill(subscriptionEntities)); + }).subscribeOn(Schedulers.io()); + } + + @Override + public Completable refreshPlaylists() { + return Completable.fromAction(() -> { + //TODO make these calls parallel + List streamEntities = streamDAO.fetchAll(); + List playlistEntities = playlistDAO.fetchAll(); + List playlistStreamEntities = playlistStreamDAO.fetchAll(); + List playlistRemoteEntities = playlistRemoteDAO.fetchAll(); + roomDb.runInTransaction(() -> { + roomDb.streamDAO().upsertAll(streamEntities); + roomDb.playlistDAO().destroyAndRefill(playlistEntities); + roomDb.playlistStreamDAO().destroyAndRefill(playlistStreamEntities); + roomDb.playlistRemoteDAO().destroyAndRefill(playlistRemoteEntities); + }); + }).subscribeOn(Schedulers.io()); + } + + @Override + public Completable refreshHistory() { + return Completable.fromAction(() -> { + //TODO make these calls parallel + List streamEntities = streamDAO.fetchAll(); + List streamHistoryEntities = streamHistoryDAO.fetchAll(); + List streamStateEntities = streamStateDAO.fetchAll(); + List searchHistoryEntries = searchHistoryDAO.fetchAll(); + + roomDb.runInTransaction(() -> { + roomDb.streamDAO().destroyAndRefill(streamEntities); + roomDb.streamHistoryDAO().destroyAndRefill(streamHistoryEntities); + roomDb.streamStateDAO().destroyAndRefill(streamStateEntities); + roomDb.searchHistoryDAO().destroyAndRefill(searchHistoryEntries); + }); + }).subscribeOn(Schedulers.io()); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/RemoteSearchHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/RemoteSearchHistoryDAO.java new file mode 100644 index 00000000000..ecc4812f5b4 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/RemoteSearchHistoryDAO.java @@ -0,0 +1,204 @@ +package org.schabi.newpipe.database.history.dao; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.grack.nanojson.JsonArray; +import com.grack.nanojson.JsonBuilder; +import com.grack.nanojson.JsonObject; +import com.grack.nanojson.JsonParser; +import com.grack.nanojson.JsonParserException; +import com.grack.nanojson.JsonWriter; + +import org.schabi.newpipe.auth.EncryptionUtils; +import org.schabi.newpipe.database.AppDatabase; +import org.schabi.newpipe.database.RemoteDatabaseClient; +import org.schabi.newpipe.database.history.model.SearchHistoryEntry; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +import io.reactivex.Flowable; +import io.reactivex.Single; +import io.reactivex.schedulers.Schedulers; + +public class RemoteSearchHistoryDAO extends SearchHistoryDAO { + + private static final String ENDPOINT = "/searchhistory"; + private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss"; + + private final RemoteDatabaseClient client; + private final AppDatabase roomDb; + private final EncryptionUtils eU; + + public RemoteSearchHistoryDAO(AppDatabase roomDb, Context context) { + this.roomDb = roomDb; + this.client = RemoteDatabaseClient.getInstance(context); + try { + this.eU = EncryptionUtils.getInstance(context); + } catch (GeneralSecurityException | IOException e) { + throw new IllegalStateException("unable to get encryption utils", e); + } + } + + @Override + public long insert(SearchHistoryEntry searchHistoryEntity) { + long id = Single.fromCallable(() -> client.post(ENDPOINT, toJson(searchHistoryEntity))) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.computation()) + .flatMap(s -> Single.just(parseObject(s))).blockingGet().getUid(); + searchHistoryEntity.setUid(id); + return roomDb.searchHistoryDAO().insert(searchHistoryEntity); + } + + @Override + public List insertAll(SearchHistoryEntry... searchHistoryEntities) { + List result = new ArrayList<>(); + for (SearchHistoryEntry entity : searchHistoryEntities) { + result.add(insert(entity)); + } + return result; + } + + @Override + public List insertAll(Collection searchHistoryEntities) { + List result = new ArrayList<>(); + for (SearchHistoryEntry entity : searchHistoryEntities) { + result.add(insert(entity)); + } + return result; + } + + @Override + public Flowable> getAll() { + return roomDb.searchHistoryDAO().getAll(); + } + + @Override + public Flowable> getUniqueEntries(int limit) { + return roomDb.searchHistoryDAO().getUniqueEntries(limit); + } + + @Nullable + @Override + public SearchHistoryEntry getLatestEntry() { + return roomDb.searchHistoryDAO().getLatestEntry(); + } + + @Override + public int deleteAll() { + getAll().blockingForEach(t -> delete(t)); + return 0; + } + + @Override + public int deleteAllWhereQuery(String query) { + return 0; + } + + @Override + public int update(SearchHistoryEntry searchHistoryEntity) { + client.put(ENDPOINT + "/" + searchHistoryEntity.getUid(), toJson(searchHistoryEntity)); + return roomDb.searchHistoryDAO().update(searchHistoryEntity); + } + + @Override + public void update(Collection searchHistoryEntities) { + for (SearchHistoryEntry entity : searchHistoryEntities) { + update(entity); + } + } + + @Override + public Flowable> listByService(int serviceId) { + return roomDb.searchHistoryDAO().listByService(serviceId); + } + + @Override + public Flowable> getSimilarEntries(String query, int limit) { + return roomDb.searchHistoryDAO().getSimilarEntries(query, limit); + } + + @Override + public void delete(SearchHistoryEntry searchHistoryEntity) { + client.delete(ENDPOINT + "/" + searchHistoryEntity.getUid()); + roomDb.searchHistoryDAO().delete(searchHistoryEntity); + } + + @Override + public int delete(Collection searchHistoryEntities) { + for (SearchHistoryEntry entity : searchHistoryEntities) { + delete(entity); + } + return 0; + } + + public List fetchAll() throws JsonParserException { + String response = client.get(ENDPOINT); + return parseList(response); + } + + @Override + public void destroyAndRefill(Collection entities) { + throw new UnsupportedOperationException(); + } + + @NonNull + private SearchHistoryEntry parseObject(String s) throws JsonParserException { + JsonObject jsonObject = JsonParser.object().from(s); + return fromJson(jsonObject); + } + + @NonNull + private List parseList(String s) throws JsonParserException { + JsonArray list = JsonParser.array().from(s); + List resultList = new ArrayList<>(); + for (Object object : list) { + if (object instanceof JsonObject) { + JsonObject entity = (JsonObject) object; + resultList.add(fromJson(entity)); + } + } + return resultList; + } + + private String toJson(SearchHistoryEntry entity) { + JsonBuilder json = JsonObject.builder(); + try { + json.value("serviceId", eU.encrypt(entity.getServiceId())); + json.value("search", eU.encrypt(entity.getSearch())); + String creationDate = new SimpleDateFormat(DATE_FORMAT, Locale.US).format(entity.getCreationDate()); + json.value("creationDate", eU.encrypt(creationDate)); + } catch (GeneralSecurityException e) { + throw new RuntimeException("encryption error", e); + } + String ret = JsonWriter.string(json.done()); + return ret; + } + + private SearchHistoryEntry fromJson(JsonObject object) { + try { + int serviceId = Long.valueOf(eU.decryptAsLong(object.getString("serviceId"))).intValue(); + DateFormat df = new SimpleDateFormat(DATE_FORMAT, Locale.US); + Date creationDate = df.parse(eU.decrypt(object.getString("creationDate"))); + String search = eU.decrypt(object.getString("search")); + int uid = object.getInt("uid"); + SearchHistoryEntry entity = new SearchHistoryEntry(creationDate, serviceId, search); + entity.setUid(uid); + return entity; + } catch (GeneralSecurityException e) { + throw new RuntimeException("decryption error", e); + } catch (ParseException e) { + throw new RuntimeException("error parsing date", e); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/RemoteStreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/RemoteStreamHistoryDAO.java new file mode 100644 index 00000000000..f3cac9fe952 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/RemoteStreamHistoryDAO.java @@ -0,0 +1,213 @@ +package org.schabi.newpipe.database.history.dao; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.grack.nanojson.JsonArray; +import com.grack.nanojson.JsonBuilder; +import com.grack.nanojson.JsonObject; +import com.grack.nanojson.JsonParser; +import com.grack.nanojson.JsonParserException; +import com.grack.nanojson.JsonWriter; + +import org.schabi.newpipe.auth.EncryptionUtils; +import org.schabi.newpipe.database.AppDatabase; +import org.schabi.newpipe.database.RemoteDatabaseClient; +import org.schabi.newpipe.database.history.model.StreamHistoryEntity; +import org.schabi.newpipe.database.history.model.StreamHistoryEntry; +import org.schabi.newpipe.database.stream.StreamStatisticsEntry; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +import io.reactivex.Flowable; +import io.reactivex.Single; +import io.reactivex.schedulers.Schedulers; + +public class RemoteStreamHistoryDAO extends StreamHistoryDAO { + + private static final String ENDPOINT = "/streamhistory"; + private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss"; + + private final RemoteDatabaseClient client; + private final AppDatabase roomDb; + private final EncryptionUtils eU; + + public RemoteStreamHistoryDAO(AppDatabase roomDb, Context context) { + this.roomDb = roomDb; + this.client = RemoteDatabaseClient.getInstance(context); + try { + this.eU = EncryptionUtils.getInstance(context); + } catch (GeneralSecurityException | IOException e) { + throw new IllegalStateException("unable to get encryption utils", e); + } + } + + @Override + public long insert(StreamHistoryEntity streamHistoryEntity) { + + long id = Single.fromCallable(() -> client.post(ENDPOINT, toJson(streamHistoryEntity))) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.computation()) + .flatMap(s -> Single.just(parseObject(s))).blockingGet().getUid(); + streamHistoryEntity.setUid(id); + return roomDb.streamHistoryDAO().insert(streamHistoryEntity); + } + + @Override + public List insertAll(StreamHistoryEntity... streamHistoryEntities) { + List result = new ArrayList<>(); + for (StreamHistoryEntity entity : streamHistoryEntities) { + result.add(insert(entity)); + } + return result; + } + + @Override + public List insertAll(Collection streamHistoryEntities) { + List result = new ArrayList<>(); + for (StreamHistoryEntity entity : streamHistoryEntities) { + result.add(insert(entity)); + } + return result; + } + + @Override + public Flowable> getAll() { + return roomDb.streamHistoryDAO().getAll(); + } + + @Nullable + @Override + public StreamHistoryEntity getLatestEntry() { + return roomDb.streamHistoryDAO().getLatestEntry(); + } + + @Override + public int deleteAll() { + getAll().blockingForEach(t -> delete(t)); + return 0; + } + + @Override + public int update(StreamHistoryEntity streamHistoryEntity) { + client.put(ENDPOINT + "/" + streamHistoryEntity.getUid(), toJson(streamHistoryEntity)); + return roomDb.streamHistoryDAO().update(streamHistoryEntity); + } + + @Override + public void update(Collection streamHistoryEntities) { + for (StreamHistoryEntity entity : streamHistoryEntities) { + update(entity); + } + } + + @Override + public Flowable> listByService(int serviceId) { + return roomDb.streamHistoryDAO().listByService(serviceId); + } + + @Override + public Flowable> getHistory() { + return roomDb.streamHistoryDAO().getHistory(); + } + + @Nullable + @Override + public StreamHistoryEntity getLatestEntry(long streamId) { + return roomDb.streamHistoryDAO().getLatestEntry(streamId); + } + + @Override + public int deleteStreamHistory(long streamId) { + return 0; + } + + @Override + public Flowable> getStatistics() { + return roomDb.streamHistoryDAO().getStatistics(); + } + + @Override + public void delete(StreamHistoryEntity streamHistoryEntity) { + client.delete(ENDPOINT + "/" + streamHistoryEntity.getUid()); + roomDb.streamHistoryDAO().delete(streamHistoryEntity); + } + + @Override + public int delete(Collection streamHistoryEntities) { + for (StreamHistoryEntity entity : streamHistoryEntities) { + delete(entity); + } + return 0; + } + + public List fetchAll() throws JsonParserException { + String response = client.get(ENDPOINT); + return parseList(response); + } + + @Override + public void destroyAndRefill(Collection entities) { + throw new UnsupportedOperationException(); + } + + @NonNull + private StreamHistoryEntity parseObject(String s) throws JsonParserException { + JsonObject jsonObject = JsonParser.object().from(s); + return fromJson(jsonObject); + } + + @NonNull + private List parseList(String s) throws JsonParserException { + JsonArray list = JsonParser.array().from(s); + List resultList = new ArrayList<>(); + for (Object object : list) { + if (object instanceof JsonObject) { + JsonObject entity = (JsonObject) object; + resultList.add(fromJson(entity)); + } + } + return resultList; + } + + private String toJson(StreamHistoryEntity entity) { + JsonBuilder json = JsonObject.builder(); + try { + json.value("streamId", entity.getStreamUid()); + String accessDate = new SimpleDateFormat(DATE_FORMAT, Locale.US).format(entity.getAccessDate()); + json.value("accessDate", eU.encrypt(accessDate)); + json.value("repeatCount", eU.encrypt(entity.getRepeatCount())); + } catch (GeneralSecurityException e) { + throw new RuntimeException("encryption error", e); + } + String ret = JsonWriter.string(json.done()); + return ret; + } + + private StreamHistoryEntity fromJson(JsonObject object) { + try { + int streamId = object.getInt("streamId"); + DateFormat df = new SimpleDateFormat(DATE_FORMAT, Locale.US); + Date accessDate = df.parse(eU.decrypt(object.getString("accessDate"))); + long repeatCount = eU.decryptAsLong(object.getString("repeatCount")); + int uid = object.getInt("uid"); + StreamHistoryEntity entity = new StreamHistoryEntity(streamId, accessDate, repeatCount); + entity.setUid(uid); + return entity; + } catch (GeneralSecurityException e) { + throw new RuntimeException("decryption error", e); + } catch (ParseException e){ + throw new RuntimeException("error parsing date", e); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java index 83e629e489f..561afe44964 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java @@ -2,48 +2,57 @@ import android.arch.persistence.room.Dao; import android.arch.persistence.room.Query; +import android.arch.persistence.room.Transaction; import android.support.annotation.Nullable; import org.schabi.newpipe.database.history.model.SearchHistoryEntry; +import java.util.Collection; import java.util.List; import io.reactivex.Flowable; import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.CREATION_DATE; -import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.ID; import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SEARCH; +import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SEARCH_HISTORY_ID; import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SERVICE_ID; import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.TABLE_NAME; @Dao -public interface SearchHistoryDAO extends HistoryDAO { +public abstract class SearchHistoryDAO implements HistoryDAO { - String ORDER_BY_CREATION_DATE = " ORDER BY " + CREATION_DATE + " DESC"; + private final static String ORDER_BY_CREATION_DATE = " ORDER BY " + CREATION_DATE + " DESC"; @Query("SELECT * FROM " + TABLE_NAME + - " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")") + " WHERE " + SEARCH_HISTORY_ID + " = (SELECT MAX(" + SEARCH_HISTORY_ID + ") FROM " + TABLE_NAME + ")") @Nullable - SearchHistoryEntry getLatestEntry(); + public abstract SearchHistoryEntry getLatestEntry(); @Query("DELETE FROM " + TABLE_NAME) @Override - int deleteAll(); + public abstract int deleteAll(); @Query("DELETE FROM " + TABLE_NAME + " WHERE " + SEARCH + " = :query") - int deleteAllWhereQuery(String query); + public abstract int deleteAllWhereQuery(String query); @Query("SELECT * FROM " + TABLE_NAME + ORDER_BY_CREATION_DATE) @Override - Flowable> getAll(); + public abstract Flowable> getAll(); @Query("SELECT * FROM " + TABLE_NAME + " GROUP BY " + SEARCH + ORDER_BY_CREATION_DATE + " LIMIT :limit") - Flowable> getUniqueEntries(int limit); + public abstract Flowable> getUniqueEntries(int limit); @Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE) @Override - Flowable> listByService(int serviceId); + public abstract Flowable> listByService(int serviceId); @Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SEARCH + " LIKE :query || '%' GROUP BY " + SEARCH + " LIMIT :limit") - Flowable> getSimilarEntries(String query, int limit); + public abstract Flowable> getSimilarEntries(String query, int limit); + + @Override + @Transaction + public void destroyAndRefill(Collection entities) { + deleteAll(); + insertAll(entities); + } } diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java index 50d723f1fd6..bc44cae1079 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java @@ -3,24 +3,27 @@ import android.arch.persistence.room.Dao; import android.arch.persistence.room.Query; +import android.arch.persistence.room.Transaction; import android.support.annotation.Nullable; +import org.schabi.newpipe.database.history.model.StreamHistoryEntity; import org.schabi.newpipe.database.history.model.StreamHistoryEntry; import org.schabi.newpipe.database.stream.StreamStatisticsEntry; -import org.schabi.newpipe.database.history.model.StreamHistoryEntity; +import org.schabi.newpipe.database.stream.model.StreamEntity; +import java.util.Collection; import java.util.List; import io.reactivex.Flowable; +import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID; +import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE; +import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_REPEAT_COUNT; import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_LATEST_DATE; import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WATCH_COUNT; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; -import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID; -import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE; -import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; @Dao public abstract class StreamHistoryDAO implements HistoryDAO { @@ -45,7 +48,8 @@ public Flowable> listByService(int serviceId) { } @Query("SELECT * FROM " + STREAM_TABLE + - " INNER JOIN " + STREAM_HISTORY_TABLE + + " INNER JOIN " + + "(SELECT " + JOIN_STREAM_ID + ", " + STREAM_ACCESS_DATE + ", " + STREAM_REPEAT_COUNT + " FROM " + STREAM_HISTORY_TABLE + ")" + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID + " ORDER BY " + STREAM_ACCESS_DATE + " DESC") public abstract Flowable> getHistory(); @@ -69,4 +73,12 @@ public Flowable> listByService(int serviceId) { " ON " + STREAM_ID + " = " + JOIN_STREAM_ID) public abstract Flowable> getStatistics(); + + + @Override + @Transaction + public void destroyAndRefill(Collection entities) { + deleteAll(); + insertAll(entities); + } } diff --git a/app/src/main/java/org/schabi/newpipe/database/history/model/SearchHistoryEntry.java b/app/src/main/java/org/schabi/newpipe/database/history/model/SearchHistoryEntry.java index dcfff99b8a2..109f81e8877 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/model/SearchHistoryEntry.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/model/SearchHistoryEntry.java @@ -14,15 +14,15 @@ indices = {@Index(value = SEARCH)}) public class SearchHistoryEntry { - public static final String ID = "id"; public static final String TABLE_NAME = "search_history"; + public static final String SEARCH_HISTORY_ID = "uid"; public static final String SERVICE_ID = "service_id"; public static final String CREATION_DATE = "creation_date"; public static final String SEARCH = "search"; - @ColumnInfo(name = ID) + @ColumnInfo(name = SEARCH_HISTORY_ID) @PrimaryKey(autoGenerate = true) - private long id; + private long uid; @ColumnInfo(name = CREATION_DATE) private Date creationDate; @@ -39,12 +39,12 @@ public SearchHistoryEntry(Date creationDate, int serviceId, String search) { this.search = search; } - public long getId() { - return id; + public long getUid() { + return uid; } - public void setId(long id) { - this.id = id; + public void setUid(long uid) { + this.uid = uid; } public Date getCreationDate() { diff --git a/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.java b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.java index b553f437d8c..fbd0c1569e5 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.java @@ -5,6 +5,7 @@ import android.arch.persistence.room.ForeignKey; import android.arch.persistence.room.Ignore; import android.arch.persistence.room.Index; +import android.arch.persistence.room.PrimaryKey; import android.support.annotation.NonNull; import org.schabi.newpipe.database.stream.model.StreamEntity; @@ -12,14 +13,12 @@ import java.util.Date; import static android.arch.persistence.room.ForeignKey.CASCADE; -import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID; import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE; +import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; @Entity(tableName = STREAM_HISTORY_TABLE, - primaryKeys = {JOIN_STREAM_ID, STREAM_ACCESS_DATE}, - // No need to index for timestamp as they will almost always be unique - indices = {@Index(value = {JOIN_STREAM_ID})}, + indices = {@Index(value = {JOIN_STREAM_ID, STREAM_ACCESS_DATE}, unique = true)}, foreignKeys = { @ForeignKey(entity = StreamEntity.class, parentColumns = StreamEntity.STREAM_ID, @@ -28,10 +27,15 @@ }) public class StreamHistoryEntity { final public static String STREAM_HISTORY_TABLE = "stream_history"; + final public static String STREAM_HISTORY_ID = "uid"; final public static String JOIN_STREAM_ID = "stream_id"; final public static String STREAM_ACCESS_DATE = "access_date"; final public static String STREAM_REPEAT_COUNT = "repeat_count"; + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = STREAM_HISTORY_ID) + private long uid; + @ColumnInfo(name = JOIN_STREAM_ID) private long streamUid; @@ -53,6 +57,14 @@ public StreamHistoryEntity(long streamUid, @NonNull Date accessDate) { this(streamUid, accessDate, 1); } + public long getUid() { + return uid; + } + + public void setUid(long uid) { + this.uid = uid; + } + public long getStreamUid() { return streamUid; } diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java index 7a6282f9647..0d5cc3bc36a 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java @@ -2,10 +2,13 @@ import android.arch.persistence.room.Dao; import android.arch.persistence.room.Query; +import android.arch.persistence.room.Transaction; import org.schabi.newpipe.database.BasicDAO; import org.schabi.newpipe.database.playlist.model.PlaylistEntity; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; +import java.util.Collection; import java.util.List; import io.reactivex.Flowable; @@ -33,4 +36,11 @@ public Flowable> listByService(int serviceId) { @Query("DELETE FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId") public abstract int deletePlaylist(final long playlistId); + + @Override + @Transaction + public void destroyAndRefill(Collection entities) { + deleteAll(); + insertAll(entities); + } } diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.java index 82d767b07c6..85da5af3304 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.java @@ -7,6 +7,7 @@ import org.schabi.newpipe.database.BasicDAO; import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; +import java.util.Collection; import java.util.List; import io.reactivex.Flowable; @@ -57,4 +58,11 @@ public long upsert(PlaylistRemoteEntity playlist) { @Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE + " WHERE " + REMOTE_PLAYLIST_ID + " = :playlistId") public abstract int deletePlaylist(final long playlistId); + + @Override + @Transaction + public void destroyAndRefill(Collection entities) { + deleteAll(); + insertAll(entities); + } } diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java index 8b6d62ca465..043aad42d8c 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java @@ -9,14 +9,22 @@ import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; +import java.util.Collection; import java.util.List; import io.reactivex.Flowable; import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT; -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.*; -import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.*; -import static org.schabi.newpipe.database.stream.model.StreamEntity.*; +import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID; +import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME; +import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE; +import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL; +import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_INDEX; +import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_PLAYLIST_ID; +import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_STREAM_ID; +import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE; +import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; +import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; @Dao public abstract class PlaylistStreamDAO implements BasicDAO { @@ -55,14 +63,21 @@ public Flowable> listByService(int serviceId) { public abstract Flowable> getOrderedStreamsOf(long playlistId); @Transaction - @Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", " + + @Query("SELECT " + PLAYLIST_TABLE + "." + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", " + PLAYLIST_THUMBNAIL_URL + ", " + "COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT + " FROM " + PLAYLIST_TABLE + " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE + - " ON " + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID + - " GROUP BY " + JOIN_PLAYLIST_ID + + " ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID + + " GROUP BY " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC") public abstract Flowable> getPlaylistMetadata(); + + @Override + @Transaction + public void destroyAndRefill(Collection entities) { + deleteAll(); + insertAll(entities); + } } diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/RemotePlaylistDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/RemotePlaylistDAO.java new file mode 100644 index 00000000000..df34f100881 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/RemotePlaylistDAO.java @@ -0,0 +1,184 @@ +package org.schabi.newpipe.database.playlist.dao; + +import android.content.Context; +import android.support.annotation.NonNull; + +import com.grack.nanojson.JsonArray; +import com.grack.nanojson.JsonBuilder; +import com.grack.nanojson.JsonObject; +import com.grack.nanojson.JsonParser; +import com.grack.nanojson.JsonParserException; +import com.grack.nanojson.JsonWriter; + +import org.schabi.newpipe.auth.EncryptionUtils; +import org.schabi.newpipe.database.AppDatabase; +import org.schabi.newpipe.database.RemoteDatabaseClient; +import org.schabi.newpipe.database.playlist.model.PlaylistEntity; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import io.reactivex.Flowable; +import io.reactivex.Single; +import io.reactivex.schedulers.Schedulers; + +public class RemotePlaylistDAO extends PlaylistDAO { + + private static final String ENDPOINT = "/playlists"; + + private final RemoteDatabaseClient client; + private final AppDatabase roomDb; + private final EncryptionUtils eU; + + public RemotePlaylistDAO(AppDatabase roomDb, Context context) { + this.roomDb = roomDb; + this.client = RemoteDatabaseClient.getInstance(context); + try { + this.eU = EncryptionUtils.getInstance(context); + } catch (GeneralSecurityException | IOException e) { + throw new IllegalStateException("unable to get encryption utils", e); + } + } + + @Override + public long insert(PlaylistEntity playlistEntity) { + + long id = Single.fromCallable(() -> client.post(ENDPOINT, toJson(playlistEntity))) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.computation()) + .flatMap(s -> Single.just(parseObject(s))).blockingGet().getUid(); + playlistEntity.setUid(id); + return roomDb.playlistDAO().insert(playlistEntity); + + } + + @Override + public List insertAll(PlaylistEntity... playlistEntities) { + List result = new ArrayList<>(); + for (PlaylistEntity entity : playlistEntities) { + result.add(insert(entity)); + } + return result; + } + + @Override + public List insertAll(Collection playlistEntities) { + List result = new ArrayList<>(); + for (PlaylistEntity entity : playlistEntities) { + result.add(insert(entity)); + } + return result; + } + + @Override + public Flowable> getAll() { + return roomDb.playlistDAO().getAll(); + } + + @Override + public int deleteAll() { + getAll().blockingForEach(t -> delete(t)); + return 0; + } + + @Override + public int update(PlaylistEntity playlistEntity) { + client.put(ENDPOINT + "/" + playlistEntity.getUid(), toJson(playlistEntity)); + return roomDb.playlistDAO().update(playlistEntity); + } + + @Override + public void update(Collection playlistEntities) { + for (PlaylistEntity entity : playlistEntities) { + update(entity); + } + } + + @Override + public Flowable> listByService(int serviceId) { + throw new UnsupportedOperationException(); + } + + @Override + public void delete(PlaylistEntity playlistEntity) { + client.delete(ENDPOINT + "/" + playlistEntity.getUid()); + roomDb.playlistDAO().delete(playlistEntity); + } + + @Override + public int delete(Collection playlistEntities) { + for (PlaylistEntity entity : playlistEntities) { + delete(entity); + } + return 0; + } + + @Override + public Flowable> getPlaylist(final long playlistId) { + + return roomDb.playlistDAO().getPlaylist(playlistId); + } + + @Override + public int deletePlaylist(final long playlistId) { + client.delete(ENDPOINT + "/" + playlistId); + return roomDb.playlistDAO().deletePlaylist(playlistId); + } + + public List fetchAll() throws JsonParserException { + String response = client.get(ENDPOINT); + return parseList(response); + } + + @Override + public void destroyAndRefill(Collection entities) { + throw new UnsupportedOperationException(); + } + + @NonNull + private PlaylistEntity parseObject(String s) throws JsonParserException { + JsonObject jsonObject = JsonParser.object().from(s); + return fromJson(jsonObject); + } + + @NonNull + private List parseList(String s) throws JsonParserException { + JsonArray list = JsonParser.array().from(s); + List resultList = new ArrayList<>(); + for (Object object : list) { + if (object instanceof JsonObject) { + JsonObject entity = (JsonObject) object; + resultList.add(fromJson(entity)); + } + } + return resultList; + } + + private String toJson(PlaylistEntity entity) { + JsonBuilder json = JsonObject.builder(); + try { + json.value("name", eU.encrypt(entity.getName())); + json.value("thumbnailUrl", eU.encrypt(entity.getThumbnailUrl())); + } catch (GeneralSecurityException e) { + throw new RuntimeException("encryption error", e); + } + String ret = JsonWriter.string(json.done()); + return ret; + } + + private PlaylistEntity fromJson(JsonObject object) { + try { + String name = eU.decrypt(object.getString("name")); + String thumbnailUrl = eU.decrypt(object.getString("thumbnailUrl")); + int uid = object.getInt("uid"); + PlaylistEntity entity = new PlaylistEntity(name, thumbnailUrl); + entity.setUid(uid); + return entity; + } catch (GeneralSecurityException e) { + throw new RuntimeException("decryption error", e); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/RemotePlaylistRemoteDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/RemotePlaylistRemoteDAO.java new file mode 100644 index 00000000000..497254613f2 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/RemotePlaylistRemoteDAO.java @@ -0,0 +1,196 @@ +package org.schabi.newpipe.database.playlist.dao; + +import android.content.Context; +import android.support.annotation.NonNull; + +import com.grack.nanojson.JsonArray; +import com.grack.nanojson.JsonBuilder; +import com.grack.nanojson.JsonObject; +import com.grack.nanojson.JsonParser; +import com.grack.nanojson.JsonParserException; +import com.grack.nanojson.JsonWriter; + +import org.schabi.newpipe.auth.EncryptionUtils; +import org.schabi.newpipe.database.AppDatabase; +import org.schabi.newpipe.database.RemoteDatabaseClient; +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import io.reactivex.Flowable; +import io.reactivex.Single; +import io.reactivex.schedulers.Schedulers; + +public class RemotePlaylistRemoteDAO extends PlaylistRemoteDAO { + + private static final String ENDPOINT = "/remoteplaylists"; + + private final RemoteDatabaseClient client; + private final AppDatabase roomDb; + private final EncryptionUtils eU; + + public RemotePlaylistRemoteDAO(AppDatabase roomDb, Context context) { + this.roomDb = roomDb; + this.client = RemoteDatabaseClient.getInstance(context); + try { + this.eU = EncryptionUtils.getInstance(context); + } catch (GeneralSecurityException | IOException e) { + throw new IllegalStateException("unable to get encryption utils", e); + } + } + + @Override + public long insert(PlaylistRemoteEntity playlistRemoteEntity) { + + long id = Single.fromCallable(() -> client.post(ENDPOINT, toJson(playlistRemoteEntity))) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.computation()) + .flatMap(s -> Single.just(parseObject(s))).blockingGet().getUid(); + playlistRemoteEntity.setUid(id); + return roomDb.playlistRemoteDAO().insert(playlistRemoteEntity); + } + + @Override + public List insertAll(PlaylistRemoteEntity... playlistRemoteEntities) { + List result = new ArrayList<>(); + for (PlaylistRemoteEntity entity : playlistRemoteEntities) { + result.add(insert(entity)); + } + return result; + } + + @Override + public List insertAll(Collection playlistRemoteEntities) { + List result = new ArrayList<>(); + for (PlaylistRemoteEntity entity : playlistRemoteEntities) { + result.add(insert(entity)); + } + return result; + } + + @Override + public Flowable> getAll() { + return roomDb.playlistRemoteDAO().getAll(); + } + + @Override + public int deleteAll() { + getAll().blockingForEach(t -> delete(t)); + return 0; + } + + @Override + public int update(PlaylistRemoteEntity playlistRemoteEntity) { + client.put(ENDPOINT + "/" + playlistRemoteEntity.getUid(), toJson(playlistRemoteEntity)); + return roomDb.playlistRemoteDAO().update(playlistRemoteEntity); + } + + @Override + public void update(Collection playlistRemoteEntities) { + for (PlaylistRemoteEntity entity : playlistRemoteEntities) { + update(entity); + } + } + + @Override + public Flowable> listByService(int serviceId) { + return roomDb.playlistRemoteDAO().listByService(serviceId); + } + + @Override + public Flowable> getPlaylist(long serviceId, String url) { + return roomDb.playlistRemoteDAO().getPlaylist(serviceId, url); + } + + @Override + Long getPlaylistIdInternal(long serviceId, String url) { + return roomDb.playlistRemoteDAO().getPlaylistIdInternal(serviceId, url); + } + + @Override + public int deletePlaylist(long playlistId) { + client.delete(ENDPOINT + "/" + playlistId); + return roomDb.playlistRemoteDAO().deletePlaylist(playlistId); + } + + @Override + public void delete(PlaylistRemoteEntity playlistRemoteEntity) { + client.delete(ENDPOINT + "/" + playlistRemoteEntity.getUid()); + roomDb.playlistRemoteDAO().delete(playlistRemoteEntity); + } + + @Override + public int delete(Collection playlistRemoteEntities) { + for (PlaylistRemoteEntity entity : playlistRemoteEntities) { + delete(entity); + } + return 0; + } + + public List fetchAll() throws JsonParserException { + String response = client.get(ENDPOINT); + return parseList(response); + } + + @Override + public void destroyAndRefill(Collection entities) { + throw new UnsupportedOperationException(); + } + + @NonNull + private PlaylistRemoteEntity parseObject(String s) throws JsonParserException { + JsonObject jsonObject = JsonParser.object().from(s); + return fromJson(jsonObject); + } + + @NonNull + private List parseList(String s) throws JsonParserException { + JsonArray list = JsonParser.array().from(s); + List resultList = new ArrayList<>(); + for (Object object : list) { + if (object instanceof JsonObject) { + JsonObject entity = (JsonObject) object; + resultList.add(fromJson(entity)); + } + } + return resultList; + } + + private String toJson(PlaylistRemoteEntity entity) { + JsonBuilder json = JsonObject.builder(); + try { + json.value("serviceId", eU.encrypt(entity.getServiceId())); + json.value("name", eU.encrypt(entity.getName())); + json.value("url", eU.encrypt(entity.getUrl())); + json.value("thumbnailUrl", eU.encrypt(entity.getThumbnailUrl())); + json.value("uploader", eU.encrypt(entity.getUploader())); + json.value("streamCount", eU.encrypt(entity.getStreamCount())); + } catch (GeneralSecurityException e) { + throw new RuntimeException("encryption error", e); + } + String ret = JsonWriter.string(json.done()); + return ret; + } + + private PlaylistRemoteEntity fromJson(JsonObject object) { + try { + int serviceId = Long.valueOf(eU.decryptAsLong(object.getString("serviceId"))).intValue(); + String name = eU.decrypt(object.getString("name")); + String url = eU.decrypt(object.getString("url")); + String thumbnailUrl = eU.decrypt(object.getString("thumbnailUrl")); + String uploader = eU.decrypt(object.getString("uploader")); + Long streamCount = eU.decryptAsLong(object.getString("streamCount")); + int uid = object.getInt("uid"); + PlaylistRemoteEntity entity = new PlaylistRemoteEntity(serviceId, name + , url, thumbnailUrl, uploader, streamCount); + entity.setUid(uid); + return entity; + } catch (GeneralSecurityException e) { + throw new RuntimeException("decryption error", e); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/RemotePlaylistStreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/RemotePlaylistStreamDAO.java new file mode 100644 index 00000000000..59d26e7d8ea --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/RemotePlaylistStreamDAO.java @@ -0,0 +1,177 @@ +package org.schabi.newpipe.database.playlist.dao; + +import android.content.Context; +import android.support.annotation.NonNull; + +import com.grack.nanojson.JsonArray; +import com.grack.nanojson.JsonBuilder; +import com.grack.nanojson.JsonObject; +import com.grack.nanojson.JsonParser; +import com.grack.nanojson.JsonParserException; +import com.grack.nanojson.JsonWriter; + +import org.schabi.newpipe.database.AppDatabase; +import org.schabi.newpipe.database.RemoteDatabaseClient; +import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; +import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; +import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import io.reactivex.Flowable; +import io.reactivex.Single; +import io.reactivex.schedulers.Schedulers; + +public class RemotePlaylistStreamDAO extends PlaylistStreamDAO { + + private static final String ENDPOINT = "/playliststreamjoin"; + + private final RemoteDatabaseClient client; + private final AppDatabase roomDb; + + public RemotePlaylistStreamDAO(AppDatabase roomDb, Context context) { + this.roomDb = roomDb; + this.client = RemoteDatabaseClient.getInstance(context); + } + + @Override + public long insert(PlaylistStreamEntity playlistStreamEntity) { + + long id = Single.fromCallable(() -> client.post(ENDPOINT, toJson(playlistStreamEntity))) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.computation()) + .flatMap(s -> Single.just(parseObject(s))).blockingGet().getUid(); + playlistStreamEntity.setUid(id); + return roomDb.playlistStreamDAO().insert(playlistStreamEntity); + } + + @Override + public List insertAll(PlaylistStreamEntity... playlistStreamEntities) { + List result = new ArrayList<>(); + for (PlaylistStreamEntity entity : playlistStreamEntities) { + result.add(insert(entity)); + } + return result; + } + + @Override + public List insertAll(Collection playlistStreamEntities) { + List result = new ArrayList<>(); + for (PlaylistStreamEntity entity : playlistStreamEntities) { + result.add(insert(entity)); + } + return result; + } + + @Override + public Flowable> getAll() { + return roomDb.playlistStreamDAO().getAll(); + } + + @Override + public int deleteAll() { + getAll().blockingForEach(t -> delete(t)); + return 0; + } + + @Override + public int update(PlaylistStreamEntity playlistStreamEntity) { + client.put(ENDPOINT + "/" + playlistStreamEntity.getUid(), toJson(playlistStreamEntity)); + return roomDb.playlistStreamDAO().update(playlistStreamEntity); + } + + @Override + public void update(Collection playlistStreamEntities) { + for (PlaylistStreamEntity entity : playlistStreamEntities) { + update(entity); + } + } + + @Override + public Flowable> listByService(int serviceId) { + throw new UnsupportedOperationException(); + } + + @Override + public void deleteBatch(long playlistId) { + throw new UnsupportedOperationException(); + } + + @Override + public Flowable getMaximumIndexOf(long playlistId) { + return roomDb.playlistStreamDAO().getMaximumIndexOf(playlistId); + } + + @Override + public Flowable> getOrderedStreamsOf(long playlistId) { + return roomDb.playlistStreamDAO().getOrderedStreamsOf(playlistId); + } + + @Override + public Flowable> getPlaylistMetadata() { + return roomDb.playlistStreamDAO().getPlaylistMetadata(); + } + + @Override + public void delete(PlaylistStreamEntity playlistStreamEntity) { + client.delete(ENDPOINT + "/" + playlistStreamEntity.getUid()); + roomDb.playlistStreamDAO().delete(playlistStreamEntity); + } + + @Override + public int delete(Collection playlistStreamEntities) { + for (PlaylistStreamEntity entity : playlistStreamEntities) { + delete(entity); + } + return 0; + } + + public List fetchAll() throws JsonParserException { + String response = client.get(ENDPOINT); + return parseList(response); + } + + @Override + public void destroyAndRefill(Collection entities) { + throw new UnsupportedOperationException(); + } + + @NonNull + private PlaylistStreamEntity parseObject(String s) throws JsonParserException { + JsonObject jsonObject = JsonParser.object().from(s); + return fromJson(jsonObject); + } + + @NonNull + private List parseList(String s) throws JsonParserException { + JsonArray list = JsonParser.array().from(s); + List resultList = new ArrayList<>(); + for (Object object : list) { + if (object instanceof JsonObject) { + JsonObject entity = (JsonObject) object; + resultList.add(fromJson(entity)); + } + } + return resultList; + } + + private String toJson(PlaylistStreamEntity entity) { + JsonBuilder json = JsonObject.builder(); + json.value("playlistId", entity.getPlaylistUid()); + json.value("streamId", entity.getStreamUid()); + String ret = JsonWriter.string(json.done()); + return ret; + } + + private PlaylistStreamEntity fromJson(JsonObject object) { + int playlistId = object.getInt("playlistId"); + int streamId = object.getInt("streamId"); + int joinIndex = object.getInt("joinIndex"); + int uid = object.getInt("uid"); + PlaylistStreamEntity entity = new PlaylistStreamEntity(playlistId, streamId, joinIndex); + entity.setUid(uid); + return entity; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java index a5b2e8248fa..be3a1a855c9 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java @@ -4,6 +4,7 @@ import android.arch.persistence.room.Entity; import android.arch.persistence.room.ForeignKey; import android.arch.persistence.room.Index; +import android.arch.persistence.room.PrimaryKey; import org.schabi.newpipe.database.stream.model.StreamEntity; @@ -14,7 +15,6 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE; @Entity(tableName = PLAYLIST_STREAM_JOIN_TABLE, - primaryKeys = {JOIN_PLAYLIST_ID, JOIN_INDEX}, indices = { @Index(value = {JOIN_PLAYLIST_ID, JOIN_INDEX}, unique = true), @Index(value = {JOIN_STREAM_ID}) @@ -32,10 +32,15 @@ public class PlaylistStreamEntity { final public static String PLAYLIST_STREAM_JOIN_TABLE = "playlist_stream_join"; + final public static String PLAYLIST_STREAM_ID = "uid"; final public static String JOIN_PLAYLIST_ID = "playlist_id"; final public static String JOIN_STREAM_ID = "stream_id"; final public static String JOIN_INDEX = "join_index"; + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = PLAYLIST_STREAM_ID) + private long uid; + @ColumnInfo(name = JOIN_PLAYLIST_ID) private long playlistUid; @@ -51,6 +56,14 @@ public PlaylistStreamEntity(final long playlistUid, final long streamUid, final this.index = index; } + public long getUid() { + return uid; + } + + public void setUid(long uid) { + this.uid = uid; + } + public long getPlaylistUid() { return playlistUid; } diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/RemoteStreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/RemoteStreamDAO.java new file mode 100644 index 00000000000..e21d9a1886e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/RemoteStreamDAO.java @@ -0,0 +1,207 @@ +package org.schabi.newpipe.database.stream.dao; + +import android.content.Context; +import android.support.annotation.NonNull; + +import com.grack.nanojson.JsonArray; +import com.grack.nanojson.JsonBuilder; +import com.grack.nanojson.JsonObject; +import com.grack.nanojson.JsonParser; +import com.grack.nanojson.JsonParserException; +import com.grack.nanojson.JsonWriter; + +import org.schabi.newpipe.auth.EncryptionUtils; +import org.schabi.newpipe.database.AppDatabase; +import org.schabi.newpipe.database.RemoteDatabaseClient; +import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.extractor.stream.StreamType; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import io.reactivex.Flowable; +import io.reactivex.Single; +import io.reactivex.schedulers.Schedulers; + +public class RemoteStreamDAO extends StreamDAO { + + private static final String ENDPOINT = "/streams"; + + private final RemoteDatabaseClient client; + private final AppDatabase roomDb; + private final EncryptionUtils eU; + + public RemoteStreamDAO(AppDatabase roomDb, Context context) { + this.roomDb = roomDb; + this.client = RemoteDatabaseClient.getInstance(context); + try { + this.eU = EncryptionUtils.getInstance(context); + } catch (GeneralSecurityException| IOException e) { + throw new IllegalStateException("unable to get encryption utils",e); + } + } + + @Override + public long insert(StreamEntity streamEntity) { + + long id = Single.fromCallable(() -> client.post(ENDPOINT, toJson(streamEntity))) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.computation()) + .flatMap(s -> Single.just(parseObject(s))).blockingGet().getUid(); + streamEntity.setUid(id); + return roomDb.streamDAO().insert(streamEntity); + } + + @Override + public List insertAll(StreamEntity... streamEntities) { + List result = new ArrayList<>(); + for(StreamEntity entity: streamEntities){ + result.add(insert(entity)); + } + return result; + } + + @Override + public List insertAll(Collection streamEntities) { + List result = new ArrayList<>(); + for(StreamEntity entity: streamEntities){ + result.add(insert(entity)); + } + return result; + } + + @Override + public Flowable> getAll() { + return roomDb.streamDAO().getAll(); + } + + @Override + public int deleteAll() { + getAll().blockingForEach(t -> delete(t)); + return 0; + } + + @Override + public int update(StreamEntity streamEntity) { + client.put(ENDPOINT + "/" + streamEntity.getUid(), toJson(streamEntity)); + return roomDb.streamDAO().update(streamEntity); + } + + @Override + public void update(Collection streamEntities) { + for(StreamEntity entity: streamEntities){ + update(entity); + } + } + + @Override + public Flowable> listByService(int serviceId) { + return roomDb.streamDAO().listByService(serviceId); + } + + @Override + public Flowable> getStream(long serviceId, String url) { + return roomDb.streamDAO().getStream(serviceId, url); + } + + @Override + void silentInsertAllInternal(List streams) { + for(StreamEntity entity: streams){ + Long id = getStreamIdInternal(entity.getServiceId(), entity.getUrl()); + if(id == null){ + insert(entity); + } + } + } + + @Override + Long getStreamIdInternal(long serviceId, String url) { + return roomDb.streamDAO().getStreamIdInternal(serviceId, url); + } + + @Override + public int deleteOrphans() { + return 0; + } + + @Override + public void delete(StreamEntity streamEntity) { + client.delete(ENDPOINT + "/" + streamEntity.getUid()); + roomDb.streamDAO().delete(streamEntity); + } + + @Override + public int delete(Collection streamEntities) { + for(StreamEntity entity: streamEntities){ + delete(entity); + } + return 0; + } + + public List fetchAll() throws JsonParserException { + String response = client.get(ENDPOINT); + return parseList(response); + } + + @Override + public void destroyAndRefill(Collection entities) { + throw new UnsupportedOperationException(); + } + + @NonNull + private StreamEntity parseObject(String s) throws JsonParserException { + JsonObject jsonObject = JsonParser.object().from(s); + return fromJson(jsonObject); + } + + @NonNull + private List parseList(String s) throws JsonParserException { + JsonArray list = JsonParser.array().from(s); + List resultList = new ArrayList<>(); + for (Object object : list) { + if (object instanceof JsonObject) { + JsonObject entity = (JsonObject) object; + resultList.add(fromJson(entity)); + } + } + return resultList; + } + + private String toJson(StreamEntity entity){ + JsonBuilder json = JsonObject.builder(); + try { + json.value("serviceId", eU.encrypt(entity.getServiceId())); + json.value("title", eU.encrypt(entity.getTitle())); + json.value("url", eU.encrypt(entity.getUrl())); + json.value("streamType", eU.encrypt(entity.getStreamType().name())); + json.value("thumbnailUrl", eU.encrypt(entity.getThumbnailUrl())); + json.value("uploader", eU.encrypt(entity.getUploader())); + json.value("duration", eU.encrypt(entity.getDuration())); + } catch (GeneralSecurityException e) { + throw new RuntimeException("encryption error", e); + } + String ret = JsonWriter.string(json.done()); + return ret; + } + + private StreamEntity fromJson(JsonObject object){ + try { + int serviceId = Long.valueOf(eU.decryptAsLong(object.getString("serviceId"))).intValue(); + String title = eU.decrypt(object.getString("title")); + String url = eU.decrypt(object.getString("url")); + String thumbnailUrl = eU.decrypt(object.getString("thumbnailUrl")); + StreamType streamType = StreamType.valueOf(eU.decrypt(object.getString("streamType"))); + String uploader = eU.decrypt(object.getString("uploader")); + Long duration = eU.decryptAsLong(object.getString("duration")); + int uid = object.getInt("uid"); + StreamEntity entity = new StreamEntity(serviceId, title, url, streamType, thumbnailUrl, uploader, duration); + entity.setUid(uid); + return entity; + } catch (GeneralSecurityException e) { + throw new RuntimeException("decryption error", e); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/RemoteStreamStateDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/RemoteStreamStateDAO.java new file mode 100644 index 00000000000..b624eb55068 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/RemoteStreamStateDAO.java @@ -0,0 +1,192 @@ +package org.schabi.newpipe.database.stream.dao; + +import android.content.Context; +import android.support.annotation.NonNull; + +import com.grack.nanojson.JsonArray; +import com.grack.nanojson.JsonBuilder; +import com.grack.nanojson.JsonObject; +import com.grack.nanojson.JsonParser; +import com.grack.nanojson.JsonParserException; +import com.grack.nanojson.JsonWriter; + +import org.schabi.newpipe.auth.EncryptionUtils; +import org.schabi.newpipe.database.AppDatabase; +import org.schabi.newpipe.database.RemoteDatabaseClient; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import io.reactivex.Flowable; +import io.reactivex.Single; +import io.reactivex.schedulers.Schedulers; + +public class RemoteStreamStateDAO extends StreamStateDAO { + + private static final String ENDPOINT = "/streamstates"; + + private final RemoteDatabaseClient client; + private final AppDatabase roomDb; + private final EncryptionUtils eU; + + public RemoteStreamStateDAO(AppDatabase roomDb, Context context) { + this.roomDb = roomDb; + this.client = RemoteDatabaseClient.getInstance(context); + try { + this.eU = EncryptionUtils.getInstance(context); + } catch (GeneralSecurityException | IOException e) { + throw new IllegalStateException("unable to get encryption utils",e); + } + } + + @Override + public long insert(StreamStateEntity streamStateEntity) { + + long id = Single.fromCallable(() -> client.post(ENDPOINT, toJson(streamStateEntity))) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.computation()) + .flatMap(s -> Single.just(parseObject(s))).blockingGet().getUid(); + streamStateEntity.setUid(id); + return roomDb.streamStateDAO().insert(streamStateEntity); + } + + @Override + public List insertAll(StreamStateEntity... streamStateEntities) { + List result = new ArrayList<>(); + for(StreamStateEntity entity: streamStateEntities){ + result.add(insert(entity)); + } + return result; + } + + @Override + public List insertAll(Collection streamStateEntities) { + List result = new ArrayList<>(); + for(StreamStateEntity entity: streamStateEntities){ + result.add(insert(entity)); + } + return result; + } + + @Override + public Flowable> getAll() { + return roomDb.streamStateDAO().getAll(); + } + + @Override + public int deleteAll() { + getAll().blockingForEach(t -> delete(t)); + return 0; + } + + @Override + public int update(StreamStateEntity streamStateEntity) { + client.put(ENDPOINT + "/" + streamStateEntity.getUid(), toJson(streamStateEntity)); + return roomDb.streamStateDAO().update(streamStateEntity); + } + + @Override + public void update(Collection streamStateEntities) { + for(StreamStateEntity entity: streamStateEntities){ + update(entity); + } + } + + @Override + public Flowable> listByService(int serviceId) { + throw new UnsupportedOperationException(); + } + + @Override + public Flowable> getState(long streamId) { + return roomDb.streamStateDAO().getState(streamId); + } + + @Override + public int deleteState(long streamId) { + client.delete(ENDPOINT + "/" + streamId); + return roomDb.streamStateDAO().deleteState(streamId); + } + + @Override + void silentInsertInternal(StreamStateEntity streamState) { + + List state = getState(streamState.getStreamUid()).blockingFirst(); + if(state.isEmpty()){ + streamState.setUid(insert(streamState)); + }else{ + streamState.setUid(state.get(0).getUid()); + } + } + + @Override + public void delete(StreamStateEntity streamStateEntity) { + deleteState(streamStateEntity.getUid()); + } + + @Override + public int delete(Collection streamStateEntities) { + for(StreamStateEntity entity: streamStateEntities){ + delete(entity); + } + return 0; + } + + public List fetchAll() throws JsonParserException { + String response = client.get(ENDPOINT); + return parseList(response); + } + + @Override + public void destroyAndRefill(Collection entities) { + throw new UnsupportedOperationException(); + } + + @NonNull + private StreamStateEntity parseObject(String s) throws JsonParserException { + JsonObject jsonObject = JsonParser.object().from(s); + return fromJson(jsonObject); + } + + @NonNull + private List parseList(String s) throws JsonParserException { + JsonArray list = JsonParser.array().from(s); + List resultList = new ArrayList<>(); + for (Object object : list) { + if (object instanceof JsonObject) { + JsonObject entity = (JsonObject) object; + resultList.add(fromJson(entity)); + } + } + return resultList; + } + + private String toJson(StreamStateEntity entity){ + JsonBuilder json = JsonObject.builder(); + try { + json.value("streamId", entity.getStreamUid()); + json.value("progressTime", eU.encrypt(entity.getProgressTime())); + } catch (GeneralSecurityException e) { + throw new RuntimeException("encryption error", e); + } + String ret = JsonWriter.string(json.done()); + return ret; + } + + private StreamStateEntity fromJson(JsonObject object){ + try { + long streamUid = object.getInt("streamId"); + long progressTime = eU.decryptAsLong(object.getString("progressTime")); + long uid = object.getInt("uid"); + StreamStateEntity entity = new StreamStateEntity(streamUid, progressTime); + entity.setUid(uid); + return entity; + } catch (GeneralSecurityException e) { + throw new RuntimeException("decryption error", e); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java index 396a29fcad8..b3b54ef7d64 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java @@ -7,21 +7,22 @@ import android.arch.persistence.room.Transaction; import org.schabi.newpipe.database.BasicDAO; +import org.schabi.newpipe.database.history.model.StreamHistoryEntity; import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.database.history.model.StreamHistoryEntity; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import io.reactivex.Flowable; +import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_SERVICE_ID; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL; -import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; @Dao public abstract class StreamDAO implements BasicDAO { @@ -84,15 +85,23 @@ public List upsertAll(List streams) { @Query("DELETE FROM " + STREAM_TABLE + " WHERE " + STREAM_ID + " NOT IN " + - "(SELECT DISTINCT " + STREAM_ID + " FROM " + STREAM_TABLE + + "(SELECT DISTINCT " + STREAM_TABLE + "." + STREAM_ID + " FROM " + STREAM_TABLE + " LEFT JOIN " + STREAM_HISTORY_TABLE + - " ON " + STREAM_ID + " = " + + " ON " + STREAM_TABLE + "." + STREAM_ID + " = " + StreamHistoryEntity.STREAM_HISTORY_TABLE + "." + StreamHistoryEntity.JOIN_STREAM_ID + " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE + - " ON " + STREAM_ID + " = " + + " ON " + STREAM_TABLE + "." + STREAM_ID + " = " + PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE + "." + PlaylistStreamEntity.JOIN_STREAM_ID + ")") public abstract int deleteOrphans(); + + + @Override + @Transaction + public void destroyAndRefill(Collection entities) { + deleteAll(); + insertAll(entities); + } } diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java index 1c06f4df9b9..1ac0cc3664b 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java @@ -7,8 +7,10 @@ import android.arch.persistence.room.Transaction; import org.schabi.newpipe.database.BasicDAO; +import org.schabi.newpipe.database.history.model.StreamHistoryEntity; import org.schabi.newpipe.database.stream.model.StreamStateEntity; +import java.util.Collection; import java.util.List; import io.reactivex.Flowable; @@ -45,4 +47,11 @@ public long upsert(StreamStateEntity stream) { silentInsertInternal(stream); return update(stream); } + + @Override + @Transaction + public void destroyAndRefill(Collection entities) { + deleteAll(); + insertAll(entities); + } } diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java index d46d5cd7492..59f96d3ce53 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java @@ -4,6 +4,8 @@ import android.arch.persistence.room.ColumnInfo; import android.arch.persistence.room.Entity; import android.arch.persistence.room.ForeignKey; +import android.arch.persistence.room.Index; +import android.arch.persistence.room.PrimaryKey; import android.support.annotation.Nullable; import java.util.concurrent.TimeUnit; @@ -13,7 +15,7 @@ import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; @Entity(tableName = STREAM_STATE_TABLE, - primaryKeys = {JOIN_STREAM_ID}, + indices = {@Index(value = {JOIN_STREAM_ID}, unique = true)}, foreignKeys = { @ForeignKey(entity = StreamEntity.class, parentColumns = StreamEntity.STREAM_ID, @@ -22,6 +24,7 @@ }) public class StreamStateEntity { final public static String STREAM_STATE_TABLE = "stream_state"; + final public static String STREAM_STATE_ID = "uid"; final public static String JOIN_STREAM_ID = "stream_id"; final public static String STREAM_PROGRESS_TIME = "progress_time"; @@ -31,6 +34,10 @@ public class StreamStateEntity { /** Playback state will not be saved, if time left less than this threshold */ private static final int PLAYBACK_SAVE_THRESHOLD_END_SECONDS = 10; + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = STREAM_STATE_ID) + private long uid; + @ColumnInfo(name = JOIN_STREAM_ID) private long streamUid; @@ -50,6 +57,14 @@ public void setStreamUid(long streamUid) { this.streamUid = streamUid; } + public Long getUid() { + return uid; + } + + public void setUid(long uid) { + this.uid = uid; + } + public long getProgressTime() { return progressTime; } diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/RemoteSubscriptionDAO.java b/app/src/main/java/org/schabi/newpipe/database/subscription/RemoteSubscriptionDAO.java new file mode 100644 index 00000000000..5a4ea730bc3 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/subscription/RemoteSubscriptionDAO.java @@ -0,0 +1,199 @@ +package org.schabi.newpipe.database.subscription; + +import android.content.Context; +import android.support.annotation.NonNull; + +import com.grack.nanojson.JsonArray; +import com.grack.nanojson.JsonBuilder; +import com.grack.nanojson.JsonObject; +import com.grack.nanojson.JsonParser; +import com.grack.nanojson.JsonParserException; +import com.grack.nanojson.JsonWriter; + +import org.schabi.newpipe.auth.EncryptionUtils; +import org.schabi.newpipe.database.AppDatabase; +import org.schabi.newpipe.database.RemoteDatabaseClient; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import io.reactivex.Flowable; +import io.reactivex.Single; +import io.reactivex.schedulers.Schedulers; + +public class RemoteSubscriptionDAO extends SubscriptionDAO { + + private static final String ENDPOINT = "/subscriptions"; + + private final RemoteDatabaseClient client; + private final AppDatabase roomDb; + private final EncryptionUtils eU; + + public RemoteSubscriptionDAO(AppDatabase roomDb, Context context) { + this.roomDb = roomDb; + this.client = RemoteDatabaseClient.getInstance(context); + try { + this.eU = EncryptionUtils.getInstance(context); + } catch (GeneralSecurityException | IOException e) { + throw new IllegalStateException("unable to get encryption utils", e); + } + } + + @Override + public long insert(SubscriptionEntity subscriptionEntity) { + long id = Single.fromCallable(() -> client.post(ENDPOINT, toJson(subscriptionEntity))) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.computation()) + .flatMap(s -> Single.just(parseObject(s))).blockingGet().getUid(); + subscriptionEntity.setUid(id); + return roomDb.subscriptionDAO().insert(subscriptionEntity); + } + + @Override + public List insertAll(SubscriptionEntity... subscriptionEntities) { + List result = new ArrayList<>(); + for (SubscriptionEntity entity : subscriptionEntities) { + result.add(insert(entity)); + } + return result; + } + + @Override + public List insertAll(Collection subscriptionEntities) { + List result = new ArrayList<>(); + for (SubscriptionEntity entity : subscriptionEntities) { + result.add(insert(entity)); + } + return result; + } + + @Override + public Flowable> getAll() { + return roomDb.subscriptionDAO().getAll(); + } + + public List fetchAll() throws JsonParserException { + String response = client.get(ENDPOINT); + return parseList(response); + } + + @Override + public int deleteAll() { + getAll().blockingForEach(t -> delete(t)); + return 0; + } + + @Override + public int update(SubscriptionEntity subscriptionEntity) { + client.put(ENDPOINT + "/" + subscriptionEntity.getUid(), toJson(subscriptionEntity)); + return roomDb.subscriptionDAO().update(subscriptionEntity); + } + + @Override + public void update(Collection subscriptionEntities) { + for (SubscriptionEntity entity : subscriptionEntities) { + update(entity); + } + } + + @Override + public Flowable> listByService(int serviceId) { + return roomDb.subscriptionDAO().listByService(serviceId); + } + + @Override + public Flowable> getSubscription(int serviceId, String url) { + return roomDb.subscriptionDAO().getSubscription(serviceId, url); + } + + @Override + Long getSubscriptionIdInternal(int serviceId, String url) { + return roomDb.subscriptionDAO().getSubscriptionIdInternal(serviceId, url); + } + + @Override + Long insertInternal(SubscriptionEntity entities) { + Long uid = getSubscriptionIdInternal(entities.getServiceId(), entities.getUrl()); + if (null == uid) { + return insert(entities); + } + return -1L; + } + + @Override + public void delete(SubscriptionEntity subscriptionEntity) { + client.delete(ENDPOINT + "/" + subscriptionEntity.getUid()); + roomDb.subscriptionDAO().delete(subscriptionEntity); + } + + @Override + public int delete(Collection subscriptionEntities) { + for (SubscriptionEntity entity : subscriptionEntities) { + delete(entity); + } + return 0; + } + + @Override + public void destroyAndRefill(Collection entities) { + throw new UnsupportedOperationException(); + } + + @NonNull + private SubscriptionEntity parseObject(String s) throws JsonParserException { + JsonObject jsonObject = JsonParser.object().from(s); + return fromJson(jsonObject); + } + + @NonNull + private List parseList(String s) throws JsonParserException { + JsonArray list = JsonParser.array().from(s); + List subscriptions = new ArrayList<>(); + for (Object object : list) { + if (object instanceof JsonObject) { + JsonObject entity = (JsonObject) object; + subscriptions.add(fromJson(entity)); + } + } + return subscriptions; + } + + private String toJson(SubscriptionEntity entity) { + JsonBuilder json = JsonObject.builder(); + try { + json.value("serviceId", eU.encrypt(entity.getServiceId())); + json.value("name", eU.encrypt(entity.getName())); + json.value("url", eU.encrypt(entity.getUrl())); + json.value("avatarUrl", eU.encrypt(entity.getAvatarUrl())); + json.value("description", eU.encrypt(entity.getDescription())); + json.value("subscriberCount", eU.encrypt(entity.getSubscriberCount())); + } catch (GeneralSecurityException e) { + throw new RuntimeException("encryption error", e); + } + String ret = JsonWriter.string(json.done()); + return ret; + } + + private SubscriptionEntity fromJson(JsonObject object) { + try { + int serviceId = Long.valueOf(eU.decryptAsLong(object.getString("serviceId"))).intValue(); + String name = eU.decrypt(object.getString("name")); + String url = eU.decrypt(object.getString("url")); + String avatarUrl = eU.decrypt(object.getString("avatarUrl")); + String description = eU.decrypt(object.getString("description")); + Long subscriberCount = eU.decryptAsLong(object.getString("subscriberCount")); + int uid = object.getInt("uid"); + SubscriptionEntity entity = new SubscriptionEntity(); + entity.setData(name, avatarUrl, description, subscriberCount); + entity.setServiceId(serviceId); + entity.setUrl(url); + entity.setUid(uid); + return entity; + } catch (GeneralSecurityException e) { + throw new RuntimeException("decryption error", e); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.java b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.java index ee6dea9fedb..550e1e8c8f0 100644 --- a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.java @@ -8,6 +8,7 @@ import org.schabi.newpipe.database.BasicDAO; +import java.util.Collection; import java.util.List; import io.reactivex.Flowable; @@ -66,4 +67,11 @@ public List upsertAll(List entities) { return entities; } + + @Override + @Transaction + public void destroyAndRefill(Collection subscriptionEntities) { + deleteAll(); + insertAll(subscriptionEntities); + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/auth/LoginFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/auth/LoginFragment.java new file mode 100644 index 00000000000..e70c1e59073 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/auth/LoginFragment.java @@ -0,0 +1,197 @@ +package org.schabi.newpipe.fragments.auth; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.design.widget.TextInputLayout; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ProgressBar; +import android.widget.Toast; + +import org.schabi.newpipe.BaseFragment; +import org.schabi.newpipe.NewPipeDatabase; +import org.schabi.newpipe.R; +import org.schabi.newpipe.auth.AuthService; +import org.schabi.newpipe.database.RemoteDatabase; +import org.schabi.newpipe.util.NavigationHelper; + +import icepick.State; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; + +public class LoginFragment extends BaseFragment implements OnClickListener { + + private Button loginButton; + private Button switchButton; + private Button skipButton; + private EditText usernameET; + private EditText passwordET; + private EditText confirmPasswordET; + private TextInputLayout confirmPasswordLayout; + private ProgressBar progressBar; + + @State + protected boolean isSignup; + + private CompositeDisposable disposables = new CompositeDisposable(); + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_login, container, false); + } + + @Override + protected void initViews(View rootView, Bundle savedInstanceState) { + super.initViews(rootView, savedInstanceState); + loginButton = rootView.findViewById(R.id.login_btn); + switchButton = rootView.findViewById(R.id.switch_btn); + skipButton = rootView.findViewById(R.id.skip_login_btn); + usernameET = rootView.findViewById(R.id.login_username); + passwordET = rootView.findViewById(R.id.login_password); + confirmPasswordET = rootView.findViewById(R.id.confirm_password); + confirmPasswordLayout = rootView.findViewById(R.id.confirm_password_layout); + progressBar = rootView.findViewById(R.id.loading_progress_bar); + + setTitle(getString(R.string.account)); + + if(isSignup){ + confirmPasswordLayout.setVisibility(View.VISIBLE); + loginButton.setText(R.string.signup_btn); + switchButton.setText(R.string.already_user); + } + } + + @Override + protected void initListeners() { + super.initListeners(); + loginButton.setOnClickListener(this); + switchButton.setOnClickListener(this); + skipButton.setOnClickListener(this); + + passwordET.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + confirmPasswordLayout.setError(null); + } + }); + + confirmPasswordET.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + confirmPasswordLayout.setError(null); + } + }); + } + + @Override + public void onClick(View v) { + switch (v.getId()) { + case R.id.login_btn: + if(isSignup){ + signup(); + }else{ + login(); + } + break; + case R.id.switch_btn: + if(isSignup){ + confirmPasswordLayout.setVisibility(View.GONE); + loginButton.setText(R.string.login_btn); + switchButton.setText(R.string.new_user); + isSignup = false; + }else{ + confirmPasswordLayout.setVisibility(View.VISIBLE); + loginButton.setText(R.string.signup_btn); + switchButton.setText(R.string.already_user); + isSignup = true; + } + break; + case R.id.skip_login_btn: + SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(getContext()).edit(); + editor.putBoolean("skip_login", true); + editor.apply(); + NavigationHelper.openMainActivity(getContext()); + break; + } + } + + private void login(){ + String username = usernameET.getText().toString(); + String password = passwordET.getText().toString(); + Context context = getContext().getApplicationContext(); + progressBar.setVisibility(View.VISIBLE); + Disposable disposable = AuthService.getInstance(context).login(username, password).observeOn(AndroidSchedulers.mainThread()).subscribe(() -> { + progressBar.setVisibility(View.GONE); + Toast.makeText(getContext().getApplicationContext(), "Logged in as " + username, Toast.LENGTH_SHORT).show(); + // sync data post login + RemoteDatabase remoteDatabase = (RemoteDatabase) NewPipeDatabase.getInstance(context); + remoteDatabase.sync().subscribe(); + // go to main activity + NavigationHelper.openMainActivity(getContext()); + }, e -> { + progressBar.setVisibility(View.GONE); + Toast.makeText(getContext().getApplicationContext(), "Login failed" , Toast.LENGTH_SHORT).show(); + }); + disposables.add(disposable); + } + + private void signup(){ + String username = usernameET.getText().toString(); + String password = passwordET.getText().toString(); + String confirmPassword = confirmPasswordET.getText().toString(); + if(!password.equals(confirmPassword)){ + confirmPasswordLayout.setError("password does not match!"); + return; + } + progressBar.setVisibility(View.VISIBLE); + Context context = getContext().getApplicationContext(); + Disposable disposable = AuthService.getInstance(context).signup(username, password).observeOn(AndroidSchedulers.mainThread()).subscribe(() -> { + progressBar.setVisibility(View.GONE); + Toast.makeText(getContext().getApplicationContext(), "Logged in as " + username, Toast.LENGTH_SHORT).show(); + // sync data post login + RemoteDatabase remoteDatabase = (RemoteDatabase) NewPipeDatabase.getInstance(context); + remoteDatabase.sync().subscribe(); + // go to main activity + NavigationHelper.openMainActivity(getContext()); + }, e -> { + progressBar.setVisibility(View.GONE); + Toast.makeText(getContext().getApplicationContext(), "Signup failed" , Toast.LENGTH_SHORT).show(); + }); + disposables.add(disposable); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (disposables != null) disposables.clear(); + disposables = null; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.java index 956e6c1c832..481639a7c53 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.java @@ -93,7 +93,7 @@ public void showLoading() { public void handleResult(@NonNull CommentsInfo result) { super.handleResult(result); - AnimationUtils.slideUp(getView(),120, 96, 0.06f); + AnimationUtils.slideUp(getView(),120, 150, 0.06f); if (!result.getErrors().isEmpty()) { showSnackBarError(result.getErrors(), UserAction.REQUESTED_COMMENTS, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index b469be3b87b..e9ef9088e58 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -5,7 +5,7 @@ import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; +import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.app.AppCompatActivity; import android.text.TextUtils; import android.util.Log; @@ -57,7 +57,7 @@ import static org.schabi.newpipe.util.AnimationUtils.animateView; -public class PlaylistFragment extends BaseListInfoFragment { +public class PlaylistFragment extends BaseListInfoFragment implements SwipeRefreshLayout.OnRefreshListener { private CompositeDisposable disposables; private Subscription bookmarkReactor; @@ -82,6 +82,7 @@ public class PlaylistFragment extends BaseListInfoFragment { private View headerBackgroundButton; private MenuItem playlistBookmarkButton; + private SwipeRefreshLayout swipeRefreshLayout; public static PlaylistFragment getInstance(int serviceId, String url, String name) { PlaylistFragment instance = new PlaylistFragment(); @@ -134,6 +135,13 @@ protected void initViews(View rootView, Bundle savedInstanceState) { super.initViews(rootView, savedInstanceState); infoListAdapter.useMiniItemVariants(true); + swipeRefreshLayout = rootView.findViewById(R.id.swipeToRefresh); + } + + @Override + protected void initListeners() { + super.initListeners(); + swipeRefreshLayout.setOnRefreshListener(this); } private PlayQueue getPlayQueueStartingAt(StreamInfoItem infoItem) { @@ -445,4 +453,9 @@ private void updateBookmarkButtons() { playlistBookmarkButton.setIcon(ThemeHelper.resolveResourceIdFromAttr(activity, iconAttr)); playlistBookmarkButton.setTitle(titleRes); } + + @Override + public void onRefresh() { + swipeRefreshLayout.setRefreshing(false); + } } \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java index 99bd70f5bed..78393de566f 100644 --- a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java @@ -6,16 +6,19 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.FragmentManager; +import android.support.v4.widget.SwipeRefreshLayout; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.Toast; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; -import org.schabi.newpipe.database.AppDatabase; +import org.schabi.newpipe.database.Database; import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.RemoteDatabase; import org.schabi.newpipe.database.playlist.PlaylistLocalItem; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; @@ -35,13 +38,16 @@ import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; public final class BookmarkFragment - extends BaseLocalListFragment, Void> { + extends BaseLocalListFragment, Void> implements SwipeRefreshLayout.OnRefreshListener { @State protected Parcelable itemsListState; + private SwipeRefreshLayout swipeRefreshLayout; + private Subscription databaseSubscription; private CompositeDisposable disposables = new CompositeDisposable(); private LocalPlaylistManager localPlaylistManager; @@ -55,7 +61,7 @@ public final class BookmarkFragment public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (activity == null) return; - final AppDatabase database = NewPipeDatabase.getInstance(activity); + final Database database = NewPipeDatabase.getInstance(activity); localPlaylistManager = new LocalPlaylistManager(database); remotePlaylistManager = new RemotePlaylistManager(database); disposables = new CompositeDisposable(); @@ -89,6 +95,8 @@ public void setUserVisibleHint(boolean isVisibleToUser) { @Override protected void initViews(View rootView, Bundle savedInstanceState) { super.initViews(rootView, savedInstanceState); + + swipeRefreshLayout = rootView.findViewById(R.id.swipeToRefresh); } @Override @@ -125,6 +133,8 @@ public void held(LocalItem selectedItem) { } } }); + + swipeRefreshLayout.setOnRefreshListener(this); } /////////////////////////////////////////////////////////////////////////// @@ -283,5 +293,22 @@ private static List merge(final List l return items; } + + @Override + public void onRefresh() { + Database db = NewPipeDatabase.getInstance(getContext().getApplicationContext()); + if(db instanceof RemoteDatabase){ + Disposable disposable = ((RemoteDatabase) db).refreshPlaylists().observeOn(AndroidSchedulers.mainThread()).subscribe(() -> { + swipeRefreshLayout.setRefreshing(false); + }, (e) -> { + swipeRefreshLayout.setRefreshing(false); + Toast.makeText(getContext(), "Failed to refresh content", Toast.LENGTH_SHORT).show(); + }); + disposables.add(disposable); + }else{ + swipeRefreshLayout.setRefreshing(false); + } + + } } diff --git a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java index 6714edcc52c..ed0716f45bb 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java @@ -25,7 +25,7 @@ import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; -import org.schabi.newpipe.database.AppDatabase; +import org.schabi.newpipe.database.Database; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.history.dao.SearchHistoryDAO; import org.schabi.newpipe.database.history.dao.StreamHistoryDAO; @@ -51,13 +51,12 @@ import io.reactivex.Completable; import io.reactivex.Flowable; import io.reactivex.Maybe; -import io.reactivex.Scheduler; import io.reactivex.Single; import io.reactivex.schedulers.Schedulers; public class HistoryRecordManager { - private final AppDatabase database; + private final Database database; private final StreamDAO streamTable; private final StreamHistoryDAO streamHistoryTable; private final SearchHistoryDAO searchHistoryTable; @@ -85,7 +84,7 @@ public Maybe onViewed(final StreamInfo info) { if (!isStreamHistoryEnabled()) return Maybe.empty(); final Date currentTime = new Date(); - return Maybe.fromCallable(() -> database.runInTransaction(() -> { + return Maybe.fromCallable(() -> { final long streamId = streamTable.upsert(new StreamEntity(info)); StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(streamId); @@ -97,7 +96,7 @@ public Maybe onViewed(final StreamInfo info) { } else { return streamHistoryTable.insert(new StreamHistoryEntity(streamId, currentTime)); } - })).subscribeOn(Schedulers.io()); + }).subscribeOn(Schedulers.io()); } public Single deleteStreamHistory(final long streamId) { @@ -155,7 +154,7 @@ public Maybe onSearched(final int serviceId, final String search) { final Date currentTime = new Date(); final SearchHistoryEntry newEntry = new SearchHistoryEntry(currentTime, serviceId, search); - return Maybe.fromCallable(() -> database.runInTransaction(() -> { + return Maybe.fromCallable(() -> { SearchHistoryEntry latestEntry = searchHistoryTable.getLatestEntry(); if (latestEntry != null && latestEntry.hasEqualValues(newEntry)) { latestEntry.setCreationDate(currentTime); @@ -163,7 +162,7 @@ public Maybe onSearched(final int serviceId, final String search) { } else { return searchHistoryTable.insert(newEntry); } - })).subscribeOn(Schedulers.io()); + }).subscribeOn(Schedulers.io()); } public Single deleteSearchHistory(final String search) { @@ -219,7 +218,7 @@ public Maybe loadStreamState(final StreamInfo info) { } public Completable saveStreamState(@NonNull final StreamInfo info, final long progressTime) { - return Completable.fromAction(() -> database.runInTransaction(() -> { + return Completable.fromAction(() -> { final long streamId = streamTable.upsert(new StreamEntity(info)); final StreamStateEntity state = new StreamStateEntity(streamId, progressTime); if (state.isValid((int) info.getDuration())) { @@ -227,7 +226,7 @@ public Completable saveStreamState(@NonNull final StreamInfo info, final long pr } else { streamStateTable.deleteState(streamId); } - })).subscribeOn(Schedulers.io()); + }).subscribeOn(Schedulers.io()); } public Single loadStreamState(final InfoItem info) { @@ -298,4 +297,5 @@ public Single removeOrphanedRecords() { return Single.fromCallable(streamTable::deleteOrphans).subscribeOn(Schedulers.io()); } + } diff --git a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java index 149dcfbdf25..ef59723f440 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java @@ -7,6 +7,7 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.design.widget.Snackbar; +import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.app.AlertDialog; import android.view.LayoutInflater; import android.view.Menu; @@ -20,9 +21,11 @@ import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; +import org.schabi.newpipe.database.Database; import org.schabi.newpipe.database.LocalItem; -import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; +import org.schabi.newpipe.database.RemoteDatabase; import org.schabi.newpipe.database.stream.StreamStatisticsEntry; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamType; @@ -48,7 +51,7 @@ import io.reactivex.disposables.Disposable; public class StatisticsPlaylistFragment - extends BaseLocalListFragment, Void> { + extends BaseLocalListFragment, Void> implements SwipeRefreshLayout.OnRefreshListener { private View headerPlayAllButton; private View headerPopupButton; @@ -57,6 +60,7 @@ public class StatisticsPlaylistFragment private View sortButton; private ImageView sortButtonIcon; private TextView sortButtonText; + private SwipeRefreshLayout swipeRefreshLayout; @State protected Parcelable itemsListState; @@ -66,6 +70,22 @@ public class StatisticsPlaylistFragment private HistoryRecordManager recordManager; private final CompositeDisposable disposables = new CompositeDisposable(); + @Override + public void onRefresh() { + Database db = NewPipeDatabase.getInstance(getContext().getApplicationContext()); + if(db instanceof RemoteDatabase){ + Disposable disposable = ((RemoteDatabase) db).refreshHistory().observeOn(AndroidSchedulers.mainThread()).subscribe(() -> { + swipeRefreshLayout.setRefreshing(false); + }, (e) -> { + swipeRefreshLayout.setRefreshing(false); + Toast.makeText(getContext(), "Failed to refresh content", Toast.LENGTH_SHORT).show(); + }); + disposables.add(disposable); + }else{ + swipeRefreshLayout.setRefreshing(false); + } + } + private enum StatisticSortMode { LAST_PLAYED, MOST_PLAYED, @@ -125,6 +145,7 @@ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { @Override protected void initViews(View rootView, Bundle savedInstanceState) { super.initViews(rootView, savedInstanceState); + swipeRefreshLayout = rootView.findViewById(R.id.swipeToRefresh); if(!useAsFrontPage) { setTitle(getString(R.string.title_last_played)); } @@ -167,6 +188,8 @@ public void held(LocalItem selectedItem) { } } }); + + swipeRefreshLayout.setOnRefreshListener(this); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java index a8750ed47d8..88b31f3038d 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java @@ -6,6 +6,7 @@ import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.app.AlertDialog; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.helper.ItemTouchHelper; @@ -22,7 +23,9 @@ import org.reactivestreams.Subscription; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; +import org.schabi.newpipe.database.Database; import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.RemoteDatabase; import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamType; @@ -51,7 +54,7 @@ import static org.schabi.newpipe.util.AnimationUtils.animateView; -public class LocalPlaylistFragment extends BaseLocalListFragment, Void> { +public class LocalPlaylistFragment extends BaseLocalListFragment, Void> implements SwipeRefreshLayout.OnRefreshListener { // Save the list 10 seconds after the last change occurred private static final long SAVE_DEBOUNCE_MILLIS = 10000; @@ -66,6 +69,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment { + swipeRefreshLayout.setRefreshing(false); + }, (e) -> { + swipeRefreshLayout.setRefreshing(false); + Toast.makeText(getContext(), "Failed to refresh content", Toast.LENGTH_SHORT).show(); + }); + disposables.add(disposable); + }else{ + swipeRefreshLayout.setRefreshing(false); + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistManager.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistManager.java index 3ed1b864c3f..7fcdf5dd276 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistManager.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistManager.java @@ -2,7 +2,7 @@ import android.support.annotation.Nullable; -import org.schabi.newpipe.database.AppDatabase; +import org.schabi.newpipe.database.Database; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; import org.schabi.newpipe.database.playlist.dao.PlaylistDAO; @@ -23,12 +23,12 @@ public class LocalPlaylistManager { - private final AppDatabase database; + private final Database database; private final StreamDAO streamTable; private final PlaylistDAO playlistTable; private final PlaylistStreamDAO playlistStreamTable; - public LocalPlaylistManager(final AppDatabase db) { + public LocalPlaylistManager(final Database db) { database = db; streamTable = db.streamDAO(); playlistTable = db.playlistDAO(); @@ -42,18 +42,17 @@ public Maybe> createPlaylist(final String name, final List database.runInTransaction(() -> - upsertStreams(playlistTable.insert(newPlaylist), streams, 0)) + return Maybe.fromCallable(() -> upsertStreams(playlistTable.insert(newPlaylist), streams, 0) ).subscribeOn(Schedulers.io()); } public Maybe> appendToPlaylist(final long playlistId, final List streams) { + return playlistStreamTable.getMaximumIndexOf(playlistId) .firstElement() - .map(maxJoinIndex -> database.runInTransaction(() -> - upsertStreams(playlistId, streams, maxJoinIndex + 1)) - ).subscribeOn(Schedulers.io()); + .map(maxJoinIndex -> + upsertStreams(playlistId, streams, maxJoinIndex + 1)).subscribeOn(Schedulers.io()); } private List upsertStreams(final long playlistId, @@ -75,10 +74,10 @@ public Completable updateJoin(final long playlistId, final List streamIds) joinEntities.add(new PlaylistStreamEntity(playlistId, streamIds.get(i), i)); } - return Completable.fromRunnable(() -> database.runInTransaction(() -> { + return Completable.fromRunnable(() -> { playlistStreamTable.deleteBatch(playlistId); playlistStreamTable.insertAll(joinEntities); - })).subscribeOn(Schedulers.io()); + }).subscribeOn(Schedulers.io()); } public Flowable> getPlaylists() { diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.java b/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.java index 1ae8a22a8dd..dfa32ca4992 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.java @@ -1,6 +1,6 @@ package org.schabi.newpipe.local.playlist; -import org.schabi.newpipe.database.AppDatabase; +import org.schabi.newpipe.database.Database; import org.schabi.newpipe.database.playlist.dao.PlaylistRemoteDAO; import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; import org.schabi.newpipe.extractor.playlist.PlaylistInfo; @@ -15,7 +15,7 @@ public class RemotePlaylistManager { private final PlaylistRemoteDAO playlistRemoteTable; - public RemotePlaylistManager(final AppDatabase db) { + public RemotePlaylistManager(final Database db) { playlistRemoteTable = db.playlistRemoteDAO(); } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java index a2727c29b9f..49a347d5883 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java @@ -22,6 +22,7 @@ import android.support.annotation.Nullable; import android.support.v4.app.FragmentManager; import android.support.v4.content.LocalBroadcastManager; +import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.app.ActionBar; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.LinearLayoutManager; @@ -37,7 +38,10 @@ import com.nononsenseapps.filepicker.Utils; +import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; +import org.schabi.newpipe.database.Database; +import org.schabi.newpipe.database.RemoteDatabase; import org.schabi.newpipe.database.subscription.SubscriptionEntity; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.NewPipe; @@ -67,6 +71,7 @@ import java.util.Locale; import icepick.State; +import io.reactivex.Completable; import io.reactivex.Observer; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; @@ -79,11 +84,13 @@ import static org.schabi.newpipe.util.AnimationUtils.animateRotation; import static org.schabi.newpipe.util.AnimationUtils.animateView; -public class SubscriptionFragment extends BaseStateFragment> implements SharedPreferences.OnSharedPreferenceChangeListener { +public class SubscriptionFragment extends BaseStateFragment> implements SharedPreferences.OnSharedPreferenceChangeListener, SwipeRefreshLayout.OnRefreshListener { private static final int REQUEST_EXPORT_CODE = 666; private static final int REQUEST_IMPORT_CODE = 667; private RecyclerView itemsList; + private SwipeRefreshLayout swipeRefreshLayout; + @State protected Parcelable itemsListState; private InfoListAdapter infoListAdapter; @@ -353,6 +360,10 @@ protected void initViews(View rootView, Bundle savedInstanceState) { importExportOptions.addListener(getExpandIconSyncListener(headerRootLayout.findViewById(R.id.import_export_expand_icon))); importExportOptions.ready(); + + swipeRefreshLayout = rootView.findViewById(R.id.swipeToRefresh); + + } private CollapsibleView.StateListener getExpandIconSyncListener(final ImageView iconView) { @@ -384,6 +395,8 @@ public void held(ChannelInfoItem selectedItem) { NavigationHelper.openWhatsNewFragment(fragmentManager); }); importExportListHeader.setOnClickListener(v -> importExportOptions.switchState()); + + swipeRefreshLayout.setOnRefreshListener(this); } private void showLongTapDialog(ChannelInfoItem selectedItem) { @@ -592,4 +605,20 @@ protected boolean isGridLayout() { return "grid".equals(list_mode); } } + + @Override + public void onRefresh() { + Database db = NewPipeDatabase.getInstance(getContext().getApplicationContext()); + if(db instanceof RemoteDatabase){ + Disposable disposable = ((RemoteDatabase) db).refreshSubscriptions().observeOn(AndroidSchedulers.mainThread()).subscribe(() -> { + swipeRefreshLayout.setRefreshing(false); + }, (e) -> { + swipeRefreshLayout.setRefreshing(false); + Toast.makeText(getContext(), "Failed to refresh content", Toast.LENGTH_SHORT).show(); + }); + disposables.add(disposable); + }else{ + swipeRefreshLayout.setRefreshing(false); + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionService.java b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionService.java index 7e80264e613..5263ba35950 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionService.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionService.java @@ -6,7 +6,6 @@ import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.NewPipeDatabase; -import org.schabi.newpipe.database.AppDatabase; import org.schabi.newpipe.database.subscription.SubscriptionDAO; import org.schabi.newpipe.database.subscription.SubscriptionEntity; import org.schabi.newpipe.extractor.channel.ChannelInfo; @@ -55,13 +54,13 @@ public static SubscriptionService getInstance(@NonNull Context context) { private static final int SUBSCRIPTION_DEBOUNCE_INTERVAL = 500; private static final int SUBSCRIPTION_THREAD_POOL_SIZE = 4; - private final AppDatabase db; + private final Context context; private final Flowable> subscription; private final Scheduler subscriptionScheduler; private SubscriptionService(Context context) { - db = NewPipeDatabase.getInstance(context.getApplicationContext()); + this.context = context.getApplicationContext(); subscription = getSubscriptionInfos(); final Executor subscriptionExecutor = Executors.newFixedThreadPool(SUBSCRIPTION_THREAD_POOL_SIZE); @@ -110,7 +109,7 @@ public Maybe getChannelInfo(final SubscriptionEntity subscriptionEn * Returns the database access interface for subscription table. */ public SubscriptionDAO subscriptionTable() { - return db.subscriptionDAO(); + return NewPipeDatabase.getInstance(context).subscriptionDAO(); } public Completable updateChannelInfo(final ChannelInfo info) { diff --git a/app/src/main/java/org/schabi/newpipe/settings/AccountSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/AccountSettingsFragment.java new file mode 100644 index 00000000000..6d7779204ca --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/AccountSettingsFragment.java @@ -0,0 +1,100 @@ +package org.schabi.newpipe.settings; + +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v7.preference.EditTextPreference; +import android.support.v7.preference.Preference; +import android.widget.Toast; + +import org.schabi.newpipe.NewPipeDatabase; +import org.schabi.newpipe.R; +import org.schabi.newpipe.auth.AuthService; +import org.schabi.newpipe.database.RemoteDatabase; +import org.schabi.newpipe.util.NavigationHelper; + +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; + +public class AccountSettingsFragment extends BasePreferenceFragment { + + private CompositeDisposable disposables = new CompositeDisposable(); + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + public boolean onPreferenceTreeClick(Preference preference) { + return super.onPreferenceTreeClick(preference); + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + + addPreferencesFromResource(R.xml.account_settings); + + Context context = getContext().getApplicationContext(); + + EditTextPreference urlPreference = (EditTextPreference) findPreference(getString(R.string.sync_server_url_key)); + urlPreference.setSummary(urlPreference.getText()); + urlPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object o) { + preference.setSummary(o.toString()); + return true; + } + }); + + Preference usernamePreference = findPreference(getString(R.string.username_key)); + usernamePreference.setOnPreferenceClickListener((Preference p) -> { + if (!AuthService.getInstance(context).isLoggedIn()) { + NavigationHelper.openLoginFragment(getFragmentManager()); + } + return true; + }); + + Preference syncPreference = findPreference(getString(R.string.sync_key)); + syncPreference.setOnPreferenceClickListener((Preference p) -> { + Toast.makeText(context, "Sync started", Toast.LENGTH_SHORT).show(); + RemoteDatabase remoteDatabase = (RemoteDatabase) NewPipeDatabase.getInstance(context); + Disposable disposable = remoteDatabase.sync().observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> Toast.makeText(context, "Sync completed", Toast.LENGTH_SHORT).show(), + e -> Toast.makeText(context, "Sync failed", Toast.LENGTH_SHORT).show()); + disposables.add(disposable); + return true; + }); + + Preference logoutPreference = findPreference(getString(R.string.logout_key)); + logoutPreference.setOnPreferenceClickListener((Preference p) -> { + Disposable disposable = AuthService.getInstance(context).logout().observeOn(AndroidSchedulers.mainThread()).subscribe(() -> { + Toast.makeText(context, "Logged out", Toast.LENGTH_SHORT).show(); + // go to main activity + NavigationHelper.openMainActivity(getContext()); + }, e -> Toast.makeText(context, "Logout failed", Toast.LENGTH_SHORT).show()); + disposables.add(disposable); + return true; + }); + + if(AuthService.getInstance(context).isLoggedIn()){ + String username = AuthService.getInstance(context).getUsername(); + usernamePreference.setTitle(username); + usernamePreference.setSummary("Logged in as " + username); + urlPreference.setEnabled(false); + usernamePreference.setEnabled(false); + syncPreference.setEnabled(true); + logoutPreference.setEnabled(true); + } + + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (disposables != null) disposables.clear(); + disposables = null; + } + +} diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index 89c4b33fe28..32d70741822 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -32,6 +32,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.MainFragment; +import org.schabi.newpipe.fragments.auth.LoginFragment; import org.schabi.newpipe.fragments.detail.VideoDetailFragment; import org.schabi.newpipe.fragments.list.channel.ChannelFragment; import org.schabi.newpipe.fragments.list.comments.CommentsFragment; @@ -261,6 +262,14 @@ public static void openMainFragment(FragmentManager fragmentManager) { .commit(); } + public static void openLoginFragment(FragmentManager fragmentManager) { + + defaultTransaction(fragmentManager) + .replace(R.id.fragment_holder, new LoginFragment()) + .addToBackStack(null) + .commit(); + } + public static boolean tryGotoSearchFragment(FragmentManager fragmentManager) { if (MainActivity.DEBUG) { for (int i = 0; i < fragmentManager.getBackStackEntryCount(); i++) { diff --git a/app/src/main/res/drawable/ic_account_black_24dp.xml b/app/src/main/res/drawable/ic_account_black_24dp.xml new file mode 100755 index 00000000000..7563ca67b5c --- /dev/null +++ b/app/src/main/res/drawable/ic_account_black_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_account_white_24dp.xml b/app/src/main/res/drawable/ic_account_white_24dp.xml new file mode 100755 index 00000000000..7563ca67b5c --- /dev/null +++ b/app/src/main/res/drawable/ic_account_white_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/fragment_bookmarks.xml b/app/src/main/res/layout/fragment_bookmarks.xml index 56e13225ffd..f6bb950ecb4 100644 --- a/app/src/main/res/layout/fragment_bookmarks.xml +++ b/app/src/main/res/layout/fragment_bookmarks.xml @@ -6,6 +6,11 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_login.xml b/app/src/main/res/layout/fragment_login.xml new file mode 100644 index 00000000000..4082f44dd3e --- /dev/null +++ b/app/src/main/res/layout/fragment_login.xml @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +