Merge pull request #761 from nextcloud/dev

[WIP] - Dev
This commit is contained in:
David Luhmer 2019-06-05 20:07:41 +02:00 committed by GitHub
commit 1f4466332c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
70 changed files with 2077 additions and 1933 deletions

2
.gitignore vendored
View file

@ -37,3 +37,5 @@ build/*
*.ipr
*~
*.swp
News-Android-App/out.map
News-Android-App/extra/release/output.json

View file

@ -1,13 +1,13 @@
1.0 (in development)
---------------------
- Single Sign on for all Nextcloud Android Apps!
0.9.9.26
---------------------
- Fix - <a href="https://github.com/owncloud/News-Android-App/issues/726">#726 Add new feed fails</a>
- Fix - <a href="https://github.com/owncloud/News-Android-App/issues/744">#744 Fix issues when adding feeds (Thanks @Unpublished)</a>
- Feature - <a href="https://github.com/owncloud/News-Android-App/issues/747">#747 Add option to share article when using chrome-custom-tabs</a>
- Fix - Reset database when account is stored
- Fix - Workaround for app-crashes due to widget problems
- Feature - Support for Android Auto (Podcast playback)
- Feature - Use picture-in-picture mode for video podcasts
- Fix - Fix restarts of app due to a bug in android compat library (when using dark mode)
0.9.9.25

View file

@ -40,8 +40,8 @@ android {
buildTypes {
debug {
shrinkResources true
minifyEnabled true
shrinkResources false
minifyEnabled false
useProguard true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
testProguardFiles 'proguard-test.pro'
@ -131,26 +131,27 @@ dependencies {
// implementation 'com.google.android.gms:play-services:4.2.42'
//implementation project(':Android-SingleSignOn')
//implementation project(path: ':MaterialShowcaseView:library', configuration: 'default')
//implementation 'com.github.nextcloud:Android-SingleSignOn:e781a33e01'
implementation 'com.github.nextcloud:Android-SingleSignOn:unit-testing-SNAPSHOT'
implementation 'com.github.nextcloud:Android-SingleSignOn:fix-account-not-found-SNAPSHOT'
implementation 'com.github.David-Development:MaterialShowcaseView:bf6afa225d'
// https://mvnrepository.com/artifact/androidx.legacy/legacy-support-v4
implementation "androidx.legacy:legacy-support-v4:1.0.0"
implementation "androidx.core:core:1.1.0-alpha05"
implementation "androidx.appcompat:appcompat:1.1.0-alpha04"
implementation "androidx.preference:preference:1.1.0-alpha04"
implementation "androidx.core:core:1.2.0-alpha01"
implementation "androidx.appcompat:appcompat:1.1.0-alpha05"
implementation "androidx.preference:preference:1.1.0-alpha05"
// https://mvnrepository.com/artifact/com.google.android.material/material
implementation "com.google.android.material:material:1.1.0-alpha05"
implementation "com.google.android.material:material:1.1.0-alpha06"
//implementation "com.google.android.material:material:1.0.0"
implementation "androidx.palette:palette:1.0.0"
implementation "androidx.recyclerview:recyclerview:1.1.0-alpha04"
implementation "androidx.recyclerview:recyclerview:1.1.0-alpha05"
implementation "androidx.browser:browser:1.0.0"
implementation "androidx.cardview:cardview:1.0.0"
//implementation 'de.mrmaffen:holocircularprogressbar:1.0.1'
implementation 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
implementation 'com.google.code.gson:gson:2.8.5'
implementation 'com.jakewharton:butterknife:10.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
annotationProcessor 'com.jakewharton:butterknife-compiler:10.1.0'
implementation 'com.sothree.slidinguppanel:library:3.2.1'
@ -169,7 +170,6 @@ dependencies {
transitive = true
}
implementation "com.google.dagger:dagger:${DAGGER_VERSION}"
annotationProcessor "com.google.dagger:dagger-compiler:${DAGGER_VERSION}"
@ -186,7 +186,7 @@ dependencies {
implementation 'com.nbsp:library:1.02' // MaterialFilePicker
extraImplementation 'com.github.tommus:youtube-android-player-api:1.2.2'
//extraImplementation 'com.github.tommus:youtube-android-player-api:1.2.2'
testImplementation 'junit:junit:4.12'

View file

@ -3,11 +3,18 @@ package de.luhmer.owncloudnewsreader.di;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import androidx.test.InstrumentationRegistry;
import com.nextcloud.android.sso.AccountImporter;
import com.nextcloud.android.sso.model.SingleSignOnAccount;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import de.luhmer.owncloudnewsreader.NewsReaderListFragment;
import de.luhmer.owncloudnewsreader.SettingsActivity;
@ -17,6 +24,7 @@ import de.luhmer.owncloudnewsreader.ssl.MemorizingTrustManager;
public class TestApiModule extends ApiModule {
private static final String TAG = TestApiModule.class.getCanonicalName();
private Application application;
public static String DUMMY_ACCOUNT_AccountName = "test-account";
@ -81,9 +89,43 @@ public class TestApiModule extends ApiModule {
return application.getPackageName() + "_preferences_test";
}
@Override
public String providesDatabaseFileName() {
String filename = "OwncloudNewsReaderOrmTest.db";
try {
String dst = "/data/data/" + application.getApplicationContext().getPackageName() + "/databases/" + filename;
File dstFile = new File(dst);
dstFile.getParentFile().mkdirs();
// https://stackoverflow.com/a/35690692
copy(InstrumentationRegistry.getContext().getAssets().open("OwncloudNewsReaderOrm.db"), dstFile);
} catch (IOException e) {
Log.e(TAG, "Failed copying Test Database", e);
}
//return PreferenceManager.getDefaultSharedPreferencesName(mApplication);
return filename;
}
@Override
protected ApiProvider provideAPI(MemorizingTrustManager mtm, SharedPreferences sp) {
ApiProvider apiProvider = new TestApiProvider(mtm, sp, application);
return apiProvider;
}
public static void copy(InputStream in, File dst) throws IOException {
try (OutputStream out = new FileOutputStream(dst)) {
// Transfer bytes from in to out
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}
}
in.close();
}
}

View file

@ -82,17 +82,36 @@ public class TestApiProvider extends ApiProvider {
InputStream inputStream;
switch (request.getUrl()) {
case "/index.php/apps/news/api/v1-2/feeds":
inputStream = handleCreateFeed(request);
if("POST".equals(request.getMethod())) {
inputStream = handleCreateFeed(request);
} else {
inputStream = stringToInputStream("{\"feeds\": []}");
}
break;
case "/index.php/apps/news/api/v1-2/user":
inputStream = handleUser();
break;
case "/index.php/apps/news/api/v1-2/folders":
inputStream = handleFolders();
break;
case "/index.php/apps/news/api/v1-2/items":
inputStream = stringToInputStream("{\"items\": []}");
break;
//case "index.php/apps/news/api/v1-2/feeds":
case "/index.php/apps/news/api/v1-2/items/unread/multiple":
inputStream = stringToInputStream("");
break;
default:
Log.e(TAG, request.getUrl());
throw new Error("Not implemented yet!");
}
return inputStream;
}
private InputStream handleFolders() {
String folders = "{\"folders\":[{\"id\":2,\"name\":\"Comic\"},{\"id\":3,\"name\":\"Android\"}]}";
return stringToInputStream(folders);
}
// https://github.com/nextcloud/news/blob/master/docs/externalapi/Legacy.md#create-a-feed

View file

@ -0,0 +1,45 @@
package de.luhmer.owncloudnewsreader.helper;
import android.content.Context;
import android.content.SharedPreferences;
import de.luhmer.owncloudnewsreader.R;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
public class Utils {
public static void initMaterialShowCaseView(Context context) {
String PREFS_NAME = "material_showcaseview_prefs";
SharedPreferences sp = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
sp.edit()
.putInt("status_SWIPE_LEFT_RIGHT_AND_PTR", -1)
.putInt("status_LOGO_SYNC", -1)
.commit();
}
public static void clearFocus() {
sleep(200);
onView(withId(R.id.toolbar)).perform(click());
sleep(200);
}
public static void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void sleep(float seconds) {
try {
Thread.sleep((long) seconds * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

View file

@ -1,11 +1,12 @@
package de.luhmer.owncloudnewsreader.tests;
import org.junit.Rule;
import org.junit.runner.RunWith;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Rule;
import org.junit.runner.RunWith;
import de.luhmer.owncloudnewsreader.NewsReaderListActivity;
@RunWith(AndroidJUnit4.class)

View file

@ -1,6 +1,9 @@
package de.luhmer.owncloudnewsreader.tests;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
import com.nextcloud.android.sso.aidl.NextcloudRequest;
import org.junit.Before;
@ -12,8 +15,6 @@ import org.mockito.runners.MockitoJUnitRunner;
import javax.inject.Inject;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
import de.luhmer.owncloudnewsreader.NewFeedActivity;
import de.luhmer.owncloudnewsreader.R;
import de.luhmer.owncloudnewsreader.TestApplication;

View file

@ -3,25 +3,39 @@ package de.luhmer.owncloudnewsreader.tests;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.SystemClock;
import android.view.View;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.test.espresso.Espresso;
import androidx.test.espresso.contrib.RecyclerViewActions;
import androidx.test.espresso.matcher.BoundedMatcher;
import androidx.test.espresso.matcher.ViewMatchers;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
import androidx.test.rule.GrantPermissionRule;
import androidx.test.runner.AndroidJUnit4;
import com.nextcloud.android.sso.aidl.NextcloudRequest;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.stream.Collectors;
import javax.inject.Inject;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.test.espresso.Espresso;
import androidx.test.espresso.contrib.RecyclerViewActions;
import androidx.test.espresso.matcher.ViewMatchers;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
import de.luhmer.owncloudnewsreader.Constants;
import de.luhmer.owncloudnewsreader.NewsReaderDetailFragment;
import de.luhmer.owncloudnewsreader.NewsReaderListActivity;
@ -29,22 +43,39 @@ import de.luhmer.owncloudnewsreader.R;
import de.luhmer.owncloudnewsreader.TestApplication;
import de.luhmer.owncloudnewsreader.adapter.NewsListRecyclerAdapter;
import de.luhmer.owncloudnewsreader.adapter.ViewHolder;
import de.luhmer.owncloudnewsreader.di.ApiProvider;
import de.luhmer.owncloudnewsreader.di.TestApiProvider;
import de.luhmer.owncloudnewsreader.di.TestComponent;
import helper.OrientationChangeAction;
import helper.RecyclerViewAssertions;
import static androidx.core.util.Preconditions.checkNotNull;
import static androidx.test.InstrumentationRegistry.getInstrumentation;
import static androidx.test.InstrumentationRegistry.registerInstance;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.typeText;
import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
import static androidx.test.espresso.matcher.ViewMatchers.withClassName;
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static de.luhmer.owncloudnewsreader.helper.Utils.clearFocus;
import static de.luhmer.owncloudnewsreader.helper.Utils.initMaterialShowCaseView;
import static de.luhmer.owncloudnewsreader.helper.Utils.sleep;
import static junit.framework.TestCase.assertNotNull;
import static junit.framework.TestCase.assertTrue;
import static junit.framework.TestCase.fail;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@RunWith(AndroidJUnit4.class)
@LargeTest
@ -55,7 +86,11 @@ public class NewsReaderListActivityUiTests {
@Rule
public ActivityTestRule<NewsReaderListActivity> mActivityRule = new ActivityTestRule<>(NewsReaderListActivity.class);
@Rule
public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
protected @Inject SharedPreferences mPrefs;
protected @Inject ApiProvider mApi;
private NewsReaderListActivity getActivity() {
return mActivityRule.getActivity();
@ -68,6 +103,10 @@ public class NewsReaderListActivityUiTests {
TestComponent ac = (TestComponent) ((TestApplication)(getActivity().getApplication())).getAppComponent();
ac.inject(this);
clearFocus();
initMaterialShowCaseView(getActivity());
}
@Test
@ -80,13 +119,21 @@ public class NewsReaderListActivityUiTests {
onView(isRoot()).perform(OrientationChangeAction.orientationLandscape(getActivity()));
//onView(isRoot()).perform(OrientationChangeAction.orientationPortrait(getActivity()));
sleep(1.0f);
sleep(2000);
LinearLayoutManager llm = (LinearLayoutManager) ndf.getRecyclerView().getLayoutManager();
onView(withId(R.id.list)).check(new RecyclerViewAssertions(scrollPosition-(scrollPosition-llm.findFirstVisibleItemPosition())));
int expectedPosition = scrollPosition-(scrollPosition-llm.findFirstVisibleItemPosition());
// As there is a little offset when rotating.. we need to add one here..
onView(withId(R.id.list)).check(new RecyclerViewAssertions(expectedPosition+1));
onView(withId(R.id.tv_no_items_available)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)));
//onView(isRoot()).perform(OrientationChangeAction.orientationLandscape(getActivity()));
sleep(2000);
onView(isRoot()).perform(OrientationChangeAction.orientationPortrait(getActivity()));
onView(withId(R.id.list)).check(new RecyclerViewAssertions(expectedPosition));
onView(withId(R.id.tv_no_items_available)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)));
}
@Test
@ -95,7 +142,7 @@ public class NewsReaderListActivityUiTests {
onView(withId(R.id.list)).perform(RecyclerViewActions.actionOnItemAtPosition(scrollPosition, click()));
sleep(2);
sleep(2000);
Espresso.pressBack();
@ -110,22 +157,122 @@ public class NewsReaderListActivityUiTests {
getActivity().runOnUiThread(() -> na.changeReadStateOfItem(vh, false));
sleep(1.0f);
onView(withId(R.id.list)).check(new RecyclerViewAssertions(scrollPosition-(scrollPosition-llm.findFirstVisibleItemPosition())));
int expectedPosition = scrollPosition-(scrollPosition-llm.findFirstVisibleItemPosition());
onView(withId(R.id.list)).check(new RecyclerViewAssertions(expectedPosition));
onView(withId(R.id.tv_no_items_available)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)));
}
@Test
public void testSyncFinishedRefreshRecycler_sameActivity() {
syncResultTest(true);
assertTrue(syncResultTest(true));
}
@Test
public void testSyncFinishedSnackbar_sameActivity() {
syncResultTest(false);
assertTrue(syncResultTest(false));
}
private void syncResultTest(boolean testFirstPosition) {
@Test
public void searchTest() {
String firstItem = "Immer wieder sonntags KW 19";
// String firstItem = "These are the best screen protectors for the Huawei P30 Pro";
// Check first item
checkRecyclerViewFirstItemText(firstItem);
// Open search menu
onView(allOf(withId(R.id.menu_search), withContentDescription(getString(R.string.action_search)), isDisplayed())).perform(click());
// Type in "test" into searchbar
onView(allOf(withClassName(is("android.widget.SearchView$SearchAutoComplete")), isDisplayed())).perform(typeText("test"));
sleep(1000);
checkRecyclerViewFirstItemText("VR ohne Kabel: Die Oculus Quest im Test, definitiv der richtige Ansatz");
// checkRecyclerViewFirstItemText("Testfahrt im Mercedes E 300 de mit 90-kW-Elektromotor und Vierzylinder-Diesel");
// Close search bar
onView(withContentDescription("Collapse")).perform(click());
sleep(1000);
// Test if search reset was successful
checkRecyclerViewFirstItemText(firstItem);
}
@Test
public void syncTest() {
// Open navigation drawer
onView(allOf(withContentDescription(getString(R.string.news_list_drawer_text)), isDisplayed())).perform(click());
sleep(1500);
/*
// Click on Got it
onView(allOf(withText("GOT IT"), isDisplayed())).perform(click());
sleep(1000);
*/
// Trigger refresh
onView(allOf(withContentDescription(getString(R.string.content_desc_tap_to_refresh)), isDisplayed())).perform(click());
sleep(1000);
try {
verifySyncRequested();
} catch (Exception e) {
fail(e.getMessage());
}
}
// Verify that the API was actually called
private void verifySyncRequested() throws Exception {
TestApiProvider.NewsTestNetworkRequest nr = ((TestApiProvider)mApi).networkRequestSpy;
ArgumentCaptor<NextcloudRequest> argument = ArgumentCaptor.forClass(NextcloudRequest.class);
verify(nr, times(6)).performNetworkRequest(argument.capture(), any());
List<String> requestedUrls = argument.getAllValues().stream().map(nextcloudRequest -> nextcloudRequest.getUrl()).collect(Collectors.toList());
assertTrue(requestedUrls.contains("/index.php/apps/news/api/v1-2/folders"));
assertTrue(requestedUrls.contains("/index.php/apps/news/api/v1-2/feeds"));
assertTrue(requestedUrls.contains("/index.php/apps/news/api/v1-2/items/unread/multiple"));
assertTrue(requestedUrls.contains("/index.php/apps/news/api/v1-2/items")); // TODO Double check why /items is called twice... ?
assertTrue(requestedUrls.contains("/index.php/apps/news/api/v1-2/user"));
}
private void checkRecyclerViewFirstItemText(String text) {
onView(withId(R.id.list)).check(matches(atPosition(0, hasDescendant(withText(text)))));
}
private String getString(@IdRes int resId) {
return mActivityRule.getActivity().getString(resId);
}
public static Matcher<View> atPosition(final int position, @NonNull final Matcher<View> itemMatcher) {
checkNotNull(itemMatcher);
return new BoundedMatcher<View, RecyclerView>(RecyclerView.class) {
@Override
public void describeTo(Description description) {
description.appendText("has item at position " + position + ": ");
itemMatcher.describeTo(description);
}
@Override
protected boolean matchesSafely(final RecyclerView view) {
RecyclerView.ViewHolder viewHolder = view.findViewHolderForAdapterPosition(position);
if (viewHolder == null) {
// has no item on such position
return false;
}
return itemMatcher.matches(viewHolder.itemView);
}
};
}
private boolean syncResultTest(boolean testFirstPosition) {
if(!testFirstPosition) {
onView(withId(R.id.list)).perform(RecyclerViewActions.scrollToPosition(scrollPosition));
}
@ -163,17 +310,9 @@ public class NewsReaderListActivityUiTests {
e.printStackTrace();
fail(e.getMessage());
}
return true;
}
private void sleep(float seconds) {
try {
Thread.sleep((long) seconds * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private Fragment waitForFragment(int id, int timeout) {
long endTime = SystemClock.uptimeMillis() + timeout;
while (SystemClock.uptimeMillis() <= endTime) {

View file

@ -4,6 +4,14 @@ import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.recyclerview.widget.RecyclerView;
import androidx.test.InstrumentationRegistry;
import androidx.test.espresso.contrib.RecyclerViewActions;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
import androidx.test.rule.GrantPermissionRule;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@ -15,12 +23,6 @@ import java.lang.reflect.Method;
import javax.inject.Inject;
import androidx.recyclerview.widget.RecyclerView;
import androidx.test.InstrumentationRegistry;
import androidx.test.espresso.contrib.RecyclerViewActions;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
import de.luhmer.owncloudnewsreader.NewsReaderListActivity;
import de.luhmer.owncloudnewsreader.R;
import de.luhmer.owncloudnewsreader.TestApplication;
@ -55,6 +57,9 @@ public class NightModeTest {
public ActivityTestRule<NewsReaderListActivity> mActivityRule = new ActivityTestRule<>(NewsReaderListActivity.class);
//public ActivityTestRule<NewsReaderListActivity> mActivityRule = new ActivityTestRule<>(NewsReaderListActivity.class, true, false);
@Rule
public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
private Activity getActivity() {
return mActivityRule.getActivity();
}

View file

@ -13,14 +13,14 @@ import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.core.content.ContextCompat;
import androidx.test.espresso.matcher.BoundedMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeDiagnosingMatcher;
import org.hamcrest.TypeSafeMatcher;
import androidx.core.content.ContextCompat;
import androidx.test.espresso.matcher.BoundedMatcher;
public class CustomMatchers {
private static final String TAG = CustomMatchers.class.getCanonicalName();

View file

@ -29,15 +29,15 @@ import android.app.Activity;
import android.content.pm.ActivityInfo;
import android.view.View;
import org.hamcrest.Matcher;
import java.util.Collection;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry;
import androidx.test.runner.lifecycle.Stage;
import org.hamcrest.Matcher;
import java.util.Collection;
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
/**

View file

@ -1,14 +1,17 @@
package screengrab;
import androidx.core.view.GravityCompat;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
import androidx.test.rule.GrantPermissionRule;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import androidx.core.view.GravityCompat;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
import de.luhmer.owncloudnewsreader.NewsReaderDetailFragment;
import de.luhmer.owncloudnewsreader.NewsReaderListActivity;
import de.luhmer.owncloudnewsreader.NewsReaderListFragment;
@ -17,12 +20,17 @@ import de.luhmer.owncloudnewsreader.adapter.ViewHolder;
import de.luhmer.owncloudnewsreader.database.DatabaseConnectionOrm;
import de.luhmer.owncloudnewsreader.model.PodcastItem;
import tools.fastlane.screengrab.Screengrab;
import tools.fastlane.screengrab.UiAutomatorScreenshotStrategy;
import tools.fastlane.screengrab.locale.LocaleTestRule;
import static de.luhmer.owncloudnewsreader.helper.Utils.clearFocus;
import static de.luhmer.owncloudnewsreader.helper.Utils.initMaterialShowCaseView;
/**
* Created by David on 06.03.2016.
*/
@RunWith(AndroidJUnit4.class)
@RunWith(JUnit4.class)
@LargeTest
public class ScreenshotTest {
@ClassRule
@ -31,19 +39,28 @@ public class ScreenshotTest {
@Rule
public ActivityTestRule<NewsReaderListActivity> mActivityRule = new ActivityTestRule<>(NewsReaderListActivity.class);
@Rule
public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION, android.Manifest.permission.WRITE_EXTERNAL_STORAGE);
private NewsReaderListActivity activity;
private NewsReaderListActivity mActivity;
private NewsReaderListFragment nrlf;
private NewsReaderDetailFragment nrdf;
private int itemPos = 0;
private int podcastGroupPosition = 3;
//private int podcastGroupPosition = 3;
@Before
public void setUp() {
activity = mActivityRule.getActivity();
nrlf = mActivityRule.getActivity().getSlidingListFragment();
nrdf = mActivityRule.getActivity().getNewsReaderDetailFragment();
Screengrab.setDefaultScreenshotStrategy(new UiAutomatorScreenshotStrategy());
mActivity = mActivityRule.getActivity();
nrlf = mActivity.getSlidingListFragment();
nrdf = mActivity.getNewsReaderDetailFragment();
clearFocus();
initMaterialShowCaseView(mActivity);
}
@ -52,9 +69,9 @@ public class ScreenshotTest {
public void testTakeScreenshots() {
Screengrab.screenshot("startup");
activity.runOnUiThread(() -> {
mActivity.runOnUiThread(() -> {
openDrawer();
nrlf.getListView().expandGroup(podcastGroupPosition);
//nrlf.getListView().expandGroup(podcastGroupPosition);
});
try {
@ -66,7 +83,7 @@ public class ScreenshotTest {
Screengrab.screenshot("slider_open");
activity.runOnUiThread(() -> {
mActivity.runOnUiThread(() -> {
closeDrawer();
try {
@ -75,7 +92,7 @@ public class ScreenshotTest {
e.printStackTrace();
}
activity.onClick(null, itemPos); //Select item
mActivity.onClick(null, itemPos); //Select item
});
@ -87,7 +104,7 @@ public class ScreenshotTest {
Screengrab.screenshot("detail_activity");
activity.runOnUiThread(() -> {
mActivity.runOnUiThread(() -> {
NewsListRecyclerAdapter na = (NewsListRecyclerAdapter) nrdf.getRecyclerView().getAdapter();
ViewHolder vh = (ViewHolder) nrdf.getRecyclerView().getChildViewHolder(nrdf.getRecyclerView().getLayoutManager().findViewByPosition(itemPos));
na.changeReadStateOfItem(vh, false);
@ -98,11 +115,12 @@ public class ScreenshotTest {
@Test
public void testPodcast() {
activity.runOnUiThread(() -> {
public void testAudioPodcast() {
mActivity.runOnUiThread(() -> {
openDrawer();
nrlf.getListView().expandGroup(podcastGroupPosition);
openFeed(podcastGroupPosition, 0);
//nrlf.getListView().expandGroup(podcastGroupPosition);
//openFeed(podcastGroupPosition, 0);
openFeed(2, 1); // Open Android Podcast
});
try {
@ -111,17 +129,17 @@ public class ScreenshotTest {
e.printStackTrace();
}
Screengrab.screenshot("podcast_list");
//Screengrab.screenshot("podcast_list");
activity.runOnUiThread(() -> {
mActivity.runOnUiThread(() -> {
ViewHolder vh = (ViewHolder) nrdf.getRecyclerView().getChildViewHolder(nrdf.getRecyclerView().getLayoutManager().findViewByPosition(0));
PodcastItem podcastItem = DatabaseConnectionOrm.ParsePodcastItemFromRssItem(activity, vh.getRssItem());
activity.openMediaItem(podcastItem);
PodcastItem podcastItem = DatabaseConnectionOrm.ParsePodcastItemFromRssItem(mActivity, vh.getRssItem());
mActivity.openMediaItem(podcastItem);
});
try {
Thread.sleep(5000);
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
@ -129,7 +147,7 @@ public class ScreenshotTest {
Screengrab.screenshot("podcast_running");
activity.runOnUiThread(() -> activity.pausePodcast());
mActivity.runOnUiThread(() -> mActivity.pausePodcast());
try {
@ -143,12 +161,13 @@ public class ScreenshotTest {
@Test
public void testVideoPodcast() {
activity.runOnUiThread(() -> {
mActivity.runOnUiThread(() -> {
//Set url to mock
nrlf.bindUserInfoToUI();
openDrawer();
openFeed(0, 13); //Click on ARD Podcast
//openFeed(0, 13); //Click on ARD Podcast
openFeed(7, -1);
});
try {
@ -157,23 +176,23 @@ public class ScreenshotTest {
e.printStackTrace();
}
activity.runOnUiThread(() -> {
mActivity.runOnUiThread(() -> {
ViewHolder vh = (ViewHolder) nrdf.getRecyclerView().getChildViewHolder(nrdf.getRecyclerView().getLayoutManager().findViewByPosition(1));
PodcastItem podcastItem = DatabaseConnectionOrm.ParsePodcastItemFromRssItem(activity, vh.getRssItem());
activity.openMediaItem(podcastItem);
PodcastItem podcastItem = DatabaseConnectionOrm.ParsePodcastItemFromRssItem(mActivity, vh.getRssItem());
mActivity.openMediaItem(podcastItem);
});
try {
Thread.sleep(5000);
Thread.sleep(15000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Screengrab.screenshot("video_podcast_running");
activity.runOnUiThread(() -> activity.pausePodcast());
mActivity.runOnUiThread(() -> mActivity.pausePodcast());
try {
@ -188,14 +207,14 @@ public class ScreenshotTest {
}
private void openDrawer() {
if(activity.drawerLayout != null) {
activity.drawerLayout.openDrawer(GravityCompat.START, true);
if(mActivity.drawerLayout != null) {
mActivity.drawerLayout.openDrawer(GravityCompat.START, true);
}
}
private void closeDrawer() {
if(activity.drawerLayout != null) {
activity.drawerLayout.closeDrawer(GravityCompat.START, true);
if(mActivity.drawerLayout != null) {
mActivity.drawerLayout.closeDrawer(GravityCompat.START, true);
}
}
}

View file

@ -1,34 +0,0 @@
package de.luhmer.owncloudnewsreader;
import android.app.Activity;
import android.app.FragmentTransaction;
import com.google.android.youtube.player.YouTubeInitializationResult;
import com.google.android.youtube.player.YouTubePlayer;
import com.google.android.youtube.player.YouTubePlayerFragment;
import org.greenrobot.eventbus.EventBus;
import de.luhmer.owncloudnewsreader.events.podcast.RegisterYoutubeOutput;
public class YoutubePlayerManager {
public static void StartYoutubePlayer(final Activity activity, int YOUTUBE_CONTENT_VIEW_ID, final EventBus eventBus, final Runnable onInitSuccess) {
YouTubePlayerFragment youTubePlayerFragment = YouTubePlayerFragment.newInstance();
FragmentTransaction ft = activity.getFragmentManager().beginTransaction();
ft.add(YOUTUBE_CONTENT_VIEW_ID, youTubePlayerFragment).commit();
youTubePlayerFragment.initialize("AIzaSyA2OHKWvF_hRVtPmLcwnO8yF6-iah2hjbk", new YouTubePlayer.OnInitializedListener() {
@Override
public void onInitializationSuccess(YouTubePlayer.Provider provider, YouTubePlayer youTubePlayer, boolean wasRestored) {
eventBus.post(new RegisterYoutubeOutput(youTubePlayer, wasRestored));
onInitSuccess.run();
}
@Override
public void onInitializationFailure(YouTubePlayer.Provider provider, YouTubeInitializationResult youTubeInitializationResult) {
youTubeInitializationResult.getErrorDialog(activity, 0).show();
//Toast.makeText(activity, "Error while playing youtube video! (InitializationFailure)", Toast.LENGTH_LONG).show();
}
});
}
}

View file

@ -1,168 +0,0 @@
package de.luhmer.owncloudnewsreader.services.podcast;
import android.content.Context;
import android.util.Log;
import android.widget.Toast;
import com.google.android.youtube.player.YouTubePlayer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import de.luhmer.owncloudnewsreader.model.MediaItem;
/**
* Created by david on 31.01.17.
*/
public class YoutubePlaybackService extends PlaybackService {
private static final String TAG = YoutubePlaybackService.class.getCanonicalName();
YouTubePlayer youTubePlayer;
Context context;
public YoutubePlaybackService(Context context, PodcastStatusListener podcastStatusListener, MediaItem mediaItem) {
super(podcastStatusListener, mediaItem);
this.context = context;
setStatus(Status.PREPARING);
}
@Override
public void destroy() {
if(youTubePlayer != null) {
youTubePlayer.pause();
youTubePlayer = null;
}
}
@Override
public void play() {
if(youTubePlayer != null) {
youTubePlayer.play();
}
}
@Override
public void pause() {
if(youTubePlayer != null) {
youTubePlayer.pause();
}
}
@Override
public void playbackSpeedChanged(float currentPlaybackSpeed) {
}
public void seekTo(double percent) {
if(youTubePlayer != null) {
double totalDuration = getTotalDuration();
int position = (int) ((totalDuration / 100d) * percent);
youTubePlayer.seekToMillis(position);
}
}
public int getCurrentDuration() {
if(youTubePlayer != null) {
return youTubePlayer.getCurrentTimeMillis();
}
return 0;
}
public int getTotalDuration() {
if(youTubePlayer != null) {
return youTubePlayer.getDurationMillis();
}
return 0;
}
@Override
public VideoType getVideoType() {
return VideoType.YouTube;
}
public void setYoutubePlayer(Object youTubePlayer, boolean wasRestored) {
this.youTubePlayer = (YouTubePlayer) youTubePlayer;
this.youTubePlayer.setPlaybackEventListener(youtubePlaybackEventListener);
this.youTubePlayer.setPlayerStateChangeListener(youtubePlayerStateChangeListener);
this.youTubePlayer.setPlayerStyle(YouTubePlayer.PlayerStyle.MINIMAL);
// Start buffering
if (!wasRestored) {
Pattern youtubeIdPattern = Pattern.compile(".*?v=([^&]*)");
Matcher matcher = youtubeIdPattern.matcher(getMediaItem().link);
if(matcher.matches()) {
String youtubeId = matcher.group(1);
this.youTubePlayer.cueVideo(youtubeId);
} else {
Toast.makeText(context, "Cannot find youtube video id", Toast.LENGTH_LONG).show();
setStatus(Status.FAILED);
}
}
}
YouTubePlayer.PlayerStateChangeListener youtubePlayerStateChangeListener = new YouTubePlayer.PlayerStateChangeListener() {
@Override
public void onLoading() {
Log.d(TAG, "onLoading() called");
}
@Override
public void onLoaded(String s) {
Log.d(TAG, "onLoaded() called with: s = [" + s + "]");
youTubePlayer.play();
}
@Override
public void onAdStarted() {
Log.d(TAG, "onAdStarted() called");
}
@Override
public void onVideoStarted() {
Log.d(TAG, "onVideoStarted() called");
}
@Override
public void onVideoEnded() {
Log.d(TAG, "onVideoEnded() called");
}
@Override
public void onError(YouTubePlayer.ErrorReason errorReason) {
Log.d(TAG, "onError() called with: errorReason = [" + errorReason + "]");
}
};
YouTubePlayer.PlaybackEventListener youtubePlaybackEventListener = new YouTubePlayer.PlaybackEventListener() {
@Override
public void onPlaying() {
Log.d(TAG, "onPlaying() called");
setStatus(Status.PLAYING);
}
@Override
public void onPaused() {
Log.d(TAG, "onPaused() called");
setStatus(Status.PAUSED);
}
@Override
public void onStopped() {
Log.d(TAG, "onStopped() called");
setStatus(Status.PAUSED);
}
@Override
public void onBuffering(boolean b) {
Log.d(TAG, "onBuffering() called with: b = [" + b + "]");
}
@Override
public void onSeekTo(int i) {
Log.d(TAG, "onSeekTo() called with: i = [" + i + "]");
}
};
}

View file

@ -3,8 +3,8 @@
xmlns:tools="http://schemas.android.com/tools"
package="de.luhmer.owncloudnewsreader"
android:installLocation="internalOnly"
android:versionCode="141"
android:versionName="0.9.9.25">
android:versionCode="142"
android:versionName="0.9.9.26">
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.INTERNET" />
@ -14,12 +14,14 @@
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
<!-- <uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL" /> -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- <uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" /> -->
<!-- Required for TwilightManager -->
<!-- <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<application
android:name=".NewsReaderApplication"
android:allowBackup="true"
@ -30,21 +32,42 @@
android:theme="@style/SplashTheme"
android:usesCleartextTraffic="true"
tools:replace="android:icon, android:label, android:theme, android:name">
<meta-data android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc"/>
<meta-data android:name="com.google.android.gms.car.notification.SmallIcon"
android:resource="@drawable/ic_notification" />
<activity
android:name=".NewsReaderListActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="@string/app_name"
android:launchMode="singleTop">
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Use this intent filter to get voice searches -->
<intent-filter>
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity> <!-- android:configChanges="keyboardHidden|orientation|screenSize" -->
<activity
android:name=".NewsDetailActivity"
android:label="@string/title_activity_news_detail" />
<!-- android:configChanges="keyboardHidden|orientation|screenSize" -->
<activity android:name=".PiPVideoPlaybackActivity"
android:resizeableActivity="true"
android:supportsPictureInPicture="true"
android:launchMode="singleTask"
android:taskAffinity="de.luhmer.owncloudnewsreader.pip"
android:autoRemoveFromRecents="true"
android:excludeFromRecents="true"
android:exported="false"
tools:targetApi="n"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" />
<activity
android:name=".SettingsActivity"
android:configChanges="keyboardHidden|orientation|screenSize"

View file

@ -45,9 +45,13 @@ import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import com.google.android.material.textfield.TextInputLayout;
import com.nextcloud.android.sso.AccountImporter;
import com.nextcloud.android.sso.api.NextcloudAPI;
import com.nextcloud.android.sso.exceptions.AndroidGetAccountsPermissionNotGranted;
import com.nextcloud.android.sso.exceptions.NextcloudFilesAppNotInstalledException;
import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
import com.nextcloud.android.sso.helper.SingleAccountHelper;
@ -60,8 +64,6 @@ import java.net.URL;
import javax.inject.Inject;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
@ -190,6 +192,8 @@ public class LoginDialogActivity extends AppCompatActivity {
AccountImporter.pickNewAccount(LoginDialogActivity.this);
} catch (NextcloudFilesAppNotInstalledException e) {
UiExceptionManager.showDialogForException(LoginDialogActivity.this, e);
} catch (AndroidGetAccountsPermissionNotGranted e) {
AccountImporter.requestAndroidAccountPermissionsAndPickAccount(this);
}
}
@ -252,6 +256,8 @@ public class LoginDialogActivity extends AppCompatActivity {
editor.putBoolean(SettingsActivity.SW_USE_SINGLE_SIGN_ON, true);
editor.commit();
resetDatabase();
SingleAccountHelper.setCurrentAccount(this, importedAccount.name);
mApi.initApi(new NextcloudAPI.ApiConnectedListener() {
@ -337,6 +343,8 @@ public class LoginDialogActivity extends AppCompatActivity {
editor.putBoolean(SettingsActivity.SW_USE_SINGLE_SIGN_ON, false);
editor.commit();
resetDatabase();
final ProgressDialog dialogLogin = buildPendingDialogWhileLoggingIn();
dialogLogin.show();
@ -357,7 +365,13 @@ public class LoginDialogActivity extends AppCompatActivity {
}
}
private void finishLogin(final ProgressDialog dialogLogin) {
private void resetDatabase() {
//Reset Database
DatabaseConnectionOrm dbConn = new DatabaseConnectionOrm(LoginDialogActivity.this);
dbConn.resetDatabase();
}
private void finishLogin(final ProgressDialog dialogLogin) {
mApi.getAPI().version()
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
@ -409,10 +423,6 @@ public class LoginDialogActivity extends AppCompatActivity {
Log.v(TAG, "onComplete() called");
if(loginSuccessful) {
//Reset Database
DatabaseConnectionOrm dbConn = new DatabaseConnectionOrm(LoginDialogActivity.this);
dbConn.resetDatabase();
Intent returnIntent = new Intent();
setResult(RESULT_OK, returnIntent);
@ -443,12 +453,16 @@ public class LoginDialogActivity extends AppCompatActivity {
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
AccountImporter.onActivityResult(requestCode, resultCode, data, LoginDialogActivity.this, new AccountImporter.IAccountAccessGranted() {
@Override
public void accountAccessGranted(SingleSignOnAccount account) {
LoginDialogActivity.this.importedAccount = account;
loginSingleSignOn();
}
AccountImporter.onActivityResult(requestCode, resultCode, data, LoginDialogActivity.this, account -> {
LoginDialogActivity.this.importedAccount = account;
loginSingleSignOn();
});
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
AccountImporter.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
}
}

View file

@ -27,11 +27,6 @@ import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Bundle;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.core.content.ContextCompat;
import androidx.appcompat.widget.Toolbar;
import android.text.Html;
import android.util.Log;
import android.util.SparseArray;
@ -41,6 +36,16 @@ import android.view.MenuItem;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import androidx.appcompat.widget.Toolbar;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
import java.lang.ref.WeakReference;
import java.util.HashSet;
import java.util.List;
@ -48,12 +53,6 @@ import java.util.Set;
import javax.inject.Inject;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
import butterknife.BindView;
import butterknife.ButterKnife;
import de.luhmer.owncloudnewsreader.database.DatabaseConnectionOrm;
@ -320,7 +319,7 @@ public class NewsDetailActivity extends PodcastFragmentActivity {
menuItem_Read.setIcon(R.drawable.ic_check_box_outline_blank_white);
menuItem_Read.setChecked(false);
}
}
}
@Override
@ -377,17 +376,12 @@ public class NewsDetailActivity extends PodcastFragmentActivity {
mPostDelayHandler.delayTimer();
break;
case R.id.action_starred:
Boolean curState = rssItem.getStarred_temp();
rssItem.setStarred_temp(!curState);
dbConn.updateRssItem(rssItem);
updateActionBarIcons();
case R.id.action_starred:
toggleRssItemStarredState();
break;
mPostDelayHandler.delayTimer();
break;
case R.id.action_openInBrowser:
case R.id.action_openInBrowser:
NewsDetailFragment newsDetailFragment = getNewsDetailFragmentAtPosition(currentPosition);
String link = "about:blank";
@ -481,6 +475,17 @@ public class NewsDetailActivity extends PodcastFragmentActivity {
return super.onOptionsItemSelected(item);
}
public void toggleRssItemStarredState() {
RssItem rssItem = rssItems.get(currentPosition);
Boolean curState = rssItem.getStarred_temp();
rssItem.setStarred_temp(!curState);
dbConn.updateRssItem(rssItem);
updateActionBarIcons();
mPostDelayHandler.delayTimer();
}
private boolean isChromeDefaultBrowser() {
Intent browserIntent = new Intent("android.intent.action.VIEW", Uri.parse("http://"));
ResolveInfo resolveInfo = getPackageManager().resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY);

View file

@ -21,15 +21,16 @@
package de.luhmer.owncloudnewsreader;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.ContextMenu;
import android.view.GestureDetector;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
@ -43,6 +44,13 @@ import android.webkit.WebViewClient;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.select.Elements;
@ -55,12 +63,6 @@ import java.util.Map;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
import butterknife.BindView;
import butterknife.ButterKnife;
import de.luhmer.owncloudnewsreader.adapter.ProgressBarWebChromeClient;
@ -87,7 +89,7 @@ public class NewsDetailFragment extends Fragment implements RssItemToHtmlTask.Li
private int section_number;
protected String html;
boolean changedUrl = false;
private GestureDetector mGestureDetector;
public NewsDetailFragment() { }
@ -180,6 +182,8 @@ public class NewsDetailFragment extends Fragment implements RssItemToHtmlTask.Li
mProgressBarLoading.setVisibility(View.GONE);
}
setUpGestureDetector();
return rootView;
}
@ -188,6 +192,38 @@ public class NewsDetailFragment extends Fragment implements RssItemToHtmlTask.Li
mWebView.saveState(outState);
}
private void setUpGestureDetector() {
mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener());
mGestureDetector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener()
{
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
return false;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
Log.v(TAG, "onDoubleTap() called with: e = [" + e + "]");
NewsDetailActivity ndActivity = ((NewsDetailActivity)getActivity());
if(ndActivity != null) {
((NewsDetailActivity) getActivity()).toggleRssItemStarredState();
// Star has 5 corners. So we can rotate it by 2/5
View view = getActivity().findViewById(R.id.action_starred);
ObjectAnimator animator = ObjectAnimator.ofFloat(view, "rotation", view.getRotation() + (2*(360f/5f)));
animator.start();
}
return false;
}
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
return false;
}
});
}
private void startLoadRssItemToWebViewTask() {
Log.d(TAG, "startLoadRssItemToWebViewTask() called");
mWebView.setVisibility(View.GONE);
@ -319,16 +355,16 @@ public class NewsDetailFragment extends Fragment implements RssItemToHtmlTask.Li
});
mWebView.setOnTouchListener(new View.OnTouchListener() {
mWebView.setOnTouchListener((v, event) -> {
mGestureDetector.onTouchEvent(event);
@Override
public boolean onTouch(View v, MotionEvent event) {
if (v.getId() == R.id.webview && event.getAction() == MotionEvent.ACTION_DOWN) {
changedUrl = true;
}
return false;
/*
if (v.getId() == R.id.webview && event.getAction() == MotionEvent.ACTION_DOWN) {
changedUrl = true;
}
*/
return false;
});
}

View file

@ -31,13 +31,6 @@ import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcelable;
import androidx.core.content.ContextCompat;
import androidx.core.view.GestureDetectorCompat;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.ItemTouchHelper;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
@ -48,13 +41,19 @@ import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.Toast;
import androidx.core.content.ContextCompat;
import androidx.core.view.GestureDetectorCompat;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import java.util.List;
import javax.inject.Inject;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import butterknife.BindView;
import butterknife.ButterKnife;
import de.luhmer.owncloudnewsreader.adapter.NewsListRecyclerAdapter;
@ -64,6 +63,7 @@ import de.luhmer.owncloudnewsreader.database.DatabaseConnectionOrm.SORT_DIRECTIO
import de.luhmer.owncloudnewsreader.database.model.RssItem;
import de.luhmer.owncloudnewsreader.database.model.RssItemDao;
import de.luhmer.owncloudnewsreader.helper.AsyncTaskHelper;
import de.luhmer.owncloudnewsreader.helper.PostDelayHandler;
import de.luhmer.owncloudnewsreader.helper.Search;
import de.luhmer.owncloudnewsreader.helper.StopWatch;
import io.reactivex.observers.DisposableObserver;
@ -107,8 +107,30 @@ public class NewsReaderDetailFragment extends Fragment {
private RecyclerView.OnItemTouchListener itemTouchListener;
protected @Inject SharedPreferences mPrefs;
protected @Inject PostDelayHandler mPostDelayHandler;
protected DisposableObserver<List<RssItem>> SearchResultObserver = new DisposableObserver<List<RssItem>>() {
private PodcastFragmentActivity mActivity;
/**
* Mandatory empty constructor for the fragment manager to instantiate the
* fragment (e.g. upon screen orientation changes).
*/
public NewsReaderDetailFragment() {
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
this.mActivity = (PodcastFragmentActivity) context;
}
@Override
public void onDetach() {
this.mActivity = null;
super.onDetach();
}
protected DisposableObserver<List<RssItem>> searchResultObserver = new DisposableObserver<List<RssItem>>() {
@Override
public void onNext(List<RssItem> rssItems) {
loadRssItemsIntoView(rssItems);
@ -117,7 +139,7 @@ public class NewsReaderDetailFragment extends Fragment {
@Override
public void onError(Throwable e) {
pbLoading.setVisibility(View.GONE);
Toast.makeText(getActivity(), e.getLocalizedMessage(), Toast.LENGTH_LONG).show();
Toast.makeText(mActivity, e.getLocalizedMessage(), Toast.LENGTH_LONG).show();
}
@Override
@ -126,12 +148,6 @@ public class NewsReaderDetailFragment extends Fragment {
}
};
/**
* Mandatory empty constructor for the fragment manager to instantiate the
* fragment (e.g. upon screen orientation changes).
*/
public NewsReaderDetailFragment() {
}
public static SORT_DIRECTION getSortDirection(SharedPreferences prefs) {
return NewsDetailActivity.getSortDirectionFromSettings(prefs);
@ -158,16 +174,16 @@ public class NewsReaderDetailFragment extends Fragment {
return titel;
}
public void setData(Long idFeed, Long idFolder, String titel, boolean updateListView) {
protected void setData(Long idFeed, Long idFolder, String title, boolean updateListView) {
Log.v(TAG, "Creating new itstance");
this.idFeed = idFeed;
this.idFolder = idFolder;
this.titel = titel;
((AppCompatActivity) getActivity()).getSupportActionBar().setTitle(titel);
this.titel = title;
mActivity.getSupportActionBar().setTitle(title);
if (updateListView) {
updateCurrentRssView(getActivity());
updateCurrentRssView();
} else {
refreshCurrentRssView();
}
@ -188,9 +204,9 @@ public class NewsReaderDetailFragment extends Fragment {
super.onResume();
}
public void updateMenuItemsState() {
NewsReaderListActivity nla = (NewsReaderListActivity) getActivity();
if (nla.getMenuItemDownloadMoreItems() != null) {
protected void updateMenuItemsState() {
NewsReaderListActivity nla = (NewsReaderListActivity) mActivity;
if(nla != null && nla.getMenuItemDownloadMoreItems() != null) {
if (idFolder != null && idFolder == ALL_UNREAD_ITEMS.getValue()) {
nla.getMenuItemDownloadMoreItems().setEnabled(false);
} else {
@ -199,7 +215,7 @@ public class NewsReaderDetailFragment extends Fragment {
}
}
public void notifyDataSetChangedOnAdapter() {
protected void notifyDataSetChangedOnAdapter() {
NewsListRecyclerAdapter nca = (NewsListRecyclerAdapter) recyclerView.getAdapter();
if (nca != null) {
nca.notifyDataSetChanged();
@ -209,20 +225,17 @@ public class NewsReaderDetailFragment extends Fragment {
/**
* Refreshes the current RSS-View
*/
public void refreshCurrentRssView() {
protected void refreshCurrentRssView() {
Log.v(TAG, "refreshCurrentRssView");
NewsListRecyclerAdapter nra = ((NewsListRecyclerAdapter) recyclerView.getAdapter());
if (nra != null) {
nra.refreshAdapterDataAsync(new NewsListRecyclerAdapter.IOnRefreshFinished() {
@Override
public void OnRefreshFinished() {
pbLoading.setVisibility(View.GONE);
nra.refreshAdapterDataAsync(() -> {
pbLoading.setVisibility(View.GONE);
if (layoutManagerSavedState != null) {
recyclerView.getLayoutManager().onRestoreInstanceState(layoutManagerSavedState);
layoutManagerSavedState = null;
}
if (layoutManagerSavedState != null) {
recyclerView.getLayoutManager().onRestoreInstanceState(layoutManagerSavedState);
layoutManagerSavedState = null;
}
});
}
@ -230,11 +243,10 @@ public class NewsReaderDetailFragment extends Fragment {
/**
* Updates the current RSS-View
* @param context
*/
public void updateCurrentRssView(Context context) {
public void updateCurrentRssView() {
Log.v(TAG, "updateCurrentRssView");
AsyncTaskHelper.StartAsyncTask(new UpdateCurrentRssViewTask(context));
AsyncTaskHelper.StartAsyncTask(new UpdateCurrentRssViewTask());
}
public RecyclerView getRecyclerView() {
@ -247,26 +259,22 @@ public class NewsReaderDetailFragment extends Fragment {
}
protected List<RssItem> performSearch(String searchString) {
Handler mainHandler = new Handler(getActivity().getMainLooper());
Handler mainHandler = new Handler(mActivity.getMainLooper());
Runnable myRunnable = new Runnable() {
@Override
public void run() {
pbLoading.setVisibility(View.VISIBLE);
tvNoItemsAvailable.setVisibility(View.GONE);
}
Runnable myRunnable = () -> {
pbLoading.setVisibility(View.VISIBLE);
tvNoItemsAvailable.setVisibility(View.GONE);
};
mainHandler.post(myRunnable);
return Search.PerformSearch(getActivity(), idFolder, idFeed, searchString, mPrefs);
return Search.PerformSearch(mActivity, idFolder, idFeed, searchString, mPrefs);
}
void loadRssItemsIntoView(List<RssItem> rssItems) {
try {
NewsListRecyclerAdapter nra = ((NewsListRecyclerAdapter) recyclerView.getAdapter());
if (nra == null) {
nra = new NewsListRecyclerAdapter(getActivity(), recyclerView, (PodcastFragmentActivity) getActivity(), ((PodcastFragmentActivity) getActivity()).mPostDelayHandler, mPrefs);
nra = new NewsListRecyclerAdapter(mActivity, recyclerView, mActivity, mPostDelayHandler, mPrefs);
recyclerView.setAdapter(nra);
}
nra.updateAdapterData(rssItems);
@ -293,25 +301,25 @@ public class NewsReaderDetailFragment extends Fragment {
ButterKnife.bind(this, rootView);
recyclerView.setHasFixedSize(true);
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity(), RecyclerView.VERTICAL, false));
recyclerView.setLayoutManager(new LinearLayoutManager(mActivity, RecyclerView.VERTICAL, false));
recyclerView.setItemAnimator(new DefaultItemAnimator());
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new NewsReaderItemTouchHelperCallback());
itemTouchHelper.attachToRecyclerView(recyclerView);
//recyclerView.addItemDecoration(new DividerItemDecoration(getActivity())); // Enable divider line
//recyclerView.addItemDecoration(new DividerItemDecoration(mActivity)); // Enable divider line
/*
recyclerView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
((NewsReaderListActivity) getActivity()).clearSearchViewFocus();
((NewsReaderListActivity) mActivity).clearSearchViewFocus();
return false;
}
});
*/
swipeRefresh.setColorSchemeColors(accentColor);
swipeRefresh.setOnRefreshListener((SwipeRefreshLayout.OnRefreshListener) getActivity());
swipeRefresh.setOnRefreshListener((SwipeRefreshLayout.OnRefreshListener) mActivity);
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
@ -326,7 +334,7 @@ public class NewsReaderDetailFragment extends Fragment {
});
itemTouchListener = new RecyclerView.OnItemTouchListener() {
GestureDetectorCompat detector = new GestureDetectorCompat(getActivity(), new RecyclerViewOnGestureListener());
GestureDetectorCompat detector = new GestureDetectorCompat(mActivity, new RecyclerViewOnGestureListener());
@Override
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
@ -433,11 +441,6 @@ public class NewsReaderDetailFragment extends Fragment {
}
private class UpdateCurrentRssViewTask extends AsyncTask<Void, Void, List<RssItem>> {
private Context context;
UpdateCurrentRssViewTask(Context context) {
this.context = context;
}
@Override
protected void onPreExecute() {
@ -448,7 +451,7 @@ public class NewsReaderDetailFragment extends Fragment {
@Override
protected List<RssItem> doInBackground(Void... voids) {
DatabaseConnectionOrm dbConn = new DatabaseConnectionOrm(context);
DatabaseConnectionOrm dbConn = new DatabaseConnectionOrm(NewsReaderDetailFragment.this.getContext());
SORT_DIRECTION sortDirection = getSortDirection(mPrefs);
boolean onlyUnreadItems = mPrefs.getBoolean(SettingsActivity.CB_SHOWONLYUNREAD_STRING, false);
boolean onlyStarredItems = false;
@ -508,7 +511,7 @@ public class NewsReaderDetailFragment extends Fragment {
minLeftEdgeDistance = 0;
} else {
// otherwise, have left-edge offset to avoid mark-read gesture when user is pulling to open drawer
minLeftEdgeDistance = ((NewsReaderListActivity) getActivity()).getEdgeSizeOfDrawer();
minLeftEdgeDistance = ((NewsReaderListActivity) mActivity).getEdgeSizeOfDrawer();
}
}

View file

@ -40,9 +40,25 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.SearchView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
import androidx.core.view.GravityCompat;
import androidx.customview.widget.ViewDragHelper;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.preference.PreferenceManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.google.android.material.snackbar.Snackbar;
import com.nextcloud.android.sso.AccountImporter;
import com.nextcloud.android.sso.api.NextcloudAPI;
@ -68,22 +84,6 @@ import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Named;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.core.view.GravityCompat;
import androidx.customview.widget.ViewDragHelper;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.preference.PreferenceManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import butterknife.BindView;
import butterknife.ButterKnife;
import de.luhmer.owncloudnewsreader.ListView.SubscriptionExpandableListAdapter;
@ -97,6 +97,7 @@ import de.luhmer.owncloudnewsreader.database.model.RssItem;
import de.luhmer.owncloudnewsreader.events.podcast.FeedPanelSlideEvent;
import de.luhmer.owncloudnewsreader.helper.DatabaseUtils;
import de.luhmer.owncloudnewsreader.helper.ThemeChooser;
import de.luhmer.owncloudnewsreader.helper.ThemeUtils;
import de.luhmer.owncloudnewsreader.reader.nextcloud.RssItemObservable;
import de.luhmer.owncloudnewsreader.services.DownloadImagesService;
import de.luhmer.owncloudnewsreader.services.DownloadWebPageService;
@ -109,12 +110,12 @@ import io.reactivex.Completable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.functions.Action;
import io.reactivex.functions.Consumer;
import io.reactivex.functions.Function;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.PublishSubject;
import uk.co.deanwild.materialshowcaseview.MaterialShowcaseSequence;
import uk.co.deanwild.materialshowcaseview.ShowcaseConfig;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import static de.luhmer.owncloudnewsreader.LoginDialogActivity.RESULT_LOGIN;
import static de.luhmer.owncloudnewsreader.LoginDialogActivity.ShowAlertDialog;
@ -151,7 +152,9 @@ public class NewsReaderListActivity extends PodcastFragmentActivity implements
@VisibleForTesting @Nullable @BindView(R.id.drawer_layout) public DrawerLayout drawerLayout;
private ActionBarDrawerToggle drawerToggle;
private SearchView searchView;
private SearchView mSearchView;
private String mSearchString;
private static final String SEARCH_KEY = "SEARCH_KEY";
private PublishSubject<String> searchPublishSubject;
private static final int REQUEST_CODE_PERMISSION_DOWNLOAD_WEB_ARCHIVE = 1;
@ -207,11 +210,10 @@ public class NewsReaderListActivity extends PodcastFragmentActivity implements
.commit();
if (drawerLayout != null) {
drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.empty_view_content, R.string.empty_view_content) {
drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.news_list_drawer_text, R.string.news_list_drawer_text) {
@Override
public void onDrawerClosed(View drawerView) {
super.onDrawerClosed(drawerView);
togglePodcastVideoViewAnimation();
syncState();
EventBus.getDefault().post(new FeedPanelSlideEvent(false));
@ -220,7 +222,6 @@ public class NewsReaderListActivity extends PodcastFragmentActivity implements
@Override
public void onDrawerOpened(View drawerView) {
super.onDrawerOpened(drawerView);
togglePodcastVideoViewAnimation();
reloadCountNumbersOfSlidingPaneAdapter();
syncState();
@ -239,17 +240,18 @@ public class NewsReaderListActivity extends PodcastFragmentActivity implements
drawerToggle.syncState();
}
if (savedInstanceState == null) {//When the app starts (no orientation change)
startDetailFragment(SubscriptionExpandableListAdapter.SPECIAL_FOLDERS.ALL_UNREAD_ITEMS.getValue(), true, null, true);
}
//AppRater.app_launched(this);
//AppRater.rateNow(this);
if (savedInstanceState == null) { //When the app starts (no orientation change)
updateDetailFragment(SubscriptionExpandableListAdapter.SPECIAL_FOLDERS.ALL_UNREAD_ITEMS.getValue(), true, null, true);
}
}
@Override
public void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
if (drawerToggle != null) {
drawerToggle.syncState();
}
@ -266,20 +268,27 @@ public class NewsReaderListActivity extends PodcastFragmentActivity implements
if (tabletSize) {
showTapLogoToSyncShowcaseView();
}
if (ActivityCompat.checkSelfPermission(this, ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(getString(R.string.permission_req_location_twilight_title))
.setMessage(getString(R.string.permission_req_location_twilight_text))
.setPositiveButton(android.R.string.ok, (dialog, id) -> {
//ActivityCompat.requestPermissions(this, new String[]{ACCESS_COARSE_LOCATION}, 1349);
ActivityCompat.requestPermissions(this, new String[]{ACCESS_FINE_LOCATION}, 139);
})
.create()
.show();
}
}
/* (non-Javadoc)
* @see com.actionbarsherlock.app.SherlockFragmentActivity#onSaveInstanceState(android.os.Bundle)
*/
@Override
protected void onSaveInstanceState(Bundle outState) {
saveInstanceState(outState);
super.onSaveInstanceState(outState);
}
/* (non-Javadoc)
* @see com.actionbarsherlock.app.SherlockFragmentActivity#onRestoreInstanceState(android.os.Bundle)
*/
@Override
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
restoreInstanceState(savedInstanceState);
@ -289,8 +298,9 @@ public class NewsReaderListActivity extends PodcastFragmentActivity implements
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (drawerToggle != null)
if (drawerToggle != null) {
drawerToggle.onConfigurationChanged(newConfig);
}
}
private void saveInstanceState(Bundle outState) {
@ -306,6 +316,10 @@ public class NewsReaderListActivity extends PodcastFragmentActivity implements
outState.putInt(LIST_ADAPTER_PAGE_COUNT, adapter.getCachedPages());
}
}
if(mSearchView != null) {
mSearchString = mSearchView.getQuery().toString();
outState.putString(SEARCH_KEY, mSearchString);
}
}
private void restoreInstanceState(Bundle savedInstanceState) {
@ -322,11 +336,12 @@ public class NewsReaderListActivity extends PodcastFragmentActivity implements
.getRecyclerView()
.setAdapter(adapter);
startDetailFragment(savedInstanceState.getLong(ID_FEED_STRING),
updateDetailFragment(savedInstanceState.getLong(ID_FEED_STRING),
savedInstanceState.getBoolean(IS_FOLDER_BOOLEAN),
savedInstanceState.getLong(OPTIONAL_FOLDER_ID),
false);
}
mSearchString = savedInstanceState.getString(SEARCH_KEY, null);
}
@ -414,12 +429,12 @@ public class NewsReaderListActivity extends PodcastFragmentActivity implements
NewsReaderDetailFragment ndf = getNewsReaderDetailFragment();
if (ndf != null) {
//ndf.reloadAdapterFromScratch();
ndf.updateCurrentRssView(NewsReaderListActivity.this);
ndf.updateCurrentRssView();
}
}
public void switchToAllUnreadItemsFolder() {
startDetailFragment(SubscriptionExpandableListAdapter.SPECIAL_FOLDERS.ALL_UNREAD_ITEMS.getValue(), true, null, true);
updateDetailFragment(SubscriptionExpandableListAdapter.SPECIAL_FOLDERS.ALL_UNREAD_ITEMS.getValue(), true, null, true);
}
@Subscribe(threadMode = ThreadMode.MAIN)
@ -507,24 +522,13 @@ public class NewsReaderListActivity extends PodcastFragmentActivity implements
if (firstVisiblePosition == 0 || firstVisiblePosition == -1) {
updateCurrentRssView();
} else {
Snackbar snackbar = Snackbar.make(findViewById(R.id.coordinator_layout),
getResources().getQuantityString(R.plurals.message_bar_new_articles_available, newItemsCount, newItemsCount),
Snackbar.LENGTH_LONG);
snackbar.setAction(getString(R.string.message_bar_reload), mSnackbarListener);
snackbar.setActionTextColor(ContextCompat.getColor(this, R.color.accent_material_dark));
// Setting android:TextColor to #000 in the light theme results in black on black
// text on the Snackbar, set the text back to white,
// TODO: find a cleaner way to do this
TextView textView = snackbar.getView().findViewById(com.google.android.material.R.id.snackbar_text);
textView.setTextColor(Color.WHITE);
snackbar.show();
showSnackbar(newItemsCount);
}
return true;
}
return false;
}
@Override
protected void onResume() {
NewsReaderListFragment newsReaderListFragment = getSlidingListFragment();
@ -541,6 +545,19 @@ public class NewsReaderListActivity extends PodcastFragmentActivity implements
startSync();
}
private void showSnackbar(int newItemsCount) {
Snackbar snackbar = Snackbar.make(findViewById(R.id.coordinator_layout),
getResources().getQuantityString(R.plurals.message_bar_new_articles_available, newItemsCount, newItemsCount),
Snackbar.LENGTH_LONG);
snackbar.setAction(getString(R.string.message_bar_reload), mSnackbarListener);
//snackbar.setActionTextColor(ContextCompat.getColor(this, R.color.accent_material_dark));
// Setting android:TextColor to #000 in the light theme results in black on black
// text on the Snackbar, set the text back to white,
//TextView textView = snackbar.getView().findViewById(com.google.android.material.R.id.snackbar_text);
//textView.setTextColor(Color.WHITE);
snackbar.show();
}
/**
* Callback method from {@link NewsReaderListFragment.Callbacks} indicating
* that the item with the given ID was selected.
@ -550,7 +567,7 @@ public class NewsReaderListActivity extends PodcastFragmentActivity implements
if (drawerLayout != null)
drawerLayout.closeDrawer(GravityCompat.START);
startDetailFragment(idFeed, isFolder, optional_folder_id, true);
updateDetailFragment(idFeed, isFolder, optional_folder_id, true);
}
@Override
@ -558,7 +575,7 @@ public class NewsReaderListActivity extends PodcastFragmentActivity implements
if (drawerLayout != null)
drawerLayout.closeDrawer(GravityCompat.START);
startDetailFragment(idFeed, false, optional_folder_id, true);
updateDetailFragment(idFeed, false, optional_folder_id, true);
}
@Override
@ -600,41 +617,37 @@ public class NewsReaderListActivity extends PodcastFragmentActivity implements
}
private NewsReaderDetailFragment startDetailFragment(long id, Boolean folder, Long optional_folder_id, boolean updateListView)
{
if(menuItemDownloadMoreItems != null) {
menuItemDownloadMoreItems.setEnabled(true);
}
private NewsReaderDetailFragment updateDetailFragment(long id, Boolean folder, Long optional_folder_id, boolean updateListView) {
if(menuItemDownloadMoreItems != null) {
menuItemDownloadMoreItems.setEnabled(true);
}
DatabaseConnectionOrm dbConn = new DatabaseConnectionOrm(getApplicationContext());
DatabaseConnectionOrm dbConn = new DatabaseConnectionOrm(getApplicationContext());
Long feedId = null;
Long folderId;
String title = null;
Long feedId = null;
Long folderId;
String title = null;
if(!folder)
{
feedId = id;
folderId = optional_folder_id;
title = dbConn.getFeedById(id).getFeedTitle();
}
else
{
folderId = id;
int idFolder = (int) id;
if(idFolder >= 0)
title = dbConn.getFolderById(id).getLabel();
else if(idFolder == -10)
title = getString(R.string.allUnreadFeeds);
else if(idFolder == -11)
title = getString(R.string.starredFeeds);
if(!folder) {
feedId = id;
folderId = optional_folder_id;
title = dbConn.getFeedById(id).getFeedTitle();
} else {
folderId = id;
int idFolder = (int) id;
if(idFolder >= 0) {
title = dbConn.getFolderById(id).getLabel();
} else if(idFolder == -10) {
title = getString(R.string.allUnreadFeeds);
} else if(idFolder == -11) {
title = getString(R.string.starredFeeds);
}
}
}
NewsReaderDetailFragment fragment = getNewsReaderDetailFragment();
fragment.setData(feedId, folderId, title, updateListView);
return fragment;
}
NewsReaderDetailFragment fragment = getNewsReaderDetailFragment();
fragment.setData(feedId, folderId, title, updateListView);
return fragment;
}
public void UpdateItemList()
@ -698,39 +711,48 @@ public class NewsReaderListActivity extends PodcastFragmentActivity implements
MenuItem searchItem = menu.findItem(R.id.menu_search);
//Set expand listener to close keyboard
searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
@Override
public boolean onMenuItemActionExpand(MenuItem item) {
return true;
}
@Override
public boolean onMenuItemActionCollapse(MenuItem item) {
clearSearchViewFocus();
return true;
}
});
searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView();
searchView.setIconifiedByDefault(false);
searchView.setOnQueryTextListener(this);
searchView.setOnQueryTextFocusChangeListener(new View.OnFocusChangeListener() {
//Set expand listener to close keyboard
searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if(!hasFocus) {
clearSearchViewFocus();
}
public boolean onMenuItemActionExpand(MenuItem item) {
return true;
}
@Override
public boolean onMenuItemActionCollapse(MenuItem item) {
//onQueryTextChange(""); // Reset search
mSearchView.setQuery("", true);
clearSearchViewFocus();
return true;
}
});
NewsReaderDetailFragment ndf = getNewsReaderDetailFragment();
if(ndf != null)
ndf.updateMenuItemsState();
mSearchView = (SearchView) menu.findItem(R.id.menu_search).getActionView();
mSearchView.setIconifiedByDefault(false);
mSearchView.setOnQueryTextListener(this);
mSearchView.setOnQueryTextFocusChangeListener((v, hasFocus) -> {
if(!hasFocus) {
clearSearchViewFocus();
}
});
ThemeUtils.colorSearchViewCursorColor(mSearchView, Color.WHITE);
NewsReaderDetailFragment ndf = getNewsReaderDetailFragment();
if(ndf != null) {
ndf.updateMenuItemsState();
}
updateButtonLayout();
return true;
// focus the SearchView (if search view was active before orientation change)
if (mSearchString != null && !mSearchString.isEmpty()) {
searchItem.expandActionView();
mSearchView.setQuery(mSearchString, true);
mSearchView.clearFocus();
}
return true;
}
public MenuItem getMenuItemDownloadMoreItems() {
@ -826,9 +848,9 @@ public class NewsReaderListActivity extends PodcastFragmentActivity implements
return true;
case R.id.menu_search:
searchView.setIconified(false);
searchView.setFocusable(true);
searchView.requestFocusFromTouch();
mSearchView.setIconified(false);
mSearchView.setFocusable(true);
mSearchView.requestFocusFromTouch();
return true;
case R.id.menu_download_web_archive:
@ -1071,17 +1093,10 @@ public class NewsReaderListActivity extends PodcastFragmentActivity implements
searchPublishSubject
.debounce(400, TimeUnit.MILLISECONDS)
.distinctUntilChanged()
.map(new Function<String, List<RssItem>>() {
@Override
public List<RssItem> apply(String s) throws Exception {
return getNewsReaderDetailFragment().performSearch(s);
}
})
.map(s -> getNewsReaderDetailFragment().performSearch(s))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(getNewsReaderDetailFragment().SearchResultObserver)
.subscribeWith(getNewsReaderDetailFragment().searchResultObserver)
.isDisposed();
}
@ -1090,6 +1105,6 @@ public class NewsReaderListActivity extends PodcastFragmentActivity implements
}
public void clearSearchViewFocus() {
searchView.clearFocus();
mSearchView.clearFocus();
}
}

View file

@ -49,6 +49,7 @@ import java.io.Serializable;
import javax.inject.Inject;
import androidx.annotation.VisibleForTesting;
import androidx.fragment.app.Fragment;
import butterknife.BindView;
import butterknife.ButterKnife;
@ -56,6 +57,8 @@ import de.luhmer.owncloudnewsreader.ListView.SubscriptionExpandableListAdapter;
import de.luhmer.owncloudnewsreader.database.DatabaseConnectionOrm;
import de.luhmer.owncloudnewsreader.di.ApiProvider;
import de.luhmer.owncloudnewsreader.interfaces.ExpListTextClicked;
import de.luhmer.owncloudnewsreader.model.AbstractItem;
import de.luhmer.owncloudnewsreader.model.ConcreteFeedItem;
import de.luhmer.owncloudnewsreader.model.FolderSubscribtionItem;
import de.luhmer.owncloudnewsreader.model.UserInfo;
import io.reactivex.Observer;
@ -221,17 +224,27 @@ public class NewsReaderListFragment extends Fragment implements OnCreateContextM
};
// Code below is only used for unit tests
@VisibleForTesting
public OnChildClickListener onChildClickListener = new OnChildClickListener() {
@Override
public boolean onChildClick(ExpandableListView parent, View v,
int groupPosition, int childPosition, long id) {
long idItem = lvAdapter.getChildId(groupPosition, childPosition);
long idItem;
if(childPosition != -1) {
idItem = lvAdapter.getChildId(groupPosition, childPosition);
} else {
idItem = groupPosition;
}
Long optional_id_folder = null;
FolderSubscribtionItem groupItem = (FolderSubscribtionItem) lvAdapter.getGroup(groupPosition);
AbstractItem groupItem = (AbstractItem) lvAdapter.getGroup(groupPosition);
if(groupItem != null)
optional_id_folder = groupItem.id_database;
if(groupItem instanceof ConcreteFeedItem) {
idItem = ((ConcreteFeedItem)groupItem).feedId;
}
mCallbacks.onChildItemClicked(idItem, optional_id_folder);

View file

@ -0,0 +1,276 @@
package de.luhmer.owncloudnewsreader;
import android.app.PictureInPictureParams;
import android.content.ComponentName;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.Point;
import android.os.Build;
import android.os.Bundle;
import android.os.RemoteException;
import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.Log;
import android.view.Display;
import android.view.SurfaceView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import org.greenrobot.eventbus.EventBus;
import de.luhmer.owncloudnewsreader.events.podcast.RegisterVideoOutput;
import de.luhmer.owncloudnewsreader.helper.ThemeChooser;
import de.luhmer.owncloudnewsreader.services.PodcastPlaybackService;
import de.luhmer.owncloudnewsreader.services.podcast.PlaybackService;
import static de.luhmer.owncloudnewsreader.services.PodcastPlaybackService.CURRENT_PODCAST_MEDIA_TYPE;
public class PiPVideoPlaybackActivity extends AppCompatActivity {
private static final String TAG = PiPVideoPlaybackActivity.class.getCanonicalName();
private EventBus mEventBus;
private MediaBrowserCompat mMediaBrowser;
protected static boolean activityIsRunning = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
ThemeChooser.chooseTheme(this);
super.onCreate(savedInstanceState);
ThemeChooser.afterOnCreate(this);
setContentView(R.layout.activity_pip_video_playback);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//moveTaskToBack(false);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PictureInPictureParams.Builder pictureInPictureParamsBuilder = new PictureInPictureParams.Builder();
//Rational aspectRatio = new Rational(vv.getWidth(), vv.getHeight());
//pictureInPictureParamsBuilder.setAspectRatio(aspectRatio).build();
enterPictureInPictureMode(pictureInPictureParamsBuilder.build());
} else {
enterPictureInPictureMode();
}
enterPictureInPictureMode();
} else {
Toast.makeText(this, "This device does not support video playback.", Toast.LENGTH_LONG).show();
finish();
}
}
@Override
public void onPictureInPictureModeChanged (boolean isInPictureInPictureMode, Configuration newConfig) {
Log.d(TAG, "onPictureInPictureModeChanged() called with: isInPictureInPictureMode = [" + isInPictureInPictureMode + "], newConfig = [" + newConfig + "]");
RelativeLayout surfaceViewWrapper = findViewById(R.id.layout_activity_pip);
SurfaceView surfaceView = (SurfaceView) surfaceViewWrapper.getChildAt(0);
if(surfaceView != null) {
if (isInPictureInPictureMode) {
surfaceView.setLayoutParams(new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.MATCH_PARENT,
RelativeLayout.LayoutParams.MATCH_PARENT));
} else {
Display display = getWindowManager().getDefaultDisplay();
Point size = new Point();
display.getSize(size);
float width = size.x;
//int height = size.y;
//int newWidth = (int) (width * (9f/16f));
int newWidth = (int) (width * (3f/4f));
surfaceView.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, newWidth));
}
}
/*
if (isInPictureInPictureMode) {
// Hide the full-screen UI (controls, etc.) while in picture-in-picture mode.
} else {
// Restore the full-screen UI.
//Intent intent = new Intent(this, NewsReaderListActivity.class);
//intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
//startActivity(intent);
// Finish PiP Activity
//finish();
}
*/
/*
// When dismissing
if(!isInPictureInPictureMode) {
finish();
}
*/
}
@Override
protected void onStart() {
Log.d(TAG, "onStart() called");
super.onStart();
mEventBus = EventBus.getDefault();
//mEventBus.register(this);
mMediaBrowser = new MediaBrowserCompat(this,
new ComponentName(this, PodcastPlaybackService.class),
mConnectionCallbacks,
null); // optional Bundle
mMediaBrowser.connect();
activityIsRunning = true;
}
@Override
public void onStop() {
Log.d(TAG, "onStop() called");
unregisterVideoViews();
//mEventBus.unregister(this);
// (see "stay in sync with the MediaSession")
if (MediaControllerCompat.getMediaController(this) != null) {
MediaControllerCompat.getMediaController(this).unregisterCallback(controllerCallback);
}
mMediaBrowser.disconnect();
activityIsRunning = false;
super.onStop();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) {
finishAndRemoveTask();
}
}
}
public void unregisterVideoViews() {
mEventBus.post(new RegisterVideoOutput(null, null));
}
/*
@Subscribe
public void onEvent(CollapsePodcastView event) {
Log.d(TAG, "onEvent() called with: event = [" + event + "]");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
finishAndRemoveTask();
} else {
finish();
}
}
*/
@Override
public void onBackPressed() {
Log.d(TAG, "onBackPressed() called");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
enterPictureInPictureMode();
}
}
private final MediaBrowserCompat.ConnectionCallback mConnectionCallbacks =
new MediaBrowserCompat.ConnectionCallback() {
@Override
public void onConnected() {
Log.d(TAG, "onConnected() called");
// Get the token for the MediaSession
MediaSessionCompat.Token token = mMediaBrowser.getSessionToken();
try {
// Create a MediaControllerCompat
MediaControllerCompat mediaController = new MediaControllerCompat(PiPVideoPlaybackActivity.this, token);
// Save the controller
MediaControllerCompat.setMediaController(PiPVideoPlaybackActivity.this, mediaController);
// Register a Callback to stay in sync
mediaController.registerCallback(controllerCallback);
// Display the initial state
MediaMetadataCompat metadata = mediaController.getMetadata();
handleMetadataChange(metadata);
} catch (RemoteException e) {
Log.e(TAG, "Connecting to podcast service failed!", e);
}
}
};
MediaControllerCompat.Callback controllerCallback =
new MediaControllerCompat.Callback() {
@Override
public void onMetadataChanged(MediaMetadataCompat metadata) {
Log.v(TAG, "onMetadataChanged() called with: metadata = [" + metadata + "]");
handleMetadataChange(metadata);
}
@Override
public void onPlaybackStateChanged(PlaybackStateCompat stateCompat) {
Log.v(TAG, "onPlaybackStateChanged() called with: state = [" + stateCompat + "]");
}
};
private void handleMetadataChange(MediaMetadataCompat metadata) {
Log.d(TAG, "handleMetadataChange() called with: metadata = [" + metadata + "]");
unregisterVideoViews();
RelativeLayout surfaceViewWrapper = findViewById(R.id.layout_activity_pip);
surfaceViewWrapper.removeAllViews();
PlaybackService.VideoType mediaType = PlaybackService.VideoType.valueOf(metadata.getString(CURRENT_PODCAST_MEDIA_TYPE));
Log.d(TAG, "handleMetadataChange() called with: mediaType = [" + mediaType + "]");
switch (mediaType) {
case None:
finish();
break;
case Video:
// default
SurfaceView surfaceView = createSurfaceView();
surfaceViewWrapper.addView(surfaceView);
mEventBus.post(new RegisterVideoOutput(surfaceView, surfaceViewWrapper));
break;
/*
case YouTube:
final int YOUTUBE_CONTENT_VIEW_ID = 10101010;
FrameLayout frame = new FrameLayout(this);
frame.setId(YOUTUBE_CONTENT_VIEW_ID);
surfaceViewWrapper.addView(frame);
YoutubePlayerManager.StartYoutubePlayer(this, YOUTUBE_CONTENT_VIEW_ID, mEventBus, () -> Log.d(TAG, "onInit Success()"));
break;
*/
default:
break;
}
}
private SurfaceView createSurfaceView() {
SurfaceView surfaceView = new SurfaceView(this);
surfaceView.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT));
return surfaceView;
}
}

View file

@ -1,13 +1,25 @@
package de.luhmer.owncloudnewsreader;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.net.Uri;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import android.os.Handler;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.text.InputFilter;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -22,7 +34,6 @@ import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ViewSwitcher;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
@ -43,12 +54,12 @@ import butterknife.ButterKnife;
import butterknife.OnClick;
import de.luhmer.owncloudnewsreader.ListView.PodcastArrayAdapter;
import de.luhmer.owncloudnewsreader.ListView.PodcastFeedArrayAdapter;
import de.luhmer.owncloudnewsreader.events.podcast.CollapsePodcastView;
import de.luhmer.owncloudnewsreader.events.podcast.ExpandPodcastView;
import de.luhmer.owncloudnewsreader.events.podcast.SpeedPodcast;
import de.luhmer.owncloudnewsreader.events.podcast.StartDownloadPodcast;
import de.luhmer.owncloudnewsreader.events.podcast.TogglePlayerStateEvent;
import de.luhmer.owncloudnewsreader.events.podcast.UpdatePodcastStatusEvent;
import de.luhmer.owncloudnewsreader.events.podcast.WindPodcast;
import de.luhmer.owncloudnewsreader.model.MediaItem;
import de.luhmer.owncloudnewsreader.model.PodcastFeedItem;
import de.luhmer.owncloudnewsreader.model.PodcastItem;
import de.luhmer.owncloudnewsreader.services.PodcastDownloadService;
@ -56,12 +67,12 @@ import de.luhmer.owncloudnewsreader.services.PodcastPlaybackService;
import de.luhmer.owncloudnewsreader.services.podcast.PlaybackService;
import de.luhmer.owncloudnewsreader.view.PodcastSlidingUpPanelLayout;
import static android.media.MediaMetadata.METADATA_KEY_MEDIA_ID;
import static de.luhmer.owncloudnewsreader.services.PodcastPlaybackService.CURRENT_PODCAST_MEDIA_TYPE;
import static de.luhmer.owncloudnewsreader.services.PodcastPlaybackService.PLAYBACK_SPEED_FLOAT;
/**
* A simple {@link Fragment} subclass.
* Activities that contain this fragment must implement the
* {@link PodcastFragment.OnFragmentInteractionListener} interface
* to StartYoutubePlayer interaction events.
* Use the {@link PodcastFragment#newInstance} factory method to
* create an instance of this fragment.
*
@ -69,12 +80,43 @@ import de.luhmer.owncloudnewsreader.view.PodcastSlidingUpPanelLayout;
public class PodcastFragment extends Fragment {
private static final String TAG = PodcastFragment.class.getCanonicalName();
//private static UpdatePodcastStatusEvent podcast; // Retain over different instances
private UpdatePodcastStatusEvent podcast;
private int lastDrawableId;
private OnFragmentInteractionListener mListener;
private PodcastSlidingUpPanelLayout sliding_layout;
private EventBus eventBus;
private MediaBrowserCompat mMediaBrowser;
private Activity mActivity;
private long currentPositionInMillis = 0;
private long maxPositionInMillis = 100000;
protected @BindView(R.id.btn_playPausePodcast) ImageButton btnPlayPausePodcast;
protected @BindView(R.id.btn_playPausePodcastSlider) ImageButton btnPlayPausePodcastSlider;
protected @BindView(R.id.btn_nextPodcastSlider) ImageButton btnNextPodcastSlider;
protected @BindView(R.id.btn_previousPodcastSlider) ImageButton btnPreviousPodcastSlider;
protected @BindView(R.id.btn_podcastSpeed) ImageButton btnPlaybackSpeed;
protected @BindView(R.id.img_feed_favicon) ImageView imgFavIcon;
protected @BindView(R.id.tv_title) TextView tvTitle;
protected @BindView(R.id.tv_titleSlider) TextView tvTitleSlider;
protected @BindView(R.id.tv_from) TextView tvFrom;
protected @BindView(R.id.tv_to) TextView tvTo;
protected @BindView(R.id.tv_fromSlider) TextView tvFromSlider;
protected @BindView(R.id.tv_ToSlider) TextView tvToSlider;
protected @BindView(R.id.sb_progress) SeekBar sb_progress;
protected @BindView(R.id.pb_progress) ProgressBar pb_progress;
protected @BindView(R.id.pb_progress2) ProgressBar pb_progress2;
protected @BindView(R.id.podcastFeedList) ListView /* CardGridView CardListView*/ podcastFeedList;
protected @BindView(R.id.rlPodcast) RelativeLayout rlPodcast;
protected @BindView(R.id.ll_podcast_header) LinearLayout rlPodcastHeader;
protected @BindView(R.id.fl_playPausePodcastWrapper) FrameLayout playPausePodcastWrapper;
protected @BindView(R.id.podcastTitleGrid) ListView /*CardGridView*/ podcastTitleGrid;
protected @BindView(R.id.viewSwitcherProgress) ViewSwitcher /*CardGridView*/ viewSwitcherProgress;
/**
* Use this factory method to create a new instance of
@ -85,42 +127,76 @@ public class PodcastFragment extends Fragment {
public static PodcastFragment newInstance() {
return new PodcastFragment();
}
public PodcastFragment() {
// Required empty public constructor
}
//Your created method
public boolean onBackPressed() //returns if the event was handled
{
return false;
}
EventBus eventBus;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
//setRetainInstance(true);
eventBus = EventBus.getDefault();
}
@Override
public void onResume() {
eventBus.register(this);
super.onResume();
//mActivity.setVolumeControlStream(AudioManager.STREAM_MUSIC);
}
@Override
public void onPause() {
eventBus.unregister(this);
super.onPause();
eventBus.unregister(this);
}
@Override
public void onStart() {
super.onStart();
mMediaBrowser = new MediaBrowserCompat(mActivity,
new ComponentName(mActivity, PodcastPlaybackService.class),
mConnectionCallbacks,
null); // optional Bundle
mMediaBrowser.connect();
}
@Override
public void onStop() {
super.onStop();
// (see "stay in sync with the MediaSession")
if (MediaControllerCompat.getMediaController(mActivity) != null) {
MediaControllerCompat.getMediaController(mActivity).unregisterCallback(mediaControllerCallback);
MediaControllerCompat.getMediaController(mActivity).unregisterCallback(controllerCallback);
}
mMediaBrowser.disconnect();
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
mActivity = getActivity();
}
@Override
public void onDetach() {
super.onDetach();
mActivity = null;
}
protected void tryOpeningPictureinPictureMode() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//moveTaskToBack(false /* nonRoot */);
if(!PiPVideoPlaybackActivity.activityIsRunning) {
Intent intent = new Intent(getActivity(), PiPVideoPlaybackActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
//intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
//intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
//intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
}
}
@Subscribe
@ -152,164 +228,46 @@ public class PodcastFragment extends Fragment {
}
}
long lastPodcastRssItemId = -1;
@Subscribe
public void onEvent(UpdatePodcastStatusEvent podcast) {
this.podcast = podcast;
hasTitleInCache = true;
int drawableId = podcast.isPlaying() ? R.drawable.ic_action_pause : R.drawable.ic_action_play_arrow;
int contentDescriptionId = podcast.isPlaying() ? R.string.content_desc_pause : R.string.content_desc_play;
if(lastDrawableId != drawableId) {
lastDrawableId = drawableId;
btnPlayPausePodcast.setImageResource(drawableId);
btnPlayPausePodcast.setContentDescription(getString(contentDescriptionId));
btnPlayPausePodcastSlider.setImageResource(drawableId);
}
if(lastPodcastRssItemId != podcast.getRssItemId() && imgFavIcon != null) {
if(loadPodcastFavIcon()) {
lastPodcastRssItemId = podcast.getRssItemId();
}
}
int hours = (int)(podcast.getCurrent() / (1000*60*60));
int minutes = (int)(podcast.getCurrent() % (1000*60*60)) / (1000*60);
int seconds = (int) ((podcast.getCurrent() % (1000*60*60)) % (1000*60) / 1000);
minutes += hours * 60;
tvFrom.setText(String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds));
tvFromSlider.setText(String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds));
hours = (int)( podcast.getMax() / (1000*60*60));
minutes = (int)(podcast.getMax() % (1000*60*60)) / (1000*60);
seconds = (int) ((podcast.getMax() % (1000*60*60)) % (1000*60) / 1000);
minutes += hours * 60;
tvTo.setText(String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds));
tvToSlider.setText(String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds));
tvTitle.setText(podcast.getTitle());
tvTitleSlider.setText(podcast.getTitle());
if(podcast.getStatus() == PlaybackService.Status.PREPARING) {
sb_progress.setVisibility(View.INVISIBLE);
pb_progress2.setVisibility(View.VISIBLE);
pb_progress.setIndeterminate(true);
} else {
double progress = ((double) podcast.getCurrent() / (double) podcast.getMax()) * 100d;
if(!blockSeekbarUpdate) {
sb_progress.setVisibility(View.VISIBLE);
pb_progress2.setVisibility(View.INVISIBLE);
sb_progress.setProgress((int) progress);
}
pb_progress.setIndeterminate(false);
pb_progress.setProgress((int) progress);
}
@OnClick(R.id.fl_playPausePodcastWrapper)
protected void playPause() {
eventBus.post(new TogglePlayerStateEvent());
}
private boolean loadPodcastFavIcon() {
return ((PodcastFragmentActivity) getActivity()).getCurrentPlayingPodcast(
new PodcastFragmentActivity.OnCurrentPlayingPodcastCallback() {
@Override
public void currentPlayingPodcastReceived(MediaItem mediaItem) {
Log.d(TAG, "currentPlayingPodcastReceived() called with: mediaItem = [" + mediaItem + "]");
if(mediaItem != null) {
String favIconUrl = mediaItem.favIcon;
Log.d(TAG, "currentPlayingPodcastReceived: " + favIconUrl);
DisplayImageOptions displayImageOptions = new DisplayImageOptions.Builder().
showImageOnLoading(R.drawable.default_feed_icon_light).
showImageForEmptyUri(R.drawable.default_feed_icon_light).
showImageOnFail(R.drawable.default_feed_icon_light).
build();
ImageLoader.getInstance().displayImage(favIconUrl, imgFavIcon, displayImageOptions);
}
}
});
}
@BindView(R.id.btn_playPausePodcast) ImageButton btnPlayPausePodcast;
@BindView(R.id.btn_playPausePodcastSlider) ImageButton btnPlayPausePodcastSlider;
@BindView(R.id.btn_nextPodcastSlider) ImageButton btnNextPodcastSlider;
@BindView(R.id.btn_previousPodcastSlider) ImageButton btnPreviousPodcastSlider;
@BindView(R.id.btn_podcastSpeed) ImageButton btnPlaybackSpeed;
@BindView(R.id.img_feed_favicon) ImageView imgFavIcon;
@BindView(R.id.tv_title) TextView tvTitle;
@BindView(R.id.tv_titleSlider) TextView tvTitleSlider;
@BindView(R.id.tv_from) TextView tvFrom;
@BindView(R.id.tv_to) TextView tvTo;
@BindView(R.id.tv_fromSlider) TextView tvFromSlider;
@BindView(R.id.tv_ToSlider) TextView tvToSlider;
@BindView(R.id.sb_progress) SeekBar sb_progress;
@BindView(R.id.pb_progress) ProgressBar pb_progress;
@BindView(R.id.pb_progress2) ProgressBar pb_progress2;
@BindView(R.id.podcastFeedList) ListView /* CardGridView CardListView*/ podcastFeedList;
@BindView(R.id.rlPodcast) RelativeLayout rlPodcast;
@BindView(R.id.ll_podcast_header) LinearLayout rlPodcastHeader;
@BindView(R.id.fl_playPausePodcastWrapper) FrameLayout playPausePodcastWrapper;
@BindView(R.id.podcastTitleGrid) ListView /*CardGridView*/ podcastTitleGrid;
@BindView(R.id.viewSwitcherProgress) ViewSwitcher /*CardGridView*/ viewSwitcherProgress;
boolean hasTitleInCache = false;
@OnClick(R.id.fl_playPausePodcastWrapper) void playPause() {
if(!hasTitleInCache) {
Toast.makeText(getActivity(), "Please select a title first", Toast.LENGTH_SHORT).show();
} else {
eventBus.post(new TogglePlayerStateEvent());
}
}
@OnClick(R.id.btn_playPausePodcastSlider) void playPauseSlider() {
@OnClick(R.id.btn_playPausePodcastSlider)
protected void playPauseSlider() {
playPause();
}
@OnClick(R.id.btn_nextPodcastSlider) void nextChapter() {
eventBus.post(new WindPodcast() {{
long position = podcast.getCurrent() + 30000;
toPositionInPercent = ((double) position / (double) podcast.getMax()) * 100d;
}});
@OnClick(R.id.btn_nextPodcastSlider)
protected void windForward() {
eventBus.post(new WindPodcast(30000));
//Toast.makeText(getActivity(), "This feature is not supported yet :(", Toast.LENGTH_SHORT).show();
}
@OnClick(R.id.btn_previousPodcastSlider) void previousChapter() {
eventBus.post(new WindPodcast() {{
long position = podcast.getCurrent() - 10000;
toPositionInPercent = ((double) position / (double) podcast.getMax()) * 100d;
}});
@OnClick(R.id.btn_previousPodcastSlider)
protected void windBack() {
eventBus.post(new WindPodcast(-10000));
}
@OnClick(R.id.btn_podcastSpeed) void openSpeedMenu() {
@OnClick(R.id.btn_podcastSpeed)
protected void openSpeedMenu() {
showPlaybackSpeedPicker();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// create ContextThemeWrapper from the original Activity Context with the custom theme
Context context = new ContextThemeWrapper(getActivity(), R.style.Theme_MaterialComponents_Light_DarkActionBar);
//Context context = new ContextThemeWrapper(getActivity(), R.style.Theme_MaterialComponents_Light_DarkActionBar);
// clone the inflater using the ContextThemeWrapper
LayoutInflater localInflater = inflater.cloneInContext(context);
//LayoutInflater localInflater = inflater.cloneInContext(context);
// inflate using the cloned inflater, not the passed in default
View view = localInflater.inflate(R.layout.fragment_podcast, container, false);
//View view = localInflater.inflate(R.layout.fragment_podcast, container, false);
View view = inflater.inflate(R.layout.fragment_podcast, container, false);
//View view = inflater.inflate(R.layout.fragment_podcast, container, false);
ButterKnife.bind(this, view);
if(getActivity() instanceof PodcastFragmentActivity) {
sliding_layout = ((PodcastFragmentActivity) getActivity()).getSlidingLayout();
}
@ -322,8 +280,6 @@ public class PodcastFragment extends Fragment {
sliding_layout.setPanelSlideListener(onPanelSlideListener);
}
PodcastFeedArrayAdapter mArrayAdapter = new PodcastFeedArrayAdapter(getActivity(), new PodcastFeedItem[0]);
if(mArrayAdapter.getCount() > 0) {
@ -339,49 +295,16 @@ public class PodcastFragment extends Fragment {
}
public void onButtonPressed(Uri uri) {
if (mListener != null) {
mListener.onFragmentInteraction(uri);
}
}
@Override
public void onDetach() {
super.onDetach();
mListener = null;
}
/**
* This interface must be implemented by activities that contain this
* fragment to allow an interaction in this fragment to be communicated
* to the activity and potentially other fragments contained in that
* activity.
* <p>
* See the Android Training lesson <a href=
* "http://developer.android.com/training/basics/fragments/communicating.html"
* >Communicating with Other Fragments</a> for more information.
*/
public interface OnFragmentInteractionListener {
void onFragmentInteraction(Uri uri);
}
private SlidingUpPanelLayout.PanelSlideListener onPanelSlideListener = new SlidingUpPanelLayout.PanelSlideListener() {
@Override
public void onPanelSlide(View view, float v) {
}
public void onPanelSlide(View view, float v) { }
@Override
public void onPanelCollapsed(View view) {
if(sliding_layout != null)
sliding_layout.setDragView(rlPodcastHeader);
viewSwitcherProgress.setDisplayedChild(0);
if(getActivity() instanceof PodcastFragmentActivity)
((PodcastFragmentActivity)getActivity()).togglePodcastVideoViewAnimation();
}
@Override
@ -389,47 +312,45 @@ public class PodcastFragment extends Fragment {
if(sliding_layout != null)
sliding_layout.setDragView(viewSwitcherProgress);
viewSwitcherProgress.setDisplayedChild(1);
if(getActivity() instanceof PodcastFragmentActivity)
((PodcastFragmentActivity)getActivity()).togglePodcastVideoViewAnimation();
}
@Override
public void onPanelAnchored(View view) {
@Override public void onPanelAnchored(View view) { }
}
@Override
public void onPanelHidden(View view) {
}
@Override public void onPanelHidden(View view) { }
};
boolean blockSeekbarUpdate = false;
private SeekBar.OnSeekBarChangeListener onSeekBarChangeListener = new SeekBar.OnSeekBarChangeListener() {
int before;
@Override
public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
//Log.v(TAG, "onProgressChanged");
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
/*
if(fromUser) {
Log.v(TAG, "onProgressChanged: " + progress + "%");
before = progress;
}
*/
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
blockSeekbarUpdate = true;
Log.v(TAG, "onStartTrackingTouch");
before = seekBar.getProgress();
blockSeekbarUpdate = true;
}
@Override
public void onStopTrackingTouch(final SeekBar seekBar) {
if(hasTitleInCache) {
eventBus.post(new WindPodcast() {{
toPositionInPercent = seekBar.getProgress();
}});
blockSeekbarUpdate = false;
}
Log.v(TAG, "onStopTrackingTouch");
int diffInSeconds = seekBar.getProgress() - before;
eventBus.post(new WindPodcast(diffInSeconds));
blockSeekbarUpdate = false;
}
};
// TODO SEEK DOES NOT WORK PROPERLY!!!!
private void showPlaybackSpeedPicker() {
@ -437,20 +358,12 @@ public class PodcastFragment extends Fragment {
numberPicker.setDescendantFocusability(NumberPicker.FOCUS_BLOCK_DESCENDANTS);
numberPicker.setMinValue(0);
numberPicker.setMaxValue(PodcastPlaybackService.PLAYBACK_SPEEDS.length-1);
numberPicker.setFormatter(new NumberPicker.Formatter() {
@Override
public String format(int i) {
return String.valueOf(PodcastPlaybackService.PLAYBACK_SPEEDS[i]);
}
});
numberPicker.setFormatter(i -> String.valueOf(PodcastPlaybackService.PLAYBACK_SPEEDS[i]));
if(getActivity() instanceof PodcastFragmentActivity) {
((PodcastFragmentActivity) getActivity()).getCurrentPlaybackSpeed(new PodcastFragmentActivity.OnPlaybackSpeedCallback() {
@Override
public void currentPlaybackReceived(float playbackSpeed) {
int position = Arrays.binarySearch(PodcastPlaybackService.PLAYBACK_SPEEDS, playbackSpeed);
numberPicker.setValue(position);
}
getCurrentPlaybackSpeed(playbackSpeed -> {
int position = Arrays.binarySearch(PodcastPlaybackService.PLAYBACK_SPEEDS, playbackSpeed);
numberPicker.setValue(position);
});
} else {
@ -467,19 +380,12 @@ public class PodcastFragment extends Fragment {
// set dialog message
alertDialogBuilder
.setCancelable(false)
.setPositiveButton(getString(android.R.string.ok), new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
float speed = PodcastPlaybackService.PLAYBACK_SPEEDS[numberPicker.getValue()];
//Toast.makeText(getActivity(), String.valueOf(speed]), Toast.LENGTH_SHORT).show();
eventBus.post(new SpeedPodcast(speed));
dialog.cancel();
}
})
.setNegativeButton(getString(android.R.string.cancel), new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
dialog.cancel();
}
.setPositiveButton(getString(android.R.string.ok), (dialog, id) -> {
float speed = PodcastPlaybackService.PLAYBACK_SPEEDS[numberPicker.getValue()];
eventBus.post(new SpeedPodcast(speed));
dialog.cancel();
})
.setNegativeButton(getString(android.R.string.cancel), (dialog, id) -> dialog.cancel())
.setView(numberPicker);
// create alert dialog
@ -501,4 +407,232 @@ public class PodcastFragment extends Fragment {
e.printStackTrace();
}
}
private MediaControllerCompat.Callback controllerCallback =
new MediaControllerCompat.Callback() {
@Override
public void onMetadataChanged(MediaMetadataCompat metadata) {
Log.v(TAG, "onMetadataChanged() called with: metadata = [" + metadata + "]");
displayMetadata(metadata);
}
@Override
public void onPlaybackStateChanged(PlaybackStateCompat stateCompat) {
Log.v(TAG, "onPlaybackStateChanged() called with: state = [" + stateCompat + "]");
displayPlaybackState(stateCompat);
}
};
private void displayMetadata(MediaMetadataCompat metadata) {
String title = metadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE);
String author = metadata.getString(MediaMetadataCompat.METADATA_KEY_ARTIST);
if(author != null) {
title += " - " + author;
}
tvTitle.setText(title);
tvTitleSlider.setText(title);
String favIconUrl = metadata.getString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI);
if(favIconUrl != null) {
Log.d(TAG, "currentPlayingPodcastReceived: " + favIconUrl);
DisplayImageOptions displayImageOptions = new DisplayImageOptions.Builder().
showImageOnLoading(R.drawable.default_feed_icon_light).
showImageForEmptyUri(R.drawable.default_feed_icon_light).
showImageOnFail(R.drawable.default_feed_icon_light).
build();
ImageLoader.getInstance().displayImage(favIconUrl, imgFavIcon, displayImageOptions);
}
PlaybackService.VideoType mediaType = PlaybackService.VideoType.valueOf(metadata.getString(CURRENT_PODCAST_MEDIA_TYPE));
if("-1".equals(metadata.getString(METADATA_KEY_MEDIA_ID))) {
// Collapse if no podcast is loaded
eventBus.post(new CollapsePodcastView());
} else {
// Expand if podcast is loaded
eventBus.post(new ExpandPodcastView());
if (mediaType == PlaybackService.VideoType.Video) {
Log.v(TAG, "init regular video");
tryOpeningPictureinPictureMode();
}
}
maxPositionInMillis = metadata.getLong(MediaMetadataCompat.METADATA_KEY_DURATION);
}
private void displayPlaybackState(PlaybackStateCompat stateCompat) {
boolean showPlayingButton = false;
int state = stateCompat.getState();
if(PlaybackStateCompat.STATE_PLAYING == state ||
PlaybackStateCompat.STATE_BUFFERING == state ||
PlaybackStateCompat.STATE_CONNECTING == state ||
PlaybackStateCompat.STATE_PAUSED == state) {
//Log.v(TAG, "State is: " + state);
if (PlaybackStateCompat.STATE_PAUSED != state) {
showPlayingButton = true;
}
}
int drawableId = showPlayingButton ? R.drawable.ic_action_pause : R.drawable.ic_action_play;
int contentDescriptionId = showPlayingButton ? R.string.content_desc_pause : R.string.content_desc_play;
// If attached to context..
if(mActivity != null) {
btnPlayPausePodcast.setImageResource(drawableId);
btnPlayPausePodcast.setContentDescription(getString(contentDescriptionId));
btnPlayPausePodcastSlider.setImageResource(drawableId);
}
currentPositionInMillis = stateCompat.getPosition();
updateProgressBar(state);
}
private void updateProgressBar(@PlaybackStateCompat.State int state) {
int hours = (int)(currentPositionInMillis / (1000*60*60));
int minutes = (int)(currentPositionInMillis % (1000*60*60)) / (1000*60);
int seconds = (int) ((currentPositionInMillis % (1000*60*60)) % (1000*60) / 1000);
minutes += hours * 60;
tvFrom.setText(String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds));
tvFromSlider.setText(String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds));
hours = (int)(maxPositionInMillis / (1000*60*60));
minutes = (int)(maxPositionInMillis % (1000*60*60)) / (1000*60);
seconds = (int) ((maxPositionInMillis % (1000*60*60)) % (1000*60) / 1000);
minutes += hours * 60;
tvTo.setText(String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds));
tvToSlider.setText(String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds));
if(state == PlaybackStateCompat.STATE_CONNECTING) {
sb_progress.setVisibility(View.INVISIBLE);
pb_progress2.setVisibility(View.VISIBLE);
pb_progress.setIndeterminate(true);
} else {
double progress = ((double) currentPositionInMillis / (double) maxPositionInMillis) * 100d;
if(!blockSeekbarUpdate) {
sb_progress.setVisibility(View.VISIBLE);
pb_progress2.setVisibility(View.INVISIBLE);
sb_progress.setProgress((int) progress);
}
pb_progress.setIndeterminate(false);
pb_progress.setProgress((int) progress);
}
}
// https://developer.android.com/guide/topics/media-apps/audio-app/building-a-mediabrowser-client#customize-mediabrowser-connectioncallback
private final MediaBrowserCompat.ConnectionCallback mConnectionCallbacks =
new MediaBrowserCompat.ConnectionCallback() {
@Override
public void onConnected() {
Log.d(TAG, "onConnected() called");
// Get the token for the MediaSession
MediaSessionCompat.Token token = mMediaBrowser.getSessionToken();
try {
// Create a MediaControllerCompat
MediaControllerCompat mediaController = new MediaControllerCompat(mActivity, token);
// Save the controller
MediaControllerCompat.setMediaController(mActivity, mediaController);
// Register a Callback to stay in sync
mediaController.registerCallback(controllerCallback);
// Display the initial state
MediaMetadataCompat metadata = mediaController.getMetadata();
PlaybackStateCompat pbState = mediaController.getPlaybackState();
displayMetadata(metadata);
displayPlaybackState(pbState);
// Finish building the UI
//buildTransportControls();
} catch (RemoteException e) {
Log.e(TAG, "Connecting to podcast service failed!", e);
}
}
@Override
public void onConnectionSuspended() {
Log.d(TAG, "onConnectionSuspended() called");
// The Service has crashed. Disable transport controls until it automatically reconnects
}
@Override
public void onConnectionFailed() {
Log.e(TAG, "onConnectionFailed() called");
// The Service has refused our connection
}
};
public void getCurrentPlaybackSpeed(final OnPlaybackSpeedCallback callback) {
MediaControllerCompat.getMediaController(mActivity)
.sendCommand(PLAYBACK_SPEED_FLOAT,
null,
new ResultReceiver(new Handler()) {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
callback.currentPlaybackReceived(resultData.getFloat(PLAYBACK_SPEED_FLOAT));
}
});
}
/*
public boolean getCurrentPlayingPodcast(final OnCurrentPlayingPodcastCallback callback) {
if(mMediaBrowser != null && mMediaBrowser.isConnected()) {
MediaControllerCompat.getMediaController(mActivity)
.sendCommand(CURRENT_PODCAST_ITEM_MEDIA_ITEM,
null,
new ResultReceiver(new Handler()) {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
callback.currentPlayingPodcastReceived((MediaItem) resultData.getSerializable(CURRENT_PODCAST_ITEM_MEDIA_ITEM));
}
});
return true;
} else {
return false;
}
}
*/
private MediaControllerCompat.Callback mediaControllerCallback = new MediaControllerCompat.Callback() {
@Override
public void onSessionReady() {
Log.d(TAG, "onSessionReady() called");
super.onSessionReady();
}
@Override
public void onSessionDestroyed() {
Log.d(TAG, "onSessionDestroyed() called");
super.onSessionDestroyed();
}
@Override
public void onSessionEvent(String event, Bundle extras) {
Log.d(TAG, "onSessionEvent() called with: event = [" + event + "], extras = [" + extras + "]");
super.onSessionEvent(event, extras);
}
};
public interface OnPlaybackSpeedCallback {
void currentPlaybackReceived(float playbackSpeed);
}
/*
public interface OnCurrentPlayingPodcastCallback {
void currentPlayingPodcastReceived(MediaItem mediaItem);
}*/
}

View file

@ -1,28 +1,21 @@
package de.luhmer.owncloudnewsreader;
import android.animation.Animator;
import android.content.ComponentName;
import android.content.DialogInterface;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.util.Log;
import android.util.TypedValue;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.animation.Animation;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import com.nextcloud.android.sso.helper.VersionCheckHelper;
import com.sothree.slidinguppanel.SlidingUpPanelLayout;
@ -31,27 +24,21 @@ import org.greenrobot.eventbus.Subscribe;
import java.io.File;
import java.lang.reflect.Proxy;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.view.GravityCompat;
import butterknife.BindView;
import butterknife.ButterKnife;
import de.luhmer.owncloudnewsreader.ListView.SubscriptionExpandableListAdapter;
import de.luhmer.owncloudnewsreader.database.DatabaseConnectionOrm;
import de.luhmer.owncloudnewsreader.database.model.RssItem;
import de.luhmer.owncloudnewsreader.di.ApiProvider;
import de.luhmer.owncloudnewsreader.events.podcast.CollapsePodcastView;
import de.luhmer.owncloudnewsreader.events.podcast.ExpandPodcastView;
import de.luhmer.owncloudnewsreader.events.podcast.PodcastCompletedEvent;
import de.luhmer.owncloudnewsreader.events.podcast.RegisterVideoOutput;
import de.luhmer.owncloudnewsreader.events.podcast.RegisterYoutubeOutput;
import de.luhmer.owncloudnewsreader.events.podcast.UpdatePodcastStatusEvent;
import de.luhmer.owncloudnewsreader.events.podcast.VideoDoubleClicked;
import de.luhmer.owncloudnewsreader.helper.PostDelayHandler;
import de.luhmer.owncloudnewsreader.helper.SizeAnimator;
import de.luhmer.owncloudnewsreader.helper.ThemeChooser;
import de.luhmer.owncloudnewsreader.interfaces.IPlayPausePodcastClicked;
import de.luhmer.owncloudnewsreader.model.MediaItem;
@ -59,15 +46,11 @@ import de.luhmer.owncloudnewsreader.model.PodcastItem;
import de.luhmer.owncloudnewsreader.notification.NextcloudNotificationManager;
import de.luhmer.owncloudnewsreader.services.PodcastDownloadService;
import de.luhmer.owncloudnewsreader.services.PodcastPlaybackService;
import de.luhmer.owncloudnewsreader.services.podcast.PlaybackService;
import de.luhmer.owncloudnewsreader.ssl.MemorizingTrustManager;
import de.luhmer.owncloudnewsreader.view.PodcastSlidingUpPanelLayout;
import de.luhmer.owncloudnewsreader.view.ZoomableRelativeLayout;
import de.luhmer.owncloudnewsreader.widget.WidgetProvider;
import static de.luhmer.owncloudnewsreader.Constants.MIN_NEXTCLOUD_FILES_APP_VERSION_CODE;
import static de.luhmer.owncloudnewsreader.services.PodcastPlaybackService.CURRENT_PODCAST_ITEM_MEDIA_ITEM;
import static de.luhmer.owncloudnewsreader.services.PodcastPlaybackService.PLAYBACK_SPEED_FLOAT;
public class PodcastFragmentActivity extends AppCompatActivity implements IPlayPausePodcastClicked {
@ -78,32 +61,17 @@ public class PodcastFragmentActivity extends AppCompatActivity implements IPlayP
@Inject protected MemorizingTrustManager mMTM;
@Inject protected PostDelayHandler mPostDelayHandler;
private MediaBrowserCompat mMediaBrowser;
private EventBus eventBus;
private PodcastFragment mPodcastFragment;
private int appHeight;
private int appWidth;
@BindView(R.id.videoPodcastSurfaceWrapper)
protected ZoomableRelativeLayout rlVideoPodcastSurfaceWrapper;
@BindView(R.id.sliding_layout)
protected PodcastSlidingUpPanelLayout sliding_layout;
//YouTubePlayerFragment youtubeplayerfragment;
private boolean currentlyPlaying = false;
private boolean showedYoutubeFeatureNotAvailableDialog = false;
private boolean videoViewInitialized = false;
private boolean isVideoViewVisible = true;
private static final int animationTime = 300; //Milliseconds
private boolean isFullScreen = false;
private float scaleFactor = 1;
private boolean useAnimation = false;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
//Log.v(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
((NewsReaderApplication) getApplication()).getAppComponent().injectActivity(this);
ThemeChooser.chooseTheme(this);
@ -117,6 +85,18 @@ public class PodcastFragmentActivity extends AppCompatActivity implements IPlayP
mPostDelayHandler.stopRunningPostDelayHandler();
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
//Log.v(TAG, "onPostCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
super.onPostCreate(savedInstanceState);
eventBus = EventBus.getDefault();
ButterKnife.bind(this);
updatePodcastView();
}
@Override
protected void onStart() {
super.onStart();
@ -124,50 +104,11 @@ public class PodcastFragmentActivity extends AppCompatActivity implements IPlayP
mMTM.bindDisplayActivity(this);
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
eventBus = EventBus.getDefault();
ButterKnife.bind(this);
//youtubeplayerfragment = (YouTubePlayerFragment)getFragmentManager().findFragmentById(R.id.youtubeplayerfragment);
ViewTreeObserver vto = rlVideoPodcastSurfaceWrapper.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
rlVideoPodcastSurfaceWrapper.readVideoPosition();
rlVideoPodcastSurfaceWrapper.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
rlVideoPodcastSurfaceWrapper.setVisibility(View.INVISIBLE);
updatePodcastView();
/*
if (isMyServiceRunning(PodcastPlaybackService.class, this)) {
Intent intent = new Intent(this, PodcastPlaybackService.class);
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}
*/
mMediaBrowser = new MediaBrowserCompat(this,
new ComponentName(this, PodcastPlaybackService.class),
mConnectionCallbacks,
null); // optional Bundle
super.onPostCreate(savedInstanceState);
}
@Override
protected void onStop() {
mMTM.unbindDisplayActivity(this);
super.onStop();
mMediaBrowser.disconnect();
}
@ -197,33 +138,25 @@ public class PodcastFragmentActivity extends AppCompatActivity implements IPlayP
protected void onResume() {
eventBus.register(this);
if (mMediaBrowser != null && !mMediaBrowser.isConnected()) {
sliding_layout.setPanelHeight(0);
sliding_layout.setPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED);
}
super.onResume();
}
@Override
protected void onPause() {
Log.d(TAG, "onPause");
eventBus.unregister(this);
//TODO THIS IS NEVER REACHED!
/*
isVideoViewVisible = false;
videoViewInitialized = false;
eventBus.post(new RegisterVideoOutput(null, null));
eventBus.post(new RegisterYoutubeOutput(null, false));
rlVideoPodcastSurfaceWrapper.setVisibility(View.GONE);
rlVideoPodcastSurfaceWrapper.removeAllViews();
*/
WidgetProvider.UpdateWidget(this);
if (NextcloudNotificationManager.isUnreadRssCountNotificationVisible(this)) {
DatabaseConnectionOrm dbConn = new DatabaseConnectionOrm(this);
int count = Integer.parseInt(dbConn.getUnreadItemsCountForSpecificFolder(SubscriptionExpandableListAdapter.SPECIAL_FOLDERS.ALL_UNREAD_ITEMS));
@ -251,41 +184,7 @@ public class PodcastFragmentActivity extends AppCompatActivity implements IPlayP
}
*/
private final MediaBrowserCompat.ConnectionCallback mConnectionCallbacks =
new MediaBrowserCompat.ConnectionCallback() {
@Override
public void onConnected() {
Log.d(TAG, "onConnected() called");
// Get the token for the MediaSession
MediaSessionCompat.Token token = mMediaBrowser.getSessionToken();
try {
// Create a MediaControllerCompat
MediaControllerCompat mediaController = new MediaControllerCompat(PodcastFragmentActivity.this, token);
// Save the controller
MediaControllerCompat.setMediaController(PodcastFragmentActivity.this, mediaController);
// Finish building the UI
//buildTransportControls();
} catch (RemoteException e) {
Log.e(TAG, "Connecting to podcast service failed!", e);
}
}
@Override
public void onConnectionSuspended() {
Log.d(TAG, "onConnectionSuspended() called");
// The Service has crashed. Disable transport controls until it automatically reconnects
}
@Override
public void onConnectionFailed() {
Log.e(TAG, "onConnectionFailed() called");
// The Service has refused our connection
}
};
/*
private void buildTransportControls() {
@ -306,15 +205,6 @@ public class PodcastFragmentActivity extends AppCompatActivity implements IPlayP
// Register a Callback to stay in sync
mediaController.registerCallback(controllerCallback);
}
MediaControllerCompat.Callback controllerCallback =
new MediaControllerCompat.Callback() {
@Override
public void onMetadataChanged(MediaMetadataCompat metadata) {}
@Override
public void onPlaybackStateChanged(PlaybackStateCompat state) {}
};
*/
public PodcastSlidingUpPanelLayout getSlidingLayout() {
@ -323,301 +213,62 @@ public class PodcastFragmentActivity extends AppCompatActivity implements IPlayP
public boolean handlePodcastBackPressed() {
if(mPodcastFragment != null && sliding_layout.getPanelState().equals(SlidingUpPanelLayout.PanelState.EXPANDED)) {
if (!mPodcastFragment.onBackPressed())
sliding_layout.setPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED);
sliding_layout.setPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED);
return true;
}
return false;
}
protected void updatePodcastView() {
if(mPodcastFragment == null) {
mPodcastFragment = PodcastFragment.newInstance();
}
/*
if(mPodcastFragment != null) {
getSupportFragmentManager().beginTransaction().remove(mPodcastFragment).commitAllowingStateLoss();
}
*/
mPodcastFragment = PodcastFragment.newInstance();
getSupportFragmentManager().beginTransaction()
.replace(R.id.podcast_frame, mPodcastFragment)
.commitAllowingStateLoss();
if(!currentlyPlaying)
sliding_layout.setPanelHeight(0);
collapsePodcastView();
}
@Subscribe
public void onEvent(CollapsePodcastView event) {
Log.v(TAG, "onEvent(CollapsePodcastView) called with: event = [" + event + "]");
collapsePodcastView();
}
@Subscribe
public void onEvent(UpdatePodcastStatusEvent podcast) {
boolean playStateChanged = currentlyPlaying;
//If file is loaded or preparing and podcast is paused/not running expand the view
currentlyPlaying = podcast.getStatus() == PlaybackService.Status.PLAYING
|| podcast.getStatus() == PlaybackService.Status.PAUSED;
//Check if state was changed
playStateChanged = playStateChanged != currentlyPlaying;
// If preparing or state changed and is now playing or paused
if(podcast.getStatus() == PlaybackService.Status.PREPARING
|| (playStateChanged
&& (podcast.getStatus() == PlaybackService.Status.PLAYING
|| podcast.getStatus() == PlaybackService.Status.PAUSED
|| podcast.getStatus() == PlaybackService.Status.STOPPED))) {
//Expand view
sliding_layout.setPanelHeight((int) dipToPx(68));
Log.v(TAG, "expanding podcast view!");
} else if(playStateChanged) {
//Hide view
sliding_layout.setPanelHeight(0);
currentlyPlaying = false;
Log.v(TAG, "collapsing podcast view!");
}
if (podcast.isVideoFile() && podcast.getVideoType() == PlaybackService.VideoType.Video) {
if ((!isVideoViewVisible || !videoViewInitialized) && rlVideoPodcastSurfaceWrapper.isPositionReady()) {
rlVideoPodcastSurfaceWrapper.removeAllViews();
videoViewInitialized = true;
isVideoViewVisible = true;
rlVideoPodcastSurfaceWrapper.setVisibility(View.VISIBLE);
//AlphaAnimator.AnimateVisibilityChange(rlVideoPodcastSurfaceWrapper, View.VISIBLE);
public void onEvent(ExpandPodcastView event) {
Log.v(TAG, "onEvent(ExpandPodcastView) called with: event = [" + event + "]");
expandPodcastView();
}
SurfaceView surfaceView = new SurfaceView(this);
surfaceView.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT));
rlVideoPodcastSurfaceWrapper.addView(surfaceView);
eventBus.post(new RegisterVideoOutput(surfaceView, rlVideoPodcastSurfaceWrapper));
togglePodcastVideoViewAnimation();
}
} else if(podcast.getVideoType() == PlaybackService.VideoType.YouTube) {
if(BuildConfig.FLAVOR.equals("extra")) {
if (!videoViewInitialized) {
isVideoViewVisible = true;
videoViewInitialized = true;
rlVideoPodcastSurfaceWrapper.removeAllViews();
rlVideoPodcastSurfaceWrapper.setVisibility(View.VISIBLE);
togglePodcastVideoViewAnimation();
final int YOUTUBE_CONTENT_VIEW_ID = 10101010;
FrameLayout frame = new FrameLayout(this);
frame.setId(YOUTUBE_CONTENT_VIEW_ID);
rlVideoPodcastSurfaceWrapper.addView(frame);
//setContentView(frame, new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
YoutubePlayerManager.StartYoutubePlayer(this, YOUTUBE_CONTENT_VIEW_ID, eventBus, new Runnable() {
@Override
public void run() {
togglePodcastVideoViewAnimation();
}
});
}
} else if(!showedYoutubeFeatureNotAvailableDialog) {
showedYoutubeFeatureNotAvailableDialog = true;
new AlertDialog.Builder(this)
.setTitle(getString(R.string.warning))
.setMessage(R.string.dialog_feature_not_available)
.setCancelable(true)
.setPositiveButton(getString(android.R.string.ok), null)
.show();
}
} else {
isVideoViewVisible = false;
videoViewInitialized = false;
eventBus.post(new RegisterVideoOutput(null, null));
eventBus.post(new RegisterYoutubeOutput(null, false));
rlVideoPodcastSurfaceWrapper.setVisibility(View.GONE);
//AlphaAnimator.AnimateVisibilityChange(rlVideoPodcastSurfaceWrapper, View.GONE);
rlVideoPodcastSurfaceWrapper.removeAllViews();
}
private void collapsePodcastView() {
sliding_layout.setPanelHeight(0);
}
private void expandPodcastView() {
sliding_layout.setPanelHeight((int) dipToPx(68));
}
@Subscribe
public void onEvent(PodcastCompletedEvent podcastCompletedEvent) {
sliding_layout.setPanelHeight(0);
collapsePodcastView();
sliding_layout.setPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED);
currentlyPlaying = false;
//currentlyPlaying = false;
}
@Subscribe
public void onEvent(VideoDoubleClicked videoDoubleClicked) {
appHeight = getWindow().getDecorView().findViewById(android.R.id.content).getHeight();
appWidth = getWindow().getDecorView().findViewById(android.R.id.content).getWidth();
if(isFullScreen) {
rlVideoPodcastSurfaceWrapper.setDisableScale(false);
togglePodcastVideoViewAnimation();
//showSystemUI();
} else {
//hideSystemUI();
rlVideoPodcastSurfaceWrapper.setDisableScale(true);
//oldScaleFactor = rlVideoPodcastSurfaceWrapper.getScaleFactor();
final View view = rlVideoPodcastSurfaceWrapper;
final float oldHeight = view.getLayoutParams().height;
final float oldWidth = view.getLayoutParams().width;
//view.setPivotX(oldWidth/2);
//view.setPivotY(oldHeight/2);
/*
Display display = getWindowManager().getDefaultDisplay();
float width = display.getWidth(); // deprecated
float height = display.getHeight(); // deprecated
*/
scaleFactor = appWidth / (float) view.getWidth();
float newHeightTemp = oldHeight * scaleFactor;
float newWidthTemp = oldWidth * scaleFactor;
//view.animate().scaleX(scaleFactor).scaleY(scaleFactor).setDuration(100);
//scaleView(view, 1f, scaleFactor, 1f, scaleFactor);
if(newHeightTemp > appHeight) { //Could happen on Tablets or in Landscape Mode
scaleFactor = appHeight / (float) view.getHeight();
newHeightTemp = oldHeight * scaleFactor;
newWidthTemp = oldWidth * scaleFactor;
}
final float newHeight = newHeightTemp;
final float newWidth = newWidthTemp;
float newXPosition = rlVideoPodcastSurfaceWrapper.getVideoXPosition() + (int) getResources().getDimension(R.dimen.activity_vertical_margin);// (appWidth / 2) + dipToPx(10);
float newYPosition = (appHeight/2) + ((newHeight/2) - oldHeight);
useAnimation = true;
view.animate().x(newXPosition).y(newYPosition).setDuration(animationTime).setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
if(useAnimation) {
view.startAnimation(new SizeAnimator(view, newWidth, newHeight, oldWidth, oldHeight, animationTime).sizeAnimator);
}
useAnimation = false;
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
//rlVideoPodcastSurfaceWrapper.animate().scaleX(scaleFactor).scaleY(scaleFactor).x(newXPosition).y(newYPosition).setDuration(500).setListener(onResizeListener);
//surfaceView.animate().scaleX(scaleFactor).scaleY(scaleFactor).setDuration(500);
//oldScaleFactor
//scaleFactor = dipToPx(oldWidth) / newWidth;
//scaleFactor = (1/oldScaleFactor) * dipToPx(oldWidth) / newWidth;
//scaleFactor = oldWidth / newWidth;
scaleFactor = 1/scaleFactor;
}
isFullScreen = !isFullScreen;
}
public void togglePodcastVideoViewAnimation() {
boolean isLeftSliderOpen = false;
if(this instanceof NewsReaderListActivity && ((NewsReaderListActivity) this).drawerLayout != null) {
isLeftSliderOpen = ((NewsReaderListActivity) this).drawerLayout.isDrawerOpen(GravityCompat.START);
}
int podcastMediaControlHeightDp = pxToDp((int) getResources().getDimension(R.dimen.podcast_media_control_height));
if(sliding_layout.getPanelState().equals(SlidingUpPanelLayout.PanelState.EXPANDED)) { //On Tablets
animateToPosition(podcastMediaControlHeightDp);
} else if(isLeftSliderOpen) {
animateToPosition(0);
} else {
animateToPosition(64);
}
}
public static int pxToDp(int px)
{
public static int pxToDp(int px) {
return (int) (px / Resources.getSystem().getDisplayMetrics().density);
}
public void animateToPosition(final int yPosition) {
appHeight = getWindow().getDecorView().findViewById(android.R.id.content).getHeight();
appWidth = getWindow().getDecorView().findViewById(android.R.id.content).getWidth();
final View view = rlVideoPodcastSurfaceWrapper; //surfaceView
if(scaleFactor != 1) {
int oldHeight = view.getLayoutParams().height;
int oldWidth = view.getLayoutParams().width;
int newHeight = view.getLayoutParams().height *= scaleFactor;
int newWidth = view.getLayoutParams().width *= scaleFactor;
scaleFactor = 1;
Animation animator = new SizeAnimator(view, newWidth, newHeight, oldWidth, oldHeight, animationTime).sizeAnimator;
animator.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
animateToPosition(yPosition);
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
view.startAnimation(animator);
} else {
int absoluteYPosition = appHeight - view.getHeight() - (int) getResources().getDimension(R.dimen.activity_vertical_margin) - (int) dipToPx(yPosition);
float xPosition = rlVideoPodcastSurfaceWrapper.getVideoXPosition();
//TODO podcast video is only working for newer android versions
view.animate().x(xPosition).y(absoluteYPosition).setDuration(animationTime);
}
/*
int height = (int)(view.getHeight() * scaleFactor);
int width = (int)(view.getWidth() * scaleFactor);
view.setScaleX(oldScaleFactor);
view.setScaleY(oldScaleFactor);
view.getLayoutParams().height = height;
view.getLayoutParams().width = width;
*/
}
private float dipToPx(float dip) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip, getResources().getDisplayMetrics());
}
@ -628,9 +279,13 @@ public class PodcastFragmentActivity extends AppCompatActivity implements IPlayP
intent.putExtra(PodcastPlaybackService.MEDIA_ITEM, mediaItem);
startService(intent);
/*
if(!mMediaBrowser.isConnected()) {
mMediaBrowser.connect();
}
*/
//bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}
@ -642,76 +297,58 @@ public class PodcastFragmentActivity extends AppCompatActivity implements IPlayP
File file = new File(PodcastDownloadService.getUrlToPodcastFile(this, podcastItem.link, false));
if(file.exists()) {
podcastItem.link = file.getAbsolutePath();
openMediaItem(podcastItem);
} else if(!podcastItem.offlineCached) {
AlertDialog.Builder alertDialog = new AlertDialog.Builder(this)
.setNegativeButton("Abort", null)
.setNeutralButton("Download", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
PodcastDownloadService.startPodcastDownload(PodcastFragmentActivity.this, podcastItem);
.setTitle("Podcast");
Toast.makeText(PodcastFragmentActivity.this, "Starting download of podcast. Please wait..", Toast.LENGTH_SHORT).show();
}
})
.setTitle("Podcast")
.setMessage("Choose if you want to download or stream the selected podcast");
alertDialog.setPositiveButton("Stream", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
openMediaItem(podcastItem);
}
});
if("youtube".equals(podcastItem.mimeType)) {
alertDialog.setPositiveButton("Open Youtube", (dialogInterface, i) -> openYoutube(podcastItem));
} else {
alertDialog.setNeutralButton("Download", (dialogInterface, i) -> {
PodcastDownloadService.startPodcastDownload(PodcastFragmentActivity.this, podcastItem);
Toast.makeText(PodcastFragmentActivity.this, "Starting download of podcast. Please wait..", Toast.LENGTH_SHORT).show();
});
alertDialog.setPositiveButton("Stream", (dialogInterface, i) -> openMediaItem(podcastItem));
alertDialog.setMessage("Choose if you want to download or stream the selected podcast");
}
alertDialog.show();
}
}
@Override
public void pausePodcast() {
MediaControllerCompat.getMediaController(PodcastFragmentActivity.this).getTransportControls().pause();
}
public void getCurrentPlaybackSpeed(final OnPlaybackSpeedCallback callback) {
MediaControllerCompat.getMediaController(PodcastFragmentActivity.this)
.sendCommand(PLAYBACK_SPEED_FLOAT,
null,
new ResultReceiver(new Handler()) {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
callback.currentPlaybackReceived(resultData.getFloat(PLAYBACK_SPEED_FLOAT));
}
});
}
public boolean getCurrentPlayingPodcast(final OnCurrentPlayingPodcastCallback callback) {
if(mMediaBrowser != null && mMediaBrowser.isConnected()) {
MediaControllerCompat.getMediaController(PodcastFragmentActivity.this)
.sendCommand(CURRENT_PODCAST_ITEM_MEDIA_ITEM,
null,
new ResultReceiver(new Handler()) {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
callback.currentPlayingPodcastReceived((MediaItem) resultData.getSerializable(CURRENT_PODCAST_ITEM_MEDIA_ITEM));
}
});
return true;
} else {
return false;
private void openYoutube(PodcastItem podcastItem) {
Log.e(TAG, podcastItem.link);
String youtubeVideoID = getVideoIdFromYoutubeUrl(podcastItem.link);
if(youtubeVideoID == null) {
Toast.makeText(this, "Failed to extract youtube video id for url: " + podcastItem.link + ". Please report this issue.", Toast.LENGTH_LONG).show();
return;
}
Intent appIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("vnd.youtube:" + youtubeVideoID));
Intent webIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.youtube.com/watch?v=" + podcastItem.link));
try {
startActivity(appIntent);
} catch (ActivityNotFoundException ex) {
startActivity(webIntent);
}
}
public interface OnPlaybackSpeedCallback {
void currentPlaybackReceived(float playbackSpeed);
public String getVideoIdFromYoutubeUrl(String url){
String videoId = null;
String regex = "http(?:s)?:\\/\\/(?:m.)?(?:www\\.)?youtu(?:\\.be\\/|be\\.com\\/(?:watch\\?(?:feature=youtu.be\\&)?v=|v\\/|embed\\/|user\\/(?:[\\w#]+\\/)+))([^&#?\\n]+)";
Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(url);
if(matcher.find()){
videoId = matcher.group(1);
}
return videoId;
}
public interface OnCurrentPlayingPodcastCallback {
void currentPlayingPodcastReceived(MediaItem mediaItem);
}
}

View file

@ -2,29 +2,27 @@ package de.luhmer.owncloudnewsreader.adapter;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.fragment.app.FragmentActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import androidx.fragment.app.FragmentActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import de.luhmer.owncloudnewsreader.NewsReaderListActivity;
import de.luhmer.owncloudnewsreader.R;
import de.luhmer.owncloudnewsreader.SettingsActivity;
import de.luhmer.owncloudnewsreader.database.DatabaseConnectionOrm;
import de.luhmer.owncloudnewsreader.database.model.RssItem;
import de.luhmer.owncloudnewsreader.events.podcast.PodcastCompletedEvent;
import de.luhmer.owncloudnewsreader.events.podcast.UpdatePodcastStatusEvent;
import de.luhmer.owncloudnewsreader.helper.AsyncTaskHelper;
import de.luhmer.owncloudnewsreader.helper.PostDelayHandler;
import de.luhmer.owncloudnewsreader.helper.StopWatch;
@ -122,6 +120,9 @@ public class NewsListRecyclerAdapter extends RecyclerView.Adapter {
this.cachedPages = cachedPages;
}
/*
// TODO right now this is not working anymore.. We need to use the MediaSession here..
// Not sure if this is the cleanest solution though..
@Subscribe
public void onEvent(UpdatePodcastStatusEvent podcast) {
if (podcast.isPlaying()) {
@ -138,6 +139,7 @@ public class NewsListRecyclerAdapter extends RecyclerView.Adapter {
Log.v(TAG, "Updating Listview - Podcast paused");
}
}
*/
@Subscribe
public void onEvent(PodcastCompletedEvent podcastCompletedEvent) {
@ -182,26 +184,33 @@ public class NewsListRecyclerAdapter extends RecyclerView.Adapter {
final ViewHolder holder = new ViewHolder(view, mPrefs);
holder.starImageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
toggleStarredStateOfItem(holder);
}
});
holder.starImageView.setOnClickListener(view1 -> toggleStarredStateOfItem(holder));
holder.setClickListener((RecyclerItemClickListener) activity);
holder.flPlayPausePodcastWrapper.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (holder.isPlaying()) {
playPausePodcastClicked.pausePodcast();
} else {
playPausePodcastClicked.openPodcast(holder.getRssItem());
}
holder.flPlayPausePodcastWrapper.setOnClickListener(v -> {
if (holder.isPlaying()) {
playPausePodcastClicked.pausePodcast();
} else {
playPausePodcastClicked.openPodcast(holder.getRssItem());
}
});
/*
// TODO implement option to delete cached podcasts (https://github.com/nextcloud/news-android/issues/742)
holder.flPlayPausePodcastWrapper.setOnLongClickListener(v -> {
// TODO check if cached..
new AlertDialog.Builder(activity)
.setTitle("")
.setMessage("")
.setPositiveButton("", (dialog, which) -> {})
.setNegativeButton("", (dialog, which) -> {})
.create()
.show();
return false;
});
*/
return holder;
}
}

View file

@ -226,7 +226,7 @@ public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickL
public void setPlaying(boolean playing) {
this.playing = playing;
int imageId = playing ? R.drawable.ic_action_pause : R.drawable.ic_action_play_arrow;
int imageId = playing ? R.drawable.ic_action_pause : R.drawable.ic_action_play;
int contentDescriptionId = playing ? R.string.content_desc_pause : R.string.content_desc_play;
String contentDescription = btnPlayPausePodcast.getContext().getString(contentDescriptionId);

View file

@ -13,9 +13,13 @@ import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import de.greenrobot.dao.query.LazyList;
import de.greenrobot.dao.query.WhereCondition;
import de.luhmer.owncloudnewsreader.Constants;
import de.luhmer.owncloudnewsreader.NewsReaderApplication;
import de.luhmer.owncloudnewsreader.database.model.CurrentRssItemViewDao;
import de.luhmer.owncloudnewsreader.database.model.DaoSession;
import de.luhmer.owncloudnewsreader.database.model.Feed;
@ -52,13 +56,16 @@ public class DatabaseConnectionOrm {
};
private final String TAG = getClass().getCanonicalName();
private static final String[] VIDEO_FORMATS = { "youtube", "video/mp4" };
//private static final String[] VIDEO_FORMATS = { "youtube", "video/mp4" };
private static final String[] VIDEO_FORMATS = { "video/mp4" };
public enum SORT_DIRECTION { asc, desc }
private DaoSession daoSession;
private final static int PageSize = 100;
protected @Inject @Named("databaseFileName") String databasePath;
public void resetDatabase() {
daoSession.getRssItemDao().deleteAll();
daoSession.getFeedDao().deleteAll();
@ -67,7 +74,10 @@ public class DatabaseConnectionOrm {
}
public DatabaseConnectionOrm(Context context) {
daoSession = DatabaseHelperOrm.getDaoSession(context);
if(databasePath == null) {
((NewsReaderApplication) context.getApplicationContext()).getAppComponent().injectDatabaseConnection(this);
}
daoSession = DatabaseHelperOrm.getDaoSession(context, databasePath);
}
/*
@ -463,14 +473,15 @@ public class DatabaseConnectionOrm {
public static PodcastItem ParsePodcastItemFromRssItem(Context context, RssItem rssItem) {
PodcastItem podcastItem = new PodcastItem();
Feed feed = rssItem.getFeed();
podcastItem.author = feed.getFeedTitle();// rssItem.getAuthor();
podcastItem.itemId = rssItem.getId();
podcastItem.title = rssItem.getTitle();
podcastItem.link = rssItem.getEnclosureLink();
podcastItem.mimeType = rssItem.getEnclosureMime();
podcastItem.favIcon = rssItem.getFeed().getFaviconUrl();
podcastItem.favIcon = feed.getFaviconUrl();
boolean isVideo = Arrays.asList(DatabaseConnectionOrm.VIDEO_FORMATS).contains(podcastItem.mimeType);
podcastItem.isVideoPodcast = isVideo;
podcastItem.isVideoPodcast = Arrays.asList(DatabaseConnectionOrm.VIDEO_FORMATS).contains(podcastItem.mimeType);
File file = new File(PodcastDownloadService.getUrlToPodcastFile(context, podcastItem.link, false));
podcastItem.offlineCached = file.exists();

View file

@ -28,11 +28,9 @@ import de.luhmer.owncloudnewsreader.database.model.DaoMaster;
import de.luhmer.owncloudnewsreader.database.model.DaoSession;
public class DatabaseHelperOrm {
public static final String DATABASE_NAME_ORM = "OwncloudNewsReaderOrm.db";
private volatile static DaoSession daoSession;
public static DaoSession getDaoSession(Context context) {
public static DaoSession getDaoSession(Context context, String DATABASE_NAME_ORM) {
if(daoSession == null) {
synchronized (DatabaseHelperOrm.class) {
if(daoSession == null) {

View file

@ -51,6 +51,14 @@ public class ApiModule {
return mApplication.getPackageName() + "_preferences";
}
// Dagger will only look for methods annotated with @Provides
@Provides
@Named("databaseFileName")
public String providesDatabaseFileName() {
//return PreferenceManager.getDefaultSharedPreferencesName(mApplication);
return "OwncloudNewsReaderOrm.db";
}
/*
@Provides
@Singleton

View file

@ -16,6 +16,7 @@ import de.luhmer.owncloudnewsreader.SettingsActivity;
import de.luhmer.owncloudnewsreader.SettingsFragment;
import de.luhmer.owncloudnewsreader.SyncIntervalSelectorActivity;
import de.luhmer.owncloudnewsreader.authentication.OwnCloudSyncAdapter;
import de.luhmer.owncloudnewsreader.database.DatabaseConnectionOrm;
import de.luhmer.owncloudnewsreader.services.SyncItemStateService;
import de.luhmer.owncloudnewsreader.widget.WidgetProvider;
@ -46,4 +47,6 @@ public interface AppComponent {
void injectService(OwnCloudSyncAdapter ownCloudSyncAdapter);
void injectWidget(WidgetProvider widgetProvider);
void injectDatabaseConnection(DatabaseConnectionOrm databaseConnectionOrm);
}

View file

@ -0,0 +1,4 @@
package de.luhmer.owncloudnewsreader.events.podcast;
public class CollapsePodcastView {
}

View file

@ -0,0 +1,4 @@
package de.luhmer.owncloudnewsreader.events.podcast;
public class ExpandPodcastView {
}

View file

@ -1,13 +0,0 @@
package de.luhmer.owncloudnewsreader.events.podcast;
public class RegisterYoutubeOutput {
public RegisterYoutubeOutput(Object youTubePlayer, boolean wasRestored) {
this.youTubePlayer = youTubePlayer;
this.wasRestored = wasRestored;
}
public Object youTubePlayer; // (Type: com.google.android.youtube.player.YouTubePlayer;)
public boolean wasRestored;
}

View file

@ -1,5 +1,7 @@
package de.luhmer.owncloudnewsreader.events.podcast;
import android.support.v4.media.session.PlaybackStateCompat;
import de.luhmer.owncloudnewsreader.services.podcast.PlaybackService;
public class UpdatePodcastStatusEvent {
@ -8,10 +10,10 @@ public class UpdatePodcastStatusEvent {
private long max;
private String author;
private String title;
private PlaybackService.Status status;
private @PlaybackStateCompat.State int status;
private PlaybackService.VideoType videoType;
private long rssItemId;
private float speed = -1;
private float speed;
public long getRssItemId() {
return rssItemId;
@ -25,12 +27,12 @@ public class UpdatePodcastStatusEvent {
return title;
}
public PlaybackService.Status getStatus() {
public @PlaybackStateCompat.State int getStatus() {
return status;
}
public boolean isPlaying() {
return status == PlaybackService.Status.PLAYING;
return status == PlaybackStateCompat.STATE_PLAYING;
}
public long getCurrent() {
@ -47,7 +49,7 @@ public class UpdatePodcastStatusEvent {
public boolean isVideoFile() { return !(videoType == PlaybackService.VideoType.None); }
public UpdatePodcastStatusEvent(long current, long max, PlaybackService.Status status, String author, String title, PlaybackService.VideoType videoType, long rssItemId, float speed) {
public UpdatePodcastStatusEvent(long current, long max, @PlaybackStateCompat.State int status, String author, String title, PlaybackService.VideoType videoType, long rssItemId, float speed) {
this.current = current;
this.max = max;
this.status = status;

View file

@ -2,6 +2,10 @@ package de.luhmer.owncloudnewsreader.events.podcast;
public class WindPodcast {
public double toPositionInPercent;
public double milliSeconds;
public WindPodcast(double milliSeconds) {
this.milliSeconds = milliSeconds;
}
}

View file

@ -0,0 +1,70 @@
package de.luhmer.owncloudnewsreader.helper;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.widget.SearchView;
import android.widget.TextView;
import java.lang.reflect.Field;
import androidx.annotation.ColorInt;
import androidx.core.content.ContextCompat;
import de.luhmer.owncloudnewsreader.R;
public class ThemeUtils {
private static final String TAG = ThemeUtils.class.getCanonicalName();
private ThemeUtils() {}
/**
* Sets the color of the SearchView to {@code color} (cursor.
* @param searchView
*/
public static void colorSearchViewCursorColor(SearchView searchView, @ColorInt int color) {
try {
Field searchTextViewRef = SearchView.class.getDeclaredField("mSearchSrcTextView");
searchTextViewRef.setAccessible(true);
Object searchAutoComplete = searchTextViewRef.get(searchView);
Field mCursorDrawableRes = TextView.class.getDeclaredField("mCursorDrawableRes");
mCursorDrawableRes.setAccessible(true);
mCursorDrawableRes.set(searchAutoComplete, R.drawable.cursor);
// Set color of handle
// https://stackoverflow.com/a/49555923
//get the pointer resource id
Field textSelectHandleRef = TextView.class.getDeclaredField("mTextSelectHandleRes");
textSelectHandleRef.setAccessible(true);
int drawableResId = textSelectHandleRef.getInt(searchAutoComplete);
//get the editor
Field editorRef = TextView.class.getDeclaredField("mEditor");
editorRef.setAccessible(true);
Object editor = editorRef.get(searchAutoComplete);
//tint drawable
Drawable drawable = ContextCompat.getDrawable(searchView.getContext(), drawableResId);
drawable.setColorFilter(color, PorterDuff.Mode.SRC_IN);
//set the drawable
Field mSelectHandleCenter = editor.getClass().getDeclaredField("mSelectHandleCenter");
mSelectHandleCenter.setAccessible(true);
mSelectHandleCenter.set(editor, drawable);
Field mSelectHandleLeft = editor.getClass().getDeclaredField("mSelectHandleLeft");
mSelectHandleLeft.setAccessible(true);
mSelectHandleLeft.set(editor, drawable);
Field mSelectHandleRight = editor.getClass().getDeclaredField("mSelectHandleRight");
mSelectHandleRight.setAccessible(true);
mSelectHandleRight.set(editor, drawable);
} catch (Exception e) {
Log.e(TAG, "Couldn't apply color to search view cursor", e);
}
}
}

View file

@ -27,7 +27,9 @@ public class PodcastItem extends MediaItem {
public static Integer DOWNLOAD_NOT_STARTED = -2;
/*
public boolean isYoutubeVideo() {
return link.matches("^https?://(www.)?youtube.com/.*");
}
*/
}

View file

@ -76,7 +76,7 @@ public class NextcloudNotificationManager {
PendingIntent pIntent = PendingIntent.getActivity(context, 0, intentNewsReader, 0);
NotificationCompat.Builder mNotificationDownloadImages = new NotificationCompat.Builder(context, channelId)
.setContentTitle(context.getResources().getString(R.string.app_name))
.setContentText("Downloading images for offline usage")
.setContentText(context.getString(R.string.notification_download_images_offline))
.setSmallIcon(R.drawable.ic_notification)
.setContentIntent(pIntent)
.setAutoCancel(true)
@ -109,7 +109,7 @@ public class NextcloudNotificationManager {
PendingIntent pIntent = PendingIntent.getActivity(context, 0, intentNewsReader, 0);
NotificationCompat.Builder mNotificationWebPages = new NotificationCompat.Builder(context, channelId)
.setContentTitle(context.getResources().getString(R.string.app_name))
.setContentText("Downloading webpages for offline usage")
.setContentText(context.getString(R.string.notification_download_articles_offline))
.setSmallIcon(R.drawable.ic_notification)
.setContentIntent(pIntent)
.setAutoCancel(true)
@ -187,6 +187,8 @@ public class NextcloudNotificationManager {
*/
//.setUsesChronometer(true)
.setContentTitle(description.getTitle())
.setContentText(description.getSubtitle())
.setSubText(description.getDescription())
.setSmallIcon(R.drawable.ic_notification)
//.setContentText(description.getSubtitle())
//.setContentText(mediaMetadata.getText(MediaMetadataCompat.METADATA_KEY_ARTIST))
@ -195,15 +197,18 @@ public class NextcloudNotificationManager {
.setLargeIcon(bitmapIcon)
.setContentIntent(controller.getSessionActivity())
.setDeleteIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_STOP))
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setOnlyAlertOnce(true);
boolean isPlaying = controller.getPlaybackState().getState() == PlaybackStateCompat.STATE_PLAYING;
builder.addAction(getPlayPauseAction(context, isPlaying));
// Make the transport controls visible on the lockscreen
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
builder.setStyle(new MediaStyle()
//.setShowActionsInCompactView(0) // show only play/pause in compact view
.setMediaSession(mediaSession.getSessionToken())
.setShowActionsInCompactView(0)
.setShowCancelButton(true)
.setCancelButtonIntent(
MediaButtonReceiver.buildMediaButtonPendingIntent(
@ -214,7 +219,7 @@ public class NextcloudNotificationManager {
}
private static NotificationCompat.Action getPlayPauseAction(Context context, boolean isPlaying) {
int drawableId = isPlaying ? android.R.drawable.ic_media_pause : android.R.drawable.ic_media_play;
int drawableId = isPlaying ? R.drawable.ic_action_pause : R.drawable.ic_action_play;
String actionText = isPlaying ? "Pause" : "Play"; // TODO extract as string resource
PendingIntent pendingIntent = MediaButtonReceiver.buildMediaButtonPendingIntent(context,

View file

@ -36,6 +36,7 @@ import java.util.List;
import java.util.Random;
import de.greenrobot.dao.query.LazyList;
import de.luhmer.owncloudnewsreader.R;
import de.luhmer.owncloudnewsreader.async_tasks.DownloadImageHandler;
import de.luhmer.owncloudnewsreader.database.DatabaseConnectionOrm;
import de.luhmer.owncloudnewsreader.database.model.Feed;
@ -170,7 +171,7 @@ public class DownloadImagesService extends JobIntentService {
//RemoveOldImages();
} else {
mNotificationDownloadImages
.setContentText((count + 1) + "/" + maxCount + " - Downloading Images for offline usage")
.setContentText((count + 1) + "/" + maxCount + " - " + getString(R.string.notification_download_images_offline))
.setProgress(maxCount, count + 1, false);
mNotificationManager.notify(NOTIFICATION_ID, mNotificationDownloadImages.build());

View file

@ -117,13 +117,10 @@ public class DownloadWebPageService extends Service {
private void runOnMainThreadAndWait(final Runnable runnable) throws InterruptedException {
synchronized(runnable) {
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
runnable.run();
synchronized (runnable) {
runnable.notifyAll();
}
handler.post(() -> {
runnable.run();
synchronized (runnable) {
runnable.notifyAll();
}
});
runnable.wait(); // unlocks runnable while waiting
@ -210,22 +207,19 @@ public class DownloadWebPageService extends Service {
//Log.v(TAG, "Loading page:");
initWebView();
loadUrlInWebViewAndWait();
} /* else {
} else {
Log.v(TAG, "Already cached article: " + url);
} */
}
}
updateNotificationProgress();
}
private void initWebView() {
try {
runOnMainThreadAndWait(new Runnable() {
@Override
public void run() {
webView = new WebView(DownloadWebPageService.this);
webView.setWebViewClient(new DownloadImageWebViewClient(lock));
webView.setWebChromeClient(new DownloadImageWebViewChromeClient());
}
runOnMainThreadAndWait(() -> {
webView = new WebView(DownloadWebPageService.this);
webView.setWebViewClient(new DownloadImageWebViewClient(lock));
webView.setWebChromeClient(new DownloadImageWebViewChromeClient());
});
} catch (InterruptedException e) {
Log.e(TAG, "Error while setting up WebView", e);
@ -234,11 +228,9 @@ public class DownloadWebPageService extends Service {
private void loadUrlInWebViewAndWait() {
try {
runOnMainThreadAndWait(new Runnable() {
@Override
public void run() {
webView.loadUrl(url);
}
runOnMainThreadAndWait(() -> {
Log.d(TAG, "downloading website for url: " + url);
webView.loadUrl(url);
});
lock.wait();
} catch (InterruptedException e) {
@ -287,28 +279,17 @@ public class DownloadWebPageService extends Service {
}
private void saveWebArchive(final WebView view, final String url) {
new Thread(new Runnable() {
@Override
public void run() {
delayedRunOnMainThread(new Runnable() {
@Override
public void run() {
// Can't store directly on external dir.. (workaround -> store on internal storage first and move then))
final File webArchive = getWebPageArchiveFileForUrl(DownloadWebPageService.this, url);
final File webArchiveExternalStorage = getWebPageArchiveFileForUrl(DownloadWebPageService.this, url);
view.saveWebArchive(webArchive.getAbsolutePath(), false, new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
// Move file to external storage once done writing
webArchive.renameTo(webArchiveExternalStorage);
//boolean success = webArchive.renameTo(webArchiveExternalStorage);
//Log.v(TAG, "Move succeeded: " + success);
}
});
}
}, 2000);
}
}).start();
new Thread(() -> delayedRunOnMainThread(() -> {
// Can't store directly on external dir.. (workaround -> store on internal storage first and move then))
final File webArchive = getWebPageArchiveFileForUrl(DownloadWebPageService.this, url);
final File webArchiveExternalStorage = getWebPageArchiveFileForUrl(DownloadWebPageService.this, url);
view.saveWebArchive(webArchive.getAbsolutePath(), false, value -> {
// Move file to external storage once done writing
webArchive.renameTo(webArchiveExternalStorage);
//boolean success = webArchive.renameTo(webArchiveExternalStorage);
//Log.v(TAG, "Move succeeded: " + success);
});
}, 2000)).start();
}
}
@ -324,7 +305,7 @@ public class DownloadWebPageService extends Service {
EventBus.getDefault().post(new StopWebArchiveDownloadEvent());
} else {
mNotificationWebPages
.setContentText((current) + "/" + totalCount + " - Downloading Images for offline usage")
.setContentText((current) + "/" + totalCount + " - " + getString(R.string.notification_download_articles_offline))
.setProgress(totalCount, current, false);
mNotificationManager.notify(NOTIFICATION_ID, mNotificationWebPages.build());

View file

@ -5,6 +5,7 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.ResultReceiver;
@ -26,16 +27,18 @@ import org.greenrobot.eventbus.Subscribe;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import de.luhmer.owncloudnewsreader.NewsReaderListActivity;
import de.luhmer.owncloudnewsreader.R;
import de.luhmer.owncloudnewsreader.events.podcast.NewPodcastPlaybackListener;
import de.luhmer.owncloudnewsreader.events.podcast.PodcastCompletedEvent;
import de.luhmer.owncloudnewsreader.events.podcast.RegisterVideoOutput;
import de.luhmer.owncloudnewsreader.events.podcast.RegisterYoutubeOutput;
import de.luhmer.owncloudnewsreader.events.podcast.SpeedPodcast;
import de.luhmer.owncloudnewsreader.events.podcast.TogglePlayerStateEvent;
import de.luhmer.owncloudnewsreader.events.podcast.UpdatePodcastStatusEvent;
import de.luhmer.owncloudnewsreader.events.podcast.WindPodcast;
import de.luhmer.owncloudnewsreader.model.MediaItem;
import de.luhmer.owncloudnewsreader.model.PodcastItem;
@ -43,7 +46,6 @@ import de.luhmer.owncloudnewsreader.model.TTSItem;
import de.luhmer.owncloudnewsreader.services.podcast.MediaPlayerPlaybackService;
import de.luhmer.owncloudnewsreader.services.podcast.PlaybackService;
import de.luhmer.owncloudnewsreader.services.podcast.TTSPlaybackService;
import de.luhmer.owncloudnewsreader.services.podcast.YoutubePlaybackService;
import de.luhmer.owncloudnewsreader.view.PodcastNotification;
import static android.view.KeyEvent.KEYCODE_MEDIA_STOP;
@ -55,7 +57,13 @@ public class PodcastPlaybackService extends MediaBrowserServiceCompat {
private static final String TAG = "PodcastPlaybackService";
public static final String PLAYBACK_SPEED_FLOAT = "PLAYBACK_SPEED";
public static final String CURRENT_PODCAST_ITEM_MEDIA_ITEM= "CURRENT_PODCAST_ITEM";
public static final String CURRENT_PODCAST_ITEM_MEDIA_ITEM = "CURRENT_PODCAST_ITEM";
public static final String CURRENT_PODCAST_MEDIA_TYPE = "CURRENT_PODCAST_MEDIA_TYPE";
private static final long PROGRESS_UPDATE_INTERNAL = 1000;
private static final long PROGRESS_UPDATE_INITIAL_INTERVAL = 100;
private PodcastNotification podcastNotification;
private EventBus eventBus;
@ -68,6 +76,12 @@ public class PodcastPlaybackService extends MediaBrowserServiceCompat {
private float currentPlaybackSpeed = 1;
public static final int delay = 500; //In milliseconds
private final ScheduledExecutorService mExecutorService =
Executors.newSingleThreadScheduledExecutor();
private ScheduledFuture<?> mScheduleFuture;
public MediaItem getCurrentlyPlayingPodcast() {
if(mPlaybackService != null) {
return mPlaybackService.getMediaItem();
@ -90,20 +104,20 @@ public class PodcastPlaybackService extends MediaBrowserServiceCompat {
@Override
public void onLoadChildren(@NonNull String s, @NonNull Result<List<MediaBrowserCompat.MediaItem>> result) {
Log.d(TAG, "onLoadChildren() called with: s = [" + s + "], result = [" + result + "]");
result.sendResult(new ArrayList<MediaBrowserCompat.MediaItem>());
result.sendResult(new ArrayList<>());
}
@Override
public boolean onUnbind(Intent intent) {
Log.d(TAG, "onUnbind() called with: intent = [" + intent + "]");
if (!isActive()) {
Log.v(TAG, "Stopping PodcastPlaybackService because of inactivity");
stopSelf();
}
if(podcastNotification != null) {
podcastNotification.unbind();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && mSession != null) {
mSession.release();
}
}
return super.onUnbind(intent);
}
@ -125,9 +139,6 @@ public class PodcastPlaybackService extends MediaBrowserServiceCompat {
eventBus.register(this);
//eventBus.post(new PodcastPlaybackServiceStarted());
mHandler.postDelayed(mUpdateTimeTask, 0);
setSessionToken(mSession.getSessionToken());
Intent intent = new Intent(this, NewsReaderListActivity.class);
@ -153,7 +164,7 @@ public class PodcastPlaybackService extends MediaBrowserServiceCompat {
mgr.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE);
}
mHandler.removeCallbacks(mUpdateTimeTask);
mExecutorService.shutdown();
podcastNotification.cancel();
super.onDestroy();
@ -169,62 +180,110 @@ public class PodcastPlaybackService extends MediaBrowserServiceCompat {
mPlaybackService.destroy();
mPlaybackService = null;
}
mHandler.removeCallbacks(mUpdateTimeTask);
stopProgressUpdates();
if(intent.hasExtra(MEDIA_ITEM)) {
MediaItem mediaItem = (MediaItem) intent.getSerializableExtra(MEDIA_ITEM);
if (mediaItem instanceof PodcastItem) {
if (((PodcastItem) mediaItem).isYoutubeVideo()) {
mPlaybackService = new YoutubePlaybackService(this, podcastStatusListener, mediaItem);
} else {
//if (((PodcastItem) mediaItem).isYoutubeVideo()) {
// mPlaybackService = new YoutubePlaybackService(this, podcastStatusListener, mediaItem);
//} else {
mPlaybackService = new MediaPlayerPlaybackService(this, podcastStatusListener, mediaItem);
}
//}
} else if (mediaItem instanceof TTSItem) {
mPlaybackService = new TTSPlaybackService(this, podcastStatusListener, mediaItem);
}
podcastNotification.podcastChanged();
sendMediaStatus();
updateMetadata(mediaItem);
// Update notification after setting metadata (notification uses metadata information)
podcastNotification.createPodcastNotification();
mPlaybackService.playbackSpeedChanged(currentPlaybackSpeed);
startProgressUpdates();
requestAudioFocus();
}
}
return super.onStartCommand(intent, flags, startId);
}
private void updateMetadata(MediaItem mediaItem) {
MediaItem mi = mediaItem;
if(mi == null) {
mi = new PodcastItem(-1, "", "", "", "", false, null, false);
}
int totalDuration = 0;
if(mPlaybackService != null) {
totalDuration = mPlaybackService.getTotalDuration();
}
mSession.setMetadata(new MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, mi.author)
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, mi.title)
//.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, mediaItem.author) // Android Auto
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, mi.favIcon)
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, String.valueOf(mi.itemId))
.putString(CURRENT_PODCAST_MEDIA_TYPE, getCurrentlyPlayedMediaType().toString())
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, totalDuration)
//.putLong(EXTRA_IS_EXPLICIT, EXTRA_METADATA_ENABLED_VALUE) // Android Auto
//.putLong(EXTRA_IS_DOWNLOADED, EXTRA_METADATA_ENABLED_VALUE) // Android Auto
.build());
}
/*
private Long getVideoWidth() {
if(mPlaybackService instanceof MediaPlayerPlaybackService) {
return ((MediaPlayerPlaybackService)mPlaybackService).getVideoWidth();
}
return null;
}
*/
private PlaybackService.PodcastStatusListener podcastStatusListener = new PlaybackService.PodcastStatusListener() {
@Override
public void podcastStatusUpdated() {
sendMediaStatus();
syncMediaAndPlaybackStatus();
if(mPlaybackService != null) {
updateMetadata(mPlaybackService.getMediaItem());
}
}
@Override
public void podcastCompleted() {
Log.d(TAG, "Podcast completed, cleaning up");
mHandler.removeCallbacks(mUpdateTimeTask);
podcastNotification.cancel();
mPlaybackService.destroy();
mPlaybackService = null;
endCurrentMediaPlayback();
EventBus.getDefault().post(new PodcastCompletedEvent());
}
};
public static final int delay = 500; //In milliseconds
private void endCurrentMediaPlayback() {
Log.d(TAG, "endCurrentMediaPlayback() called");
stopProgressUpdates();
// Set metadata
updateMetadata(null);
/**
* Background Runnable thread
* */
private Runnable mUpdateTimeTask = new Runnable() {
public void run() {
sendMediaStatus();
mHandler.postDelayed(this, delay);
if(mPlaybackService != null) {
mPlaybackService.destroy();
mPlaybackService = null;
}
};
syncMediaAndPlaybackStatus();
Log.d(TAG, "cancel notification");
podcastNotification.cancel();
abandonAudioFocus();
}
@Subscribe
public void onEvent(TogglePlayerStateEvent event) {
@ -247,13 +306,17 @@ public class PodcastPlaybackService extends MediaBrowserServiceCompat {
}
private boolean isPlaying() {
return (mPlaybackService != null && mPlaybackService.getStatus() == PlaybackService.Status.PLAYING);
return (mPlaybackService != null && mPlaybackService.getStatus() == PlaybackStateCompat.STATE_PLAYING);
}
@Subscribe
public void onEvent(WindPodcast event) {
if(mPlaybackService != null) {
mPlaybackService.seekTo(event.toPositionInPercent);
int seekTo = (int) (mPlaybackService.getCurrentPosition() + event.milliSeconds);
if(seekTo < 0) {
seekTo = 0;
}
mPlaybackService.seekTo(seekTo);
}
}
@ -264,20 +327,9 @@ public class PodcastPlaybackService extends MediaBrowserServiceCompat {
}
}
@Subscribe
public void onEvent(RegisterYoutubeOutput videoOutput) {
if(mPlaybackService != null && mPlaybackService instanceof YoutubePlaybackService) {
if(videoOutput.youTubePlayer == null) {
mPlaybackService.destroy();
} else {
((YoutubePlaybackService) mPlaybackService).setYoutubePlayer(videoOutput.youTubePlayer, videoOutput.wasRestored);
}
}
}
@Subscribe
public void onEvent(NewPodcastPlaybackListener newListener) {
sendMediaStatus();
syncMediaAndPlaybackStatus();
}
@Subscribe
@ -291,20 +343,73 @@ public class PodcastPlaybackService extends MediaBrowserServiceCompat {
public void play() {
if(mPlaybackService != null) {
// Start playback
mPlaybackService.play();
}
startProgressUpdates();
mHandler.removeCallbacks(mUpdateTimeTask);
mHandler.postDelayed(mUpdateTimeTask, 0);
requestAudioFocus();
}
}
public void pause() {
if(mPlaybackService != null) {
mPlaybackService.pause();
}
stopProgressUpdates();
mHandler.removeCallbacks(mUpdateTimeTask);
sendMediaStatus();
abandonAudioFocus();
}
private void requestAudioFocus() {
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
// Request audio focus for playback
int result = audioManager.requestAudioFocus(
audioFocusChangeListener,
// Use the music stream.
AudioManager.STREAM_MUSIC,
// Request permanent focus.
AudioManager.AUDIOFOCUS_GAIN);
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
Log.d(TAG, "AUDIOFOCUS_REQUEST_GRANTED");
}
}
private void abandonAudioFocus() {
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
// Abandon audio focus when playback complete
audioManager.abandonAudioFocus(audioFocusChangeListener);
}
private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener = focusChange -> {
if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
// Permanent loss of audio focus
// Pause playback immediately
mSession.getController().getTransportControls().pause();
}
else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
// Pause playback
} else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
// Lower the volume, keep playing
} else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
// Your app has been granted audio focus again
// Raise volume to normal, restart playback if necessary
}
};
private void startProgressUpdates() {
mScheduleFuture = mExecutorService.scheduleAtFixedRate(
() -> mHandler.post(PodcastPlaybackService.this::syncMediaAndPlaybackStatus), PROGRESS_UPDATE_INITIAL_INTERVAL,
PROGRESS_UPDATE_INTERNAL, TimeUnit.MILLISECONDS);
}
private void stopProgressUpdates() {
if (mScheduleFuture != null) {
mScheduleFuture.cancel(false);
}
syncMediaAndPlaybackStatus(); // Send one last update
}
@ -312,22 +417,22 @@ public class PodcastPlaybackService extends MediaBrowserServiceCompat {
return currentPlaybackSpeed;
}
public void sendMediaStatus() {
UpdatePodcastStatusEvent audioPodcastEvent;
public void syncMediaAndPlaybackStatus() {
/*
if(mPlaybackService == null) {
audioPodcastEvent = new UpdatePodcastStatusEvent(0, 0, PlaybackService.Status.NOT_INITIALIZED, "", "", PlaybackService.VideoType.None, -1, -1);
} else {
audioPodcastEvent = new UpdatePodcastStatusEvent(
mPlaybackService.getCurrentDuration(),
mPlaybackService.getCurrentPosition(),
mPlaybackService.getTotalDuration(),
mPlaybackService.getStatus(),
mPlaybackService.getMediaItem().link,
mPlaybackService.getMediaItem().title,
"NOT SUPPORTED ANYMORE!!!",
mPlaybackService.getVideoType(),
mPlaybackService.getMediaItem().itemId,
getPlaybackSpeed());
}
eventBus.post(audioPodcastEvent);
if(audioPodcastEvent.isPlaying()) {
@ -335,10 +440,58 @@ public class PodcastPlaybackService extends MediaBrowserServiceCompat {
} else {
stopForeground(false);
}
*/
@PlaybackStateCompat.State int playbackState;
int currentPosition = 0;
int totalDuration = 0;
if(mPlaybackService == null || mPlaybackService.getMediaItem().itemId == -1) {
// When podcast is not initialized or playback is finished
playbackState = PlaybackStateCompat.STATE_NONE;
mSession.setPlaybackState(new PlaybackStateCompat.Builder()
.setState(playbackState, currentPosition, 1.0f)
.setActions(buildPlaybackActions(playbackState, false))
.build());
stopForeground(false);
} else {
currentPosition = mPlaybackService.getCurrentPosition();
totalDuration = mPlaybackService.getTotalDuration();
playbackState = mPlaybackService.getStatus();
if (playbackState== PlaybackStateCompat.STATE_PLAYING) {
startForeground(PodcastNotification.NOTIFICATION_ID, podcastNotification.getNotification());
} else {
stopForeground(false);
}
mSession.setPlaybackState(new PlaybackStateCompat.Builder()
.setState(playbackState, currentPosition, 1.0f)
.setActions(buildPlaybackActions(playbackState, true))
.build());
}
if(playbackState == PlaybackStateCompat.STATE_PLAYING) {
mSession.setActive(true);
} else {
mSession.setActive(false);
}
podcastNotification.updateStateOfNotification(playbackState, currentPosition, totalDuration);
}
private long buildPlaybackActions(int playbackState, boolean mediaLoaded) {
long actions = playbackState == PlaybackStateCompat.STATE_PLAYING ? PlaybackStateCompat.ACTION_PAUSE : PlaybackStateCompat.ACTION_PLAY;
actions |= PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS |
PlaybackStateCompat.ACTION_SKIP_TO_NEXT;
//PlaybackStateCompat.ACTION_STOP;
//public class PodcastPlaybackServiceStarted { }
if(mediaLoaded) {
actions |= PlaybackStateCompat.ACTION_SEEK_TO;
}
return actions;
}
PhoneStateListener phoneStateListener = new PhoneStateListener() {
@Override
@ -346,11 +499,14 @@ public class PodcastPlaybackService extends MediaBrowserServiceCompat {
if (state == TelephonyManager.CALL_STATE_RINGING) {
//Incoming call: Pause music
pause();
} else if(state == TelephonyManager.CALL_STATE_IDLE) {
}
/*
else if(state == TelephonyManager.CALL_STATE_IDLE) {
//Not in call: Play music
} else if(state == TelephonyManager.CALL_STATE_OFFHOOK) {
//A call is dialing, active or on hold
}
*/
super.onCallStateChanged(state, incomingNumber);
}
};
@ -359,12 +515,21 @@ public class PodcastPlaybackService extends MediaBrowserServiceCompat {
private final class MediaSessionCallback extends MediaSessionCompat.Callback {
@Override
public void onPlay() {
EventBus.getDefault().post(new TogglePlayerStateEvent());
Log.d(TAG, "onPlay() called");
play();
}
@Override
public void onPause() {
EventBus.getDefault().post(new TogglePlayerStateEvent());
Log.d(TAG, "onPause() called");
pause();
}
@Override
public void onPlayFromSearch(String query, Bundle extras) {
Log.d(TAG, "onPlayFromSearch() called with: query = [" + query + "], extras = [" + extras + "]");
// TODO Implement this
super.onPlayFromSearch(query, extras);
}
@Override
@ -375,12 +540,34 @@ public class PodcastPlaybackService extends MediaBrowserServiceCompat {
cb.send(0, b);
} else if(command.equals(CURRENT_PODCAST_ITEM_MEDIA_ITEM)) {
Bundle b = new Bundle();
b.putSerializable(CURRENT_PODCAST_ITEM_MEDIA_ITEM, mPlaybackService.getMediaItem());
if(mPlaybackService != null) {
b.putSerializable(CURRENT_PODCAST_ITEM_MEDIA_ITEM, mPlaybackService.getMediaItem());
} else {
b.putSerializable(CURRENT_PODCAST_ITEM_MEDIA_ITEM, null);
}
cb.send(0, b);
}
super.onCommand(command, extras, cb);
}
@Override
public void onSeekTo(long pos) {
Log.d(TAG, "onSeekTo() called with: pos = [" + pos + "]");
super.onSeekTo(pos);
}
@Override
public void onSkipToNext() {
Log.d(TAG, "onSkipToNext() called");
super.onSkipToNext();
}
@Override
public void onSkipToPrevious() {
Log.d(TAG, "onSkipToPrevious() called");
super.onSkipToPrevious();
}
@Override
public boolean onMediaButtonEvent(Intent mediaButtonEvent) {
Log.d(TAG, mediaButtonEvent.getAction());
@ -392,6 +579,7 @@ public class PodcastPlaybackService extends MediaBrowserServiceCompat {
// Stop requested (e.g. notification was swiped away)
if(keyEvent.getKeyCode() == KEYCODE_MEDIA_STOP) {
pause();
endCurrentMediaPlayback();
stopSelf();
/*
boolean isPlaying = mSession.getController().getPlaybackState().getState() == PlaybackStateCompat.STATE_PLAYING;
@ -406,6 +594,7 @@ public class PodcastPlaybackService extends MediaBrowserServiceCompat {
}
private void initMediaSessions() {
//String packageName = PodcastNotificationToggle.class.getPackage().getName();
//ComponentName receiver = new ComponentName(packageName, PodcastNotificationToggle.class.getName());
ComponentName mediaButtonReceiver = new ComponentName(this, MediaButtonReceiver.class);
@ -413,8 +602,8 @@ public class PodcastPlaybackService extends MediaBrowserServiceCompat {
mSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS |
MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
mSession.setPlaybackState(new PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_PAUSED, 0, 0)
.setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE).build());
.setState(PlaybackStateCompat.STATE_NONE, 0, 0)
.setActions(buildPlaybackActions(PlaybackStateCompat.STATE_PAUSED, false)).build());
mSession.setCallback(new MediaSessionCallback());
@ -424,21 +613,14 @@ public class PodcastPlaybackService extends MediaBrowserServiceCompat {
//mSession.setMediaButtonReceiver(pendingIntent);
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
audioManager.requestAudioFocus(new AudioManager.OnAudioFocusChangeListener() {
@Override
public void onAudioFocusChange(int focusChange) {
// Ignore
}
}, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
updateMetadata(null);
}
//MediaControllerCompat controller = mSession.getController();
//mSession.setActive(true);
mSession.setMetadata(new MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, "")
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, "")
.build());
private PlaybackService.VideoType getCurrentlyPlayedMediaType() {
if(mPlaybackService != null) {
return mPlaybackService.getVideoType();
} else {
return PlaybackService.VideoType.None;
}
}
}

View file

@ -3,6 +3,7 @@ package de.luhmer.owncloudnewsreader.services.podcast;
import android.content.Context;
import android.media.MediaPlayer;
import android.os.Build;
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
@ -21,46 +22,41 @@ import de.luhmer.owncloudnewsreader.model.PodcastItem;
public class MediaPlayerPlaybackService extends PlaybackService {
private static final String TAG = MediaPlayerPlaybackService.class.getCanonicalName();
private MediaPlayer mMediaPlayer;
private View parentResizableView;
//private View parentView;
public MediaPlayerPlaybackService(final Context context, PodcastStatusListener podcastStatusListener, MediaItem mediaItem) {
super(podcastStatusListener, mediaItem);
mMediaPlayer = new MediaPlayer();
mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mediaPlayer, int i, int i2) {
setStatus(Status.FAILED);
Toast.makeText(context, "Failed to open podcast", Toast.LENGTH_LONG).show();
return false;
}
//mMediaPlayer.setOnVideoSizeChangedListener((mp, width, height) -> configureVideo(width, height));
mMediaPlayer.setOnErrorListener((mediaPlayer, i, i2) -> {
setStatus(PlaybackStateCompat.STATE_ERROR);
Toast.makeText(context, "Failed to open podcast", Toast.LENGTH_LONG).show();
return false;
});
mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mediaPlayer) {
setStatus(Status.PAUSED);
play();
}
mMediaPlayer.setOnPreparedListener(mediaPlayer -> {
podcastStatusListener.podcastStatusUpdated();
setStatus(PlaybackStateCompat.STATE_PAUSED);
play();
});
mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mediaPlayer) {
pause();//Send the over signal
podcastCompleted();
}
mMediaPlayer.setOnCompletionListener(mediaPlayer -> {
pause();//Send the over signal
podcastCompleted();
});
try {
setStatus(Status.PREPARING);
setStatus(PlaybackStateCompat.STATE_CONNECTING);
mMediaPlayer.setDataSource(((PodcastItem) mediaItem).link);
mMediaPlayer.prepareAsync();
} catch (IOException e) {
e.printStackTrace();
setStatus(Status.FAILED);
setStatus(PlaybackStateCompat.STATE_ERROR);
}
}
@ -78,14 +74,14 @@ public class MediaPlayerPlaybackService extends PlaybackService {
if (progress >= 1) {
mMediaPlayer.seekTo(0);
}
setStatus(Status.PLAYING);
setStatus(PlaybackStateCompat.STATE_PLAYING);
} catch (Exception ex) {
ex.printStackTrace();
Log.e(TAG, "Error while playing", ex);
}
mMediaPlayer.start();
populateVideo();
//populateVideo();
}
@Override
@ -93,7 +89,7 @@ public class MediaPlayerPlaybackService extends PlaybackService {
if (mMediaPlayer.isPlaying()) {
mMediaPlayer.pause();
}
setStatus(Status.PAUSED);
setStatus(PlaybackStateCompat.STATE_PAUSED);
}
@Override
@ -103,16 +99,15 @@ public class MediaPlayerPlaybackService extends PlaybackService {
}
}
@Override
public void seekTo(double percent) {
double totalDuration = mMediaPlayer.getDuration();
int position = (int) ((totalDuration / 100d) * percent);
public void seekTo(int position) {
//double totalDuration = mMediaPlayer.getDuration();
//int position = (int) ((totalDuration / 100d) * percent);
mMediaPlayer.seekTo(position);
}
@Override
public int getCurrentDuration() {
public int getCurrentPosition() {
if (mMediaPlayer != null && isMediaLoaded()) {
return mMediaPlayer.getCurrentPosition();
}
@ -133,6 +128,7 @@ public class MediaPlayerPlaybackService extends PlaybackService {
}
/*
private void populateVideo() {
double videoHeightRel = (double) mSurfaceWidth / (double) mMediaPlayer.getVideoWidth();
int videoHeight = (int) (mMediaPlayer.getVideoHeight() * videoHeightRel);
@ -140,9 +136,13 @@ public class MediaPlayerPlaybackService extends PlaybackService {
if (mSurfaceWidth != 0 && videoHeight != 0 && mSurfaceHolder != null) {
//mSurfaceHolder.setFixedSize(mSurfaceWidth, videoHeight);
parentResizableView.getLayoutParams().height = videoHeight;
parentResizableView.setLayoutParams(parentResizableView.getLayoutParams());
parentView.getLayoutParams().height = videoHeight;
parentView.setLayoutParams(parentView.getLayoutParams());
}
}*/
public long getVideoWidth() {
return mMediaPlayer.getVideoWidth();
}
public void setVideoView(SurfaceView surfaceView, View parentResizableView) {
@ -153,37 +153,32 @@ public class MediaPlayerPlaybackService extends PlaybackService {
mMediaPlayer.setScreenOnWhilePlaying(false);
} else {
if (surfaceView.getHolder() != mSurfaceHolder) {
this.parentResizableView = parentResizableView;
//this.parentView = parentResizableView;
surfaceView.getHolder().addCallback(mSHCallback);
//videoOutput.surfaceView.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); //holder.setType(SurfaceHolder.SURFACE_TYPE_GPU);
populateVideo();
//Log.v(TAG, "Enable Screen output!");
}
}
}
private int mSurfaceWidth;
private int mSurfaceHeight;
//private int mSurfaceWidth;
//private int mSurfaceHeight;
private SurfaceHolder mSurfaceHolder;
SurfaceHolder.Callback mSHCallback = new SurfaceHolder.Callback()
{
public void surfaceChanged(SurfaceHolder holder, int format, int surfaceWidth, int surfaceHeight)
{
mSurfaceWidth = surfaceWidth;
mSurfaceHeight = surfaceHeight;
private SurfaceHolder.Callback mSHCallback = new SurfaceHolder.Callback() {
public void surfaceChanged(SurfaceHolder holder, int format, int surfaceWidth, int surfaceHeight) {
Log.v(TAG, "surfaceChanged() called with: holder = [" + holder + "], format = [" + format + "], surfaceWidth = [" + surfaceWidth + "], surfaceHeight = [" + surfaceHeight + "]");
//mSurfaceWidth = surfaceWidth;
//mSurfaceHeight = surfaceHeight;
//populateVideo();
}
public void surfaceCreated(SurfaceHolder holder)
{
public void surfaceCreated(SurfaceHolder holder) {
Log.v(TAG, "surfaceCreated() called with: holder = [" + holder + "]");
mSurfaceHolder = holder;
mMediaPlayer.setDisplay(mSurfaceHolder); //TODO required
mMediaPlayer.setScreenOnWhilePlaying(true); //TODO required
Log.d(TAG, "surfaceCreated");
mMediaPlayer.setDisplay(mSurfaceHolder);
mMediaPlayer.setScreenOnWhilePlaying(true);
}
public void surfaceDestroyed(SurfaceHolder holder)

View file

@ -1,5 +1,7 @@
package de.luhmer.owncloudnewsreader.services.podcast;
import android.support.v4.media.session.PlaybackStateCompat;
import de.luhmer.owncloudnewsreader.model.MediaItem;
/**
@ -8,18 +10,17 @@ import de.luhmer.owncloudnewsreader.model.MediaItem;
public abstract class PlaybackService {
public enum VideoType { None, Video, VideoType, YouTube }
private @PlaybackStateCompat.State int mStatus = PlaybackStateCompat.STATE_NONE;
private PodcastStatusListener podcastStatusListener;
private MediaItem mediaItem;
public interface PodcastStatusListener {
void podcastStatusUpdated();
void podcastCompleted();
}
public enum Status { NOT_INITIALIZED, FAILED, PREPARING, PLAYING, PAUSED, STOPPED };
public enum VideoType { None, Video, VideoType, YouTube }
private Status mStatus = Status.NOT_INITIALIZED;
private PodcastStatusListener podcastStatusListener;
private MediaItem mediaItem;
public PlaybackService(PodcastStatusListener podcastStatusListener, MediaItem mediaItem) {
this.podcastStatusListener = podcastStatusListener;
this.mediaItem = mediaItem;
@ -31,8 +32,8 @@ public abstract class PlaybackService {
public abstract void playbackSpeedChanged(float currentPlaybackSpeed);
public void seekTo(double percent) { }
public int getCurrentDuration() { return 0; }
public void seekTo(int position) { }
public int getCurrentPosition() { return 0; }
public int getTotalDuration() { return 0; }
public VideoType getVideoType() { return VideoType.None; }
@ -40,11 +41,11 @@ public abstract class PlaybackService {
return mediaItem;
}
public Status getStatus() {
public @PlaybackStateCompat.State int getStatus() {
return mStatus;
}
protected void setStatus(Status status) {
protected void setStatus(@PlaybackStateCompat.State int status) {
this.mStatus = status;
podcastStatusListener.podcastStatusUpdated();
}
@ -54,9 +55,8 @@ public abstract class PlaybackService {
}
public boolean isMediaLoaded() {
return getStatus() != Status.NOT_INITIALIZED
&& getStatus() != Status.PREPARING
&& getStatus() != Status.FAILED;
return getStatus() != PlaybackStateCompat.STATE_NONE
&& getStatus() != PlaybackStateCompat.STATE_CONNECTING
&& getStatus() != PlaybackStateCompat.STATE_ERROR;
}
}

View file

@ -3,6 +3,7 @@ package de.luhmer.owncloudnewsreader.services.podcast;
import android.content.Context;
import android.speech.tts.TextToSpeech;
import android.speech.tts.UtteranceProgressListener;
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.Log;
import java.util.HashMap;
@ -22,10 +23,9 @@ public class TTSPlaybackService extends PlaybackService implements TextToSpeech.
try {
ttsController = new TextToSpeech(context, this);
setStatus(Status.PREPARING);
if(ttsController == null) {
setStatus(PlaybackStateCompat.STATE_CONNECTING);
if(ttsController != null) {
ttsController.setOnUtteranceProgressListener(new UtteranceProgressListener() {
@Override
public void onDone(String utteranceId) {
@ -35,9 +35,9 @@ public class TTSPlaybackService extends PlaybackService implements TextToSpeech.
@Override public void onStart(String utteranceId) {}
@Override public void onError(String utteranceId) {}
});
}
else
} else {
onInit(TextToSpeech.SUCCESS);
}
} catch (Exception e) {
e.printStackTrace();
}
@ -59,7 +59,7 @@ public class TTSPlaybackService extends PlaybackService implements TextToSpeech.
public void pause() {
if (ttsController.isSpeaking()) {
ttsController.stop();
setStatus(Status.PAUSED);
setStatus(PlaybackStateCompat.STATE_PAUSED);
}
}
@ -84,9 +84,9 @@ public class TTSPlaybackService extends PlaybackService implements TextToSpeech.
HashMap<String,String> ttsParams = new HashMap<>();
ttsParams.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID,"dummyId");
ttsController.speak(((TTSItem)getMediaItem()).text, TextToSpeech.QUEUE_FLUSH, ttsParams);
setStatus(Status.PLAYING);
setStatus(PlaybackStateCompat.STATE_PLAYING);
} else {
Log.e("TTS", "Initilization Failed!");
Log.e("TTS", "Initialization Failed!");
ttsController = null;
}
}

View file

@ -3,23 +3,17 @@ package de.luhmer.owncloudnewsreader.view;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.Context;
import android.os.Build;
import androidx.core.app.NotificationCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.Log;
import org.greenrobot.eventbus.EventBus;
import androidx.core.app.NotificationCompat;
import org.greenrobot.eventbus.Subscribe;
import java.util.Locale;
import de.luhmer.owncloudnewsreader.events.podcast.UpdatePodcastStatusEvent;
import de.luhmer.owncloudnewsreader.model.MediaItem;
import de.luhmer.owncloudnewsreader.notification.NextcloudNotificationManager;
import de.luhmer.owncloudnewsreader.services.PodcastPlaybackService;
import de.luhmer.owncloudnewsreader.services.podcast.PlaybackService;
public class PodcastNotification {
@ -36,7 +30,7 @@ public class PodcastNotification {
private final String CHANNEL_ID = "Podcast Notification";
private MediaSessionCompat mSession;
private PlaybackService.Status lastStatus = PlaybackService.Status.NOT_INITIALIZED;
private @PlaybackStateCompat.State int lastStatus = PlaybackStateCompat.STATE_NONE;
public final static int NOTIFICATION_ID = 1111;
@ -44,41 +38,22 @@ public class PodcastNotification {
this.mContext = context;
this.mSession = session;
this.notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
this.notificationBuilder = NextcloudNotificationManager.buildPodcastNotification(mContext, CHANNEL_ID, mSession);
EventBus.getDefault().register(this);
}
public void unbind() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && mSession != null) {
mSession.release();
}
//EventBus.getDefault().register(this);
}
@Subscribe
public void onEvent(UpdatePodcastStatusEvent podcast) {
public void updateStateOfNotification(@PlaybackStateCompat.State int status, long currentPosition, long totalDuration) {
if(mSession == null) {
Log.v(TAG, "Session null.. ignore UpdatePodcastStatusEvent");
return;
}
if (status != lastStatus) {
lastStatus = status;
if (podcast.getStatus() != lastStatus) {
lastStatus = podcast.getStatus();
/*
notificationBuilder.setContentTitle(podcast.getTitle());
notificationBuilder.mActions.clear();
notificationBuilder.addAction(
drawableId,
actionText,
PendingIntent.getBroadcast(mContext, 0, new Intent(mContext, PodcastNotificationToggle.class),
PendingIntent.FLAG_ONE_SHOT));
*/
/*
if(podcast.isPlaying()) {
//Prevent the Podcast Player from getting killed because of low memory
@ -100,53 +75,36 @@ public class PodcastNotification {
.build());
*/
mSession.setActive(true);
if (podcast.isPlaying()) {
mSession.setPlaybackState(new PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_PLAYING, podcast.getCurrent(), 1.0f)
.setActions(PlaybackStateCompat.ACTION_PAUSE).build());
} else {
mSession.setPlaybackState(new PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_PAUSED, podcast.getCurrent(), 0.0f)
.setActions(PlaybackStateCompat.ACTION_PLAY).build());
}
//mSession.setActive(podcast.isPlaying());
notificationBuilder = NextcloudNotificationManager.buildPodcastNotification(mContext, CHANNEL_ID, mSession);
//int drawableId = podcast.isPlaying() ? android.R.drawable.ic_media_pause : android.R.drawable.ic_media_play;
//String actionText = podcast.isPlaying() ? "Pause" : "Play";
//notificationBuilder.addAction(new NotificationCompat.Action(drawableId, actionText, intent));
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
}
int hours = (int)( podcast.getCurrent() / (1000*60*60));
int minutes = (int)(podcast.getCurrent() % (1000*60*60)) / (1000*60);
int seconds = (int) ((podcast.getCurrent() % (1000*60*60)) % (1000*60) / 1000);
int hours = (int) (currentPosition / (1000*60*60));
int minutes = (int) ((currentPosition % (1000*60*60)) / (1000*60));
int seconds = (int) ((currentPosition % (1000*60*60)) % (1000*60) / 1000);
minutes += hours * 60;
String fromText = (String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds));
hours = (int)( podcast.getMax() / (1000*60*60));
minutes = (int)(podcast.getMax() % (1000*60*60)) / (1000*60);
seconds = (int) ((podcast.getMax() % (1000*60*60)) % (1000*60) / 1000);
hours = (int) (totalDuration / (1000*60*60));
minutes = (int) ((totalDuration % (1000*60*60)) / (1000*60));
seconds = (int) ((totalDuration % (1000*60*60)) % (1000*60) / 1000);
minutes += hours * 60;
String toText = (String.format(Locale.getDefault(),"%02d:%02d", minutes, seconds));
double progressDouble = ((double)podcast.getCurrent() / (double)podcast.getMax()) * 100d;
double progressDouble = ((double)currentPosition / (double)totalDuration) * 100d;
int progress = ((int) progressDouble);
notificationBuilder
.setContentText(fromText + " - " + toText)
.setProgress(100, progress, podcast.getStatus() == PlaybackService.Status.PREPARING);
.setProgress(100, progress, status == PlaybackStateCompat.STATE_CONNECTING); // TODO IMPLEMENT THIS!!!!
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
}
@ -156,14 +114,17 @@ public class PodcastNotification {
if(notificationManager != null) {
notificationManager.cancel(NOTIFICATION_ID);
}
/*
if(mSession != null) {
mSession.setActive(false);
}
*/
}
public void podcastChanged() {
public void createPodcastNotification() {
/*
MediaItem podcastItem = ((PodcastPlaybackService)mContext).getCurrentlyPlayingPodcast();
*/
/*
String favIconUrl = podcastItem.favIcon;
DisplayImageOptions displayImageOptions = new DisplayImageOptions.Builder().
@ -173,13 +134,14 @@ public class PodcastNotification {
build();
*/
//TODO networkOnMainThreadExceptionHere!
//Bitmap bmpAlbumArt = ImageLoader.getInstance().loadImageSync(favIconUrl, displayImageOptions);
/*
mSession.setMetadata(new MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, podcastItem.author)
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, podcastItem.title)
.build());
*/
/*
mSession.setMetadata(new MediaMetadataCompat.Builder()

View file

@ -1,248 +0,0 @@
package de.luhmer.owncloudnewsreader.view;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.widget.RelativeLayout;
import org.greenrobot.eventbus.EventBus;
import de.luhmer.owncloudnewsreader.events.podcast.VideoDoubleClicked;
//http://stackoverflow.com/questions/10013906/android-zoom-in-out-relativelayout-with-spread-pinch
public class ZoomableRelativeLayout extends RelativeLayout {
private static final int INVALID_POINTER_ID = -1;
private static final int INVALID_SIZE = -1;
public boolean disableScale = false;
public void setDisableScale(boolean disableScale) {
this.disableScale = disableScale;
}
private static final String TAG = "ZoomableRelativeLayout";
private GestureDetector mDoubleTapDetector;
private ScaleGestureDetector mScaleDetector;
float mScaleFactor = 1;
public float getScaleFactor() {
return mScaleFactor;
}
float mPosX;
float mPosY;
private float mLastTouchX;
private float mLastTouchY;
private int mActivePointerId;
private float mInitHeight = INVALID_SIZE;
private float mInitWidth = INVALID_SIZE;
public ZoomableRelativeLayout(Context context) {
super(context);
initZoomView(context);
}
public ZoomableRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
initZoomView(context);
}
public ZoomableRelativeLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initZoomView(context);
}
boolean mPositionReady = false;
public boolean isPositionReady() {
return mPositionReady;
}
/*
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
if(hasWindowFocus) {
readVideoPosition();
mPositionReady = true;
}
super.onWindowFocusChanged(hasWindowFocus);
}
*/
public void readVideoPosition() {
int position[] = new int[2];
getLocationOnScreen(position);
mVideoXPosition = position[0];
mVideoYPosition = position[1];
mPositionReady = true;
Log.d(TAG, "Grabbing new Video Wrapper Position. X:" + mVideoXPosition + " - Y:" + mVideoYPosition);
//mVideoXPosition = getX();
//mVideoYPosition = getY();
}
private void initZoomView(Context context) {
// Create our ScaleGestureDetector
mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
mDoubleTapDetector = new GestureDetector(context, new DoubleTapListener());
}
/*
@Override
protected void dispatchDraw(Canvas canvas) {
canvas.save(Canvas.MATRIX_SAVE_FLAG);
canvas.scale(mScaleFactor, mScaleFactor, mPosX, mPosY);
super.dispatchDraw(canvas);
canvas.restore();
}*/
private class DoubleTapListener extends GestureDetector.SimpleOnGestureListener {
// event when double tap occurs
@Override
public boolean onDoubleTap(MotionEvent e) {
float x = e.getX();
float y = e.getY();
Log.d("Double Tap", "Tapped at: (" + x + "," + y + ")");
EventBus.getDefault().post(new VideoDoubleClicked());
return true;
}
}
private float mVideoXPosition;
private float mVideoYPosition;
public float getVideoXPosition() {return mVideoXPosition;}
public float getVideoYPosition() {return mVideoYPosition;}
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
if(!disableScale) {
readVideoPosition();
}
super.onScaleEnd(detector);
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
if(disableScale)
return true;
if(mInitWidth == INVALID_SIZE) {
mInitWidth = getWidth();
mInitHeight = getHeight();
}
mScaleFactor *= detector.getScaleFactor();
// Don't let the object get too small or too large.
mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 5.0f));
if(mScaleFactor < 1)
mScaleFactor = 1;
Log.d(TAG, "Scale:" + mScaleFactor);
getLayoutParams().width = (int)(mInitWidth * mScaleFactor);
getLayoutParams().height = (int)(mInitHeight * mScaleFactor);
setLayoutParams(getLayoutParams());
//invalidate();
return true;
}
}
/*
public void restore() {
mScaleFactor = 1;
this.invalidate();
}*/
@Override
public boolean onTouchEvent(MotionEvent ev) {
// Let the ScaleGestureDetector inspect all events.
mScaleDetector.onTouchEvent(ev);
mDoubleTapDetector.onTouchEvent(ev);
final int action = ev.getAction();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: {
final float x = ev.getX();
final float y = ev.getY();
mLastTouchX = x;
mLastTouchY = y;
mActivePointerId = ev.getPointerId(0);
break;
}
case MotionEvent.ACTION_MOVE: {
final int pointerIndex = ev.findPointerIndex(mActivePointerId);
final float x = ev.getX(pointerIndex);
final float y = ev.getY(pointerIndex);
// Only move if the ScaleGestureDetector isn't processing a gesture.
if (!mScaleDetector.isInProgress()) {
final float dx = x - mLastTouchX;
final float dy = y - mLastTouchY;
mPosX += dx;
mPosY += dy;
invalidate();
}
mLastTouchX = x;
mLastTouchY = y;
break;
}
case MotionEvent.ACTION_UP: {
mActivePointerId = INVALID_POINTER_ID;
break;
}
case MotionEvent.ACTION_CANCEL: {
mActivePointerId = INVALID_POINTER_ID;
break;
}
case MotionEvent.ACTION_POINTER_UP: {
final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK)
>> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastTouchX = ev.getX(newPointerIndex);
mLastTouchY = ev.getY(newPointerIndex);
mActivePointerId = ev.getPointerId(newPointerIndex);
}
break;
}
}
return true;
}
}

View file

@ -39,16 +39,16 @@ import de.luhmer.owncloudnewsreader.R;
import de.luhmer.owncloudnewsreader.database.DatabaseConnectionOrm;
import de.luhmer.owncloudnewsreader.database.model.RssItem;
public class WidgetTodoViewsFactory implements RemoteViewsService.RemoteViewsFactory {
private static final String TAG = WidgetTodoViewsFactory.class.getCanonicalName();
public class WidgetNewsViewsFactory implements RemoteViewsService.RemoteViewsFactory {
private static final String TAG = WidgetNewsViewsFactory.class.getCanonicalName();
private DatabaseConnectionOrm dbConn;
private List<RssItem> rssItems;
private Context context = null;
private Context context;
private int appWidgetId;
public WidgetTodoViewsFactory(Context context, Intent intent) {
public WidgetNewsViewsFactory(Context context, Intent intent) {
this.context = context;
appWidgetId = intent.getExtras().getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
@ -79,14 +79,14 @@ public class WidgetTodoViewsFactory implements RemoteViewsService.RemoteViewsFac
// combination with the app widget item XML file to construct a RemoteViews object.
@SuppressLint("SimpleDateFormat")
public RemoteViews getViewAt(int position) {
if(Constants.debugModeWidget)
if(Constants.debugModeWidget) {
Log.d(TAG, "getViewAt: " + position);
}
RssItem rssItem = rssItems.get(position);
RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_item);
RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_item);
try {
RssItem rssItem = rssItems.get(position);
String header = rssItem.getFeed().getFeedTitle();
String colorString = rssItem.getFeed().getAvgColour();
@ -140,7 +140,7 @@ public class WidgetTodoViewsFactory implements RemoteViewsService.RemoteViewsFac
iCheck.putExtra(WidgetProvider.ACTION_CHECKED_CLICK, true);
rv.setOnClickFillInIntent(R.id.cb_lv_item_read, iCheck);
} catch(Exception ex) {
Log.e(TAG, "Error: " + ex.getLocalizedMessage());
Log.e(TAG, "Error while getting view for widget at position: " + position, ex);
}
// Return the RemoteViews object.

View file

@ -28,6 +28,6 @@ public class WidgetService extends RemoteViewsService {
@Override
public RemoteViewsFactory onGetViewFactory(Intent intent) {
return new WidgetTodoViewsFactory(this.getApplicationContext(), intent);
return new WidgetNewsViewsFactory(this.getApplicationContext(), intent);
}
}

View file

Before

Width:  |  Height:  |  Size: 199 B

After

Width:  |  Height:  |  Size: 199 B

View file

Before

Width:  |  Height:  |  Size: 155 B

After

Width:  |  Height:  |  Size: 155 B

View file

Before

Width:  |  Height:  |  Size: 226 B

After

Width:  |  Height:  |  Size: 226 B

View file

Before

Width:  |  Height:  |  Size: 294 B

After

Width:  |  Height:  |  Size: 294 B

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle" >
<solid android:color="#ffffff" />
<size android:width="2dp" />
</shape>

View file

@ -41,18 +41,6 @@
android:id="@+id/toolbar_layout"
layout="@layout/toolbar_layout" />
<de.luhmer.owncloudnewsreader.view.ZoomableRelativeLayout
android:id="@+id/videoPodcastSurfaceWrapper"
android:layout_width="@dimen/podcast_video_player_width"
android:layout_height="100dp"
android:background="#ff7c7c7c"
android:padding="2dp"
android:layout_gravity="bottom|end"
android:layout_marginEnd="@dimen/podcast_horizontal_margin"
android:layout_marginBottom="@dimen/activity_vertical_margin" >
</de.luhmer.owncloudnewsreader.view.ZoomableRelativeLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<FrameLayout

View file

@ -52,19 +52,6 @@
android:id="@+id/toolbar_layout"
layout="@layout/toolbar_layout" />
<de.luhmer.owncloudnewsreader.view.ZoomableRelativeLayout
android:id="@+id/videoPodcastSurfaceWrapper"
android:layout_width="@dimen/podcast_video_player_width"
android:layout_height="@dimen/podcast_video_player_height"
android:background="#ff7c7c7c"
android:padding="2dp"
android:layout_gravity="bottom|end"
android:layout_marginRight="@dimen/podcast_horizontal_margin"
android:layout_marginEnd="@dimen/podcast_horizontal_margin"
android:layout_marginBottom="@dimen/activity_vertical_margin" >
</de.luhmer.owncloudnewsreader.view.ZoomableRelativeLayout>
<de.luhmer.owncloudnewsreader.view.AnimatingProgressBar
android:id="@+id/progressIndicator"
android:layout_width="match_parent"

View file

@ -34,35 +34,6 @@
layout="@layout/toolbar_layout" />
<de.luhmer.owncloudnewsreader.view.ZoomableRelativeLayout
android:id="@+id/videoPodcastSurfaceWrapper"
android:layout_width="@dimen/podcast_video_player_width"
android:layout_height="@dimen/podcast_video_player_height"
android:background="#ff7c7c7c"
android:padding="2dp"
android:layout_gravity="bottom|end"
android:layout_marginRight="@dimen/podcast_horizontal_margin"
android:layout_marginEnd="@dimen/podcast_horizontal_margin"
android:layout_marginBottom="@dimen/activity_vertical_margin" >
<!--
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ff7c7c7c">
<fragment
android:name="com.google.android.youtube.player.YouTubePlayerFragment"
android:id="@+id/youtubeplayerfragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
-->
</de.luhmer.owncloudnewsreader.view.ZoomableRelativeLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<FrameLayout

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/layout_activity_pip"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".PiPVideoPlaybackActivity">
</RelativeLayout>

View file

@ -18,7 +18,6 @@
android:outAnimation="@android:anim/fade_out">
<!-- Default Header -->
<LinearLayout
android:layout_width="match_parent"
@ -64,6 +63,7 @@
android:layout_width="match_parent"
android:layout_height="20dp"
android:progress="0"
android:progressTint="@color/colorAccent"
android:max="100"
android:layoutDirection="ltr" />
@ -105,7 +105,7 @@
android:clickable="false"
android:focusable="false"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_action_play_arrow"
android:src="@drawable/ic_action_play"
android:tint="@color/tintColor"
android:background="?attr/selectableItemBackgroundBorderless"
android:scaleType="fitXY"
@ -274,11 +274,14 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:progress="0"
android:progressTint="@color/colorAccent"
android:thumbTint="@color/colorAccent"
android:max="100" />
<ProgressBar
android:id="@+id/pb_progress2"
style="?android:attr/progressBarStyleHorizontal"
android:progressTint="@color/colorAccent"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:layout_width="match_parent"
@ -326,7 +329,7 @@
android:layout_gravity="center_horizontal"
android:layout_marginRight="20dp"
android:layout_marginEnd="20dp"
android:src="@drawable/ic_action_play_arrow"
android:src="@drawable/ic_action_play"
android:tint="@color/tintColor"
android:background="?attr/selectableItemBackgroundBorderless"
android:scaleType="fitXY"

View file

@ -122,7 +122,7 @@
android:clickable="false"
android:focusable="false"
android:layout_gravity="center"
android:background="@drawable/ic_action_play_arrow"
android:background="@drawable/ic_action_play"
android:contentDescription="@string/content_desc_play"/>
</FrameLayout>

View file

@ -1,13 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.appbar.AppBarLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<com.google.android.material.appbar.AppBarLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:theme="@style/ToolbarTheme" />
android:theme="@style/ToolbarTheme"
app:popupTheme="@style/ToolbarOptionMenuBackgroundTheme"/>
</com.google.android.material.appbar.AppBarLayout>

View file

@ -28,6 +28,9 @@
<string name="img_view_thumbnail" translatable="false">Thumbnail</string>
<string name="tv_showing_cached_version">Showing cached version</string>
<string name="permission_req_location_twilight_title">Automated Light/Dark Theme</string>
<string name="permission_req_location_twilight_text">In order to automatically switch between the light and dark theme, it is required to provide the devices location in order to determine the time for sunrise and sunset.</string>
<!-- Action Bar Items -->
<string name="action_starred">Starred</string>
<string name="action_read">Read</string>
@ -42,6 +45,13 @@
<string name="action_textToSpeech">Read out</string>
<string name="action_search">Search</string>
<string name="action_download_articles_offline">Download articles offline</string>
<string name="news_list_drawer_text" translatable="false">Feed-list</string>
<!-- notifications -->
<string name="notification_download_articles_offline">Downloading articles for offline usage</string>
<string name="notification_download_images_offline">Downloading images for offline usage</string>
<plurals name="notification_new_items_ticker">
<item quantity="one">You have %d new unread item</item>
<item quantity="other">You have %d new unread items</item>
@ -51,7 +61,6 @@
<item quantity="other">%d new unread items available</item>
</plurals>
<!-- Add new feed -->
<string name="hint_feed_url">Feed URL</string>
<string name="action_add_feed">Add feed</string>

View file

@ -46,6 +46,10 @@
<item name="alertDialogTheme">@style/AlertDialogTheme</item>
</style>
<style name="ToolbarOptionMenuBackgroundTheme" parent="Theme.MaterialComponents.DayNight">
<item name="android:background">@color/rss_item_list_background</item>
<item name="android:itemBackground">@color/rss_item_list_background</item>
</style>
<!-- https://stackoverflow.com/a/54751236 -->
@ -61,6 +65,7 @@
<style name="ToolbarTheme" parent="ThemeOverlay.MaterialComponents.Dark.ActionBar">
<item name="android:background">@color/colorPrimary</item>
<item name="android:textColor">@color/options_menu_item_text</item>
</style>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<automotiveApp>
<uses name="media"/>
</automotiveApp>

View file

@ -1,12 +0,0 @@
package de.luhmer.owncloudnewsreader;
import android.app.Activity;
import org.greenrobot.eventbus.EventBus;
public class YoutubePlayerManager {
public static void StartYoutubePlayer(final Activity activity, int YOUTUBE_CONTENT_VIEW_ID, final EventBus eventBus, final Runnable onInitSuccess) {
// Dummy
}
}

View file

@ -1,45 +0,0 @@
package de.luhmer.owncloudnewsreader.services.podcast;
import android.content.Context;
import de.luhmer.owncloudnewsreader.model.MediaItem;
/**
* Created by david on 31.01.17.
*/
public class YoutubePlaybackService extends PlaybackService {
public YoutubePlaybackService(Context context, PodcastStatusListener podcastStatusListener, MediaItem mediaItem) {
super(podcastStatusListener, mediaItem);
setStatus(Status.FAILED);
}
@Override
public void destroy() { }
@Override
public void play() { }
@Override
public void pause() { }
@Override
public void playbackSpeedChanged(float currentPlaybackSpeed) { }
public void seekTo(double percent) { }
public int getCurrentDuration() {
return 0;
}
public int getTotalDuration() {
return 0;
}
@Override
public VideoType getVideoType() {
return VideoType.YouTube;
}
public void setYoutubePlayer(Object youTubePlayer, boolean wasRestored) { }
}

View file

@ -47,6 +47,14 @@ Download and install:
4. Import the Project in Android Studio and start coding!
Testing with Android Auto:
-----------------------
1. Open Android Studio, click on "Tools" -> "SDK Manager"
2. Select and install "Android Auto API Simulators"
3. Open terminal, go to <android-sdk>/extras/google/simulators
4. Install apk using adb (`../../../platform-tools/adb install media-browser-simulator.apk`)
5. Install apk using adb (`../../../platform-tools/adb install messaging-simulator.apk`)
That's all. I hope it works for you! If something is not working, please send me an email to david-dev@live.de

View file

@ -1,5 +1,6 @@
#Sat May 11 20:04:11 CEST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.3.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.3.1-all.zip