diff options
author | daniel oeh <daniel.oeh@gmail.com> | 2014-10-24 20:40:07 +0200 |
---|---|---|
committer | daniel oeh <daniel.oeh@gmail.com> | 2014-10-24 20:40:07 +0200 |
commit | cc052e91ad8a87b00b93649ec0f6a06bcae6267a (patch) | |
tree | 12cacac4fb5c94af2955812a3167eefb325f286d /app/src/main/java/de/danoeh | |
parent | baa7d5f11283cb7668d45b561af5d38f0ccb9632 (diff) | |
parent | b5066d02b4acf31da093190a1a57a9d961bb04ca (diff) | |
download | AntennaPod-cc052e91ad8a87b00b93649ec0f6a06bcae6267a.zip |
Merge branch 'migration' into develop
Non-GUI classes have been moved into the 'core' project in order to allow AntennaPod SP to reference it as a subproject.
Conflicts:
app/src/main/AndroidManifest.xml
build.gradle
core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java
core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSSimpleChapters.java
core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java
gradle/wrapper/gradle-wrapper.properties
pom.xml
Diffstat (limited to 'app/src/main/java/de/danoeh')
83 files changed, 13359 insertions, 0 deletions
diff --git a/app/src/main/java/de/danoeh/antennapod/AppConfig.java b/app/src/main/java/de/danoeh/antennapod/AppConfig.java new file mode 100644 index 000000000..24f13d4a3 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/AppConfig.java @@ -0,0 +1,7 @@ +package de.danoeh.antennapod; + +public final class AppConfig { + /** Should be used when setting User-Agent header for HTTP-requests. */ + public final static String USER_AGENT = "AntennaPod/0.9.9.4"; + +} diff --git a/app/src/main/java/de/danoeh/antennapod/PodcastApp.java b/app/src/main/java/de/danoeh/antennapod/PodcastApp.java new file mode 100644 index 000000000..87474dbdd --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/PodcastApp.java @@ -0,0 +1,55 @@ +package de.danoeh.antennapod; + +import android.app.Application; +import android.content.res.Configuration; + +import de.danoeh.antennapod.core.feed.EventDistributor; +import de.danoeh.antennapod.core.preferences.PlaybackPreferences; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.spa.SPAUtil; + +/** Main application class. */ +public class PodcastApp extends Application { + + // make sure that ClientConfigurator executes its static code + static { + try { + Class.forName("de.danoeh.antennapod.config.ClientConfigurator"); + } catch (Exception e) { + throw new RuntimeException("ClientConfigurator not found"); + } + } + + private static final String TAG = "PodcastApp"; + + private static float LOGICAL_DENSITY; + + private static PodcastApp singleton; + + public static PodcastApp getInstance() { + return singleton; + } + + @Override + public void onCreate() { + super.onCreate(); + singleton = this; + LOGICAL_DENSITY = getResources().getDisplayMetrics().density; + + UserPreferences.createInstance(this); + PlaybackPreferences.createInstance(this); + EventDistributor.getInstance(); + + SPAUtil.sendSPAppsQueryFeedsIntent(this); + } + + public static float getLogicalDensity() { + return LOGICAL_DENSITY; + } + + public boolean isLargeScreen() { + return (getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) == Configuration.SCREENLAYOUT_SIZE_LARGE + || (getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) == Configuration.SCREENLAYOUT_SIZE_XLARGE; + + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/activity/AboutActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/AboutActivity.java new file mode 100644 index 000000000..cf7de1709 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/activity/AboutActivity.java @@ -0,0 +1,32 @@ +package de.danoeh.antennapod.activity; + +import android.os.Bundle; +import android.support.v7.app.ActionBarActivity; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import de.danoeh.antennapod.R; + +/** Displays the 'about' screen */ +public class AboutActivity extends ActionBarActivity { + + private WebView webview; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getSupportActionBar().hide(); + setContentView(R.layout.about); + webview = (WebView) findViewById(R.id.webvAbout); + webview.setWebViewClient(new WebViewClient() { + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + view.loadUrl(url); + return false; + } + + }); + webview.loadUrl("file:///android_asset/about.html"); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java new file mode 100644 index 000000000..f9001adad --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java @@ -0,0 +1,746 @@ +package de.danoeh.antennapod.activity; + +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.content.res.TypedArray; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.v4.app.ActionBarDrawerToggle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentTransaction; +import android.support.v4.app.ListFragment; +import android.support.v4.widget.DrawerLayout; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnLongClickListener; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.ImageButton; +import android.widget.ImageView.ScaleType; +import android.widget.ListView; +import android.widget.TextView; + +import org.apache.commons.lang3.StringUtils; + +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.adapter.ChapterListAdapter; +import de.danoeh.antennapod.adapter.NavListAdapter; +import de.danoeh.antennapod.core.asynctask.PicassoProvider; +import de.danoeh.antennapod.dialog.VariableSpeedDialog; +import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.feed.EventDistributor; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.MediaType; +import de.danoeh.antennapod.core.feed.SimpleChapter; +import de.danoeh.antennapod.fragment.CoverFragment; +import de.danoeh.antennapod.fragment.ItemDescriptionFragment; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.menuhandler.NavDrawerActivity; +import de.danoeh.antennapod.core.util.playback.ExternalMedia; +import de.danoeh.antennapod.core.util.playback.Playable; +import de.danoeh.antennapod.core.util.playback.PlaybackController; + +/** + * Activity for playing audio files. + */ +public class AudioplayerActivity extends MediaplayerActivity implements ItemDescriptionFragment.ItemDescriptionFragmentCallback, + NavDrawerActivity { + private static final int POS_COVER = 0; + private static final int POS_DESCR = 1; + private static final int POS_CHAPTERS = 2; + private static final int NUM_CONTENT_FRAGMENTS = 3; + + final String TAG = "AudioplayerActivity"; + private static final String PREFS = "AudioPlayerActivityPreferences"; + private static final String PREF_KEY_SELECTED_FRAGMENT_POSITION = "selectedFragmentPosition"; + private static final String PREF_PLAYABLE_ID = "playableId"; + + private DrawerLayout drawerLayout; + private NavListAdapter navAdapter; + private ListView navList; + private ActionBarDrawerToggle drawerToggle; + + private Fragment[] detachedFragments; + + private CoverFragment coverFragment; + private ItemDescriptionFragment descriptionFragment; + private ListFragment chapterFragment; + + private Fragment currentlyShownFragment; + private int currentlyShownPosition = -1; + /** + * Used if onResume was called without loadMediaInfo. + */ + private int savedPosition = -1; + + private TextView txtvTitle; + private Button butPlaybackSpeed; + private ImageButton butNavLeft; + private ImageButton butNavRight; + + private void resetFragmentView() { + FragmentTransaction fT = getSupportFragmentManager().beginTransaction(); + + if (coverFragment != null) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Removing cover fragment"); + fT.remove(coverFragment); + } + if (descriptionFragment != null) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Removing description fragment"); + fT.remove(descriptionFragment); + } + if (chapterFragment != null) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Removing chapter fragment"); + fT.remove(chapterFragment); + } + if (currentlyShownFragment != null) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Removing currently shown fragment"); + fT.remove(currentlyShownFragment); + } + for (int i = 0; i < detachedFragments.length; i++) { + Fragment f = detachedFragments[i]; + if (f != null) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Removing detached fragment"); + fT.remove(f); + } + } + fT.commit(); + currentlyShownFragment = null; + coverFragment = null; + descriptionFragment = null; + chapterFragment = null; + currentlyShownPosition = -1; + detachedFragments = new Fragment[NUM_CONTENT_FRAGMENTS]; + } + + @Override + protected void onStop() { + super.onStop(); + if (BuildConfig.DEBUG) + Log.d(TAG, "onStop"); + cancelLoadTask(); + EventDistributor.getInstance().unregister(contentUpdate); + + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getSupportActionBar().setDisplayShowTitleEnabled(false); + detachedFragments = new Fragment[NUM_CONTENT_FRAGMENTS]; + } + + private void savePreferences() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Saving preferences"); + SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + if (currentlyShownPosition >= 0 && controller != null + && controller.getMedia() != null) { + editor.putInt(PREF_KEY_SELECTED_FRAGMENT_POSITION, + currentlyShownPosition); + editor.putString(PREF_PLAYABLE_ID, controller.getMedia() + .getIdentifier().toString()); + } else { + editor.putInt(PREF_KEY_SELECTED_FRAGMENT_POSITION, -1); + editor.putString(PREF_PLAYABLE_ID, ""); + } + editor.commit(); + + savedPosition = currentlyShownPosition; + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + drawerToggle.onConfigurationChanged(newConfig); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + // super.onSaveInstanceState(outState); would cause crash + if (BuildConfig.DEBUG) + Log.d(TAG, "onSaveInstanceState"); + + } + + @Override + protected void onPause() { + savePreferences(); + resetFragmentView(); + super.onPause(); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + restoreFromPreferences(); + } + + /** + * Tries to restore the selected fragment position from the Activity's + * preferences. + * + * @return true if restoreFromPrefernces changed the activity's state + */ + private boolean restoreFromPreferences() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Restoring instance state"); + SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE); + int savedPosition = prefs.getInt(PREF_KEY_SELECTED_FRAGMENT_POSITION, + -1); + String playableId = prefs.getString(PREF_PLAYABLE_ID, ""); + + if (savedPosition != -1 + && controller != null + && controller.getMedia() != null + && controller.getMedia().getIdentifier().toString() + .equals(playableId)) { + switchToFragment(savedPosition); + return true; + } else if (controller == null || controller.getMedia() == null) { + if (BuildConfig.DEBUG) + Log.d(TAG, + "Couldn't restore from preferences: controller or media was null"); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, + "Couldn't restore from preferences: savedPosition was -1 or saved identifier and playable identifier didn't match.\nsavedPosition: " + + savedPosition + ", id: " + playableId + ); + + } + return false; + } + + @Override + protected void onResume() { + super.onResume(); + if (StringUtils.equals(getIntent().getAction(), Intent.ACTION_VIEW)) { + Intent intent = getIntent(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Received VIEW intent: " + + intent.getData().getPath()); + ExternalMedia media = new ExternalMedia(intent.getData().getPath(), + MediaType.AUDIO); + Intent launchIntent = new Intent(this, PlaybackService.class); + launchIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media); + launchIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED, + true); + launchIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, false); + launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY, + true); + startService(launchIntent); + } + if (savedPosition != -1) { + switchToFragment(savedPosition); + } + + EventDistributor.getInstance().register(contentUpdate); + loadData(); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + setIntent(intent); + } + + @Override + protected void onAwaitingVideoSurface() { + if (BuildConfig.DEBUG) + Log.d(TAG, "onAwaitingVideoSurface was called in audio player -> switching to video player"); + startActivity(new Intent(this, VideoplayerActivity.class)); + } + + @Override + protected void postStatusMsg(int resId) { + setSupportProgressBarIndeterminateVisibility(resId == R.string.player_preparing_msg + || resId == R.string.player_seeking_msg + || resId == R.string.player_buffering_msg); + } + + @Override + protected void clearStatusMsg() { + setSupportProgressBarIndeterminateVisibility(false); + } + + /** + * Changes the currently displayed fragment. + * + * @param pos Must be POS_COVER, POS_DESCR, or POS_CHAPTERS + */ + private void switchToFragment(int pos) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Switching contentView to position " + pos); + if (currentlyShownPosition != pos && controller != null) { + Playable media = controller.getMedia(); + if (media != null) { + FragmentTransaction ft = getSupportFragmentManager() + .beginTransaction(); + if (currentlyShownFragment != null) { + detachedFragments[currentlyShownPosition] = currentlyShownFragment; + ft.detach(currentlyShownFragment); + } + switch (pos) { + case POS_COVER: + if (coverFragment == null) { + Log.i(TAG, "Using new coverfragment"); + coverFragment = CoverFragment.newInstance(media); + } + currentlyShownFragment = coverFragment; + break; + case POS_DESCR: + if (descriptionFragment == null) { + descriptionFragment = ItemDescriptionFragment + .newInstance(media, true, true); + } + currentlyShownFragment = descriptionFragment; + break; + case POS_CHAPTERS: + if (chapterFragment == null) { + chapterFragment = new ListFragment() { + + @Override + public void onListItemClick(ListView l, View v, + int position, long id) { + super.onListItemClick(l, v, position, id); + Chapter chapter = (Chapter) this + .getListAdapter().getItem(position); + controller.seekToChapter(chapter); + } + + }; + chapterFragment.setListAdapter(new ChapterListAdapter( + AudioplayerActivity.this, 0, media + .getChapters(), media + )); + } + currentlyShownFragment = chapterFragment; + break; + } + if (currentlyShownFragment != null) { + currentlyShownPosition = pos; + if (detachedFragments[pos] != null) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Reattaching fragment at position " + + pos); + ft.attach(detachedFragments[pos]); + } else { + ft.add(R.id.contentView, currentlyShownFragment); + } + ft.disallowAddToBackStack(); + ft.commit(); + updateNavButtonDrawable(); + } + } + } + } + + private void updateNavButtonDrawable() { + + final int[] buttonTexts = new int[]{R.string.show_shownotes_label, + R.string.show_chapters_label, R.string.show_cover_label}; + + final TypedArray drawables = obtainStyledAttributes(new int[]{ + R.attr.navigation_shownotes, R.attr.navigation_chapters}); + final Playable media = controller.getMedia(); + if (butNavLeft != null && butNavRight != null && media != null) { + + butNavRight.setTag(R.id.imageloader_key, null); + butNavLeft.setTag(R.id.imageloader_key, null); + + switch (currentlyShownPosition) { + case POS_COVER: + butNavLeft.setScaleType(ScaleType.CENTER); + butNavLeft.setImageDrawable(drawables.getDrawable(0)); + butNavLeft.setContentDescription(getString(buttonTexts[0])); + + butNavRight.setImageDrawable(drawables.getDrawable(1)); + butNavRight.setContentDescription(getString(buttonTexts[1])); + + break; + case POS_DESCR: + butNavLeft.setScaleType(ScaleType.CENTER_CROP); + butNavLeft.post(new Runnable() { + + @Override + public void run() { + PicassoProvider.getMediaMetadataPicassoInstance(AudioplayerActivity.this) + .load(media.getImageUri()) + .fit() + .into(butNavLeft); + } + }); + butNavLeft.setContentDescription(getString(buttonTexts[2])); + + butNavRight.setImageDrawable(drawables.getDrawable(1)); + butNavRight.setContentDescription(getString(buttonTexts[1])); + break; + case POS_CHAPTERS: + butNavLeft.setScaleType(ScaleType.CENTER_CROP); + butNavLeft.post(new Runnable() { + + @Override + public void run() { + PicassoProvider.getMediaMetadataPicassoInstance(AudioplayerActivity.this) + .load(media.getImageUri()) + .fit() + .into(butNavLeft); + } + + }); + butNavLeft.setContentDescription(getString(buttonTexts[2])); + + butNavRight.setImageDrawable(drawables.getDrawable(0)); + butNavRight.setContentDescription(getString(buttonTexts[0])); + break; + } + } + } + + @Override + protected void setupGUI() { + super.setupGUI(); + resetFragmentView(); + drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); + navList = (ListView) findViewById(R.id.nav_list); + txtvTitle = (TextView) findViewById(R.id.txtvTitle); + butNavLeft = (ImageButton) findViewById(R.id.butNavLeft); + butNavRight = (ImageButton) findViewById(R.id.butNavRight); + butPlaybackSpeed = (Button) findViewById(R.id.butPlaybackSpeed); + + TypedArray typedArray = obtainStyledAttributes(new int[]{R.attr.nav_drawer_toggle}); + drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, typedArray.getResourceId(0, 0), R.string.drawer_open, R.string.drawer_close) { + String currentTitle = getSupportActionBar().getTitle().toString(); + + @Override + public void onDrawerOpened(View drawerView) { + super.onDrawerOpened(drawerView); + currentTitle = getSupportActionBar().getTitle().toString(); + getSupportActionBar().setTitle(R.string.app_name); + supportInvalidateOptionsMenu(); + } + + @Override + public void onDrawerClosed(View drawerView) { + super.onDrawerClosed(drawerView); + getSupportActionBar().setTitle(currentTitle); + supportInvalidateOptionsMenu(); + } + }; + typedArray.recycle(); + drawerToggle.setDrawerIndicatorEnabled(false); + drawerLayout.setDrawerListener(drawerToggle); + + navAdapter = new NavListAdapter(itemAccess, this); + navList.setAdapter(navAdapter); + navList.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + int viewType = parent.getAdapter().getItemViewType(position); + if (viewType != NavListAdapter.VIEW_TYPE_SECTION_DIVIDER) { + int relPos = (viewType == NavListAdapter.VIEW_TYPE_NAV) ? position : position - NavListAdapter.SUBSCRIPTION_OFFSET; + Intent intent = new Intent(AudioplayerActivity.this, MainActivity.class); + intent.putExtra(MainActivity.EXTRA_NAV_TYPE, viewType); + intent.putExtra(MainActivity.EXTRA_NAV_INDEX, relPos); + startActivity(intent); + } + drawerLayout.closeDrawer(navList); + } + }); + drawerToggle.syncState(); + + butNavLeft.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + if (currentlyShownFragment == null + || currentlyShownPosition == POS_DESCR) { + switchToFragment(POS_COVER); + } else if (currentlyShownPosition == POS_COVER) { + switchToFragment(POS_DESCR); + } else if (currentlyShownPosition == POS_CHAPTERS) { + switchToFragment(POS_COVER); + } + } + }); + + butNavRight.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + if (currentlyShownPosition == POS_CHAPTERS) { + switchToFragment(POS_DESCR); + } else { + switchToFragment(POS_CHAPTERS); + } + } + }); + + butPlaybackSpeed.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + if (controller != null && controller.canSetPlaybackSpeed()) { + String[] availableSpeeds = UserPreferences + .getPlaybackSpeedArray(); + String currentSpeed = UserPreferences.getPlaybackSpeed(); + + // Provide initial value in case the speed list has changed + // out from under us + // and our current speed isn't in the new list + String newSpeed; + if (availableSpeeds.length > 0) { + newSpeed = availableSpeeds[0]; + } else { + newSpeed = "1.0"; + } + + for (int i = 0; i < availableSpeeds.length; i++) { + if (availableSpeeds[i].equals(currentSpeed)) { + if (i == availableSpeeds.length - 1) { + newSpeed = availableSpeeds[0]; + } else { + newSpeed = availableSpeeds[i + 1]; + } + break; + } + } + UserPreferences.setPlaybackSpeed(newSpeed); + controller.setPlaybackSpeed(Float.parseFloat(newSpeed)); + } + } + }); + + butPlaybackSpeed.setOnLongClickListener(new OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + VariableSpeedDialog.showDialog(AudioplayerActivity.this); + return true; + } + }); + } + + @Override + protected void onPlaybackSpeedChange() { + super.onPlaybackSpeedChange(); + updateButPlaybackSpeed(); + } + + private void updateButPlaybackSpeed() { + if (controller != null && controller.canSetPlaybackSpeed()) { + butPlaybackSpeed.setText(UserPreferences.getPlaybackSpeed()); + } + } + + @Override + protected void onPositionObserverUpdate() { + super.onPositionObserverUpdate(); + notifyMediaPositionChanged(); + } + + @Override + protected boolean loadMediaInfo() { + if (!super.loadMediaInfo()) { + return false; + } + final Playable media = controller.getMedia(); + if (media == null) { + return false; + } + txtvTitle.setText(media.getEpisodeTitle()); + if (media.getChapters() != null) { + butNavRight.setVisibility(View.VISIBLE); + } else { + butNavRight.setVisibility(View.INVISIBLE); + } + + + if (currentlyShownPosition == -1) { + if (!restoreFromPreferences()) { + switchToFragment(POS_COVER); + } + } + if (currentlyShownFragment instanceof AudioplayerContentFragment) { + ((AudioplayerContentFragment) currentlyShownFragment) + .onDataSetChanged(media); + } + + if (controller == null + || !controller.canSetPlaybackSpeed()) { + butPlaybackSpeed.setVisibility(View.GONE); + } else { + butPlaybackSpeed.setVisibility(View.VISIBLE); + } + + updateButPlaybackSpeed(); + return true; + } + + public void notifyMediaPositionChanged() { + if (chapterFragment != null) { + ArrayAdapter<SimpleChapter> adapter = (ArrayAdapter<SimpleChapter>) chapterFragment + .getListAdapter(); + adapter.notifyDataSetChanged(); + } + } + + @Override + protected void onReloadNotification(int notificationCode) { + if (notificationCode == PlaybackService.EXTRA_CODE_VIDEO) { + if (BuildConfig.DEBUG) + Log.d(TAG, + "ReloadNotification received, switching to Videoplayer now"); + finish(); + startActivity(new Intent(this, VideoplayerActivity.class)); + + } + } + + @Override + protected void onBufferStart() { + postStatusMsg(R.string.player_buffering_msg); + } + + @Override + protected void onBufferEnd() { + clearStatusMsg(); + } + + @Override + public PlaybackController getPlaybackController() { + return controller; + } + + @Override + public boolean isDrawerOpen() { + return drawerLayout != null && navList != null && drawerLayout.isDrawerOpen(navList); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + if (!MenuItemUtils.isActivityDrawerOpen(this)) { + return super.onCreateOptionsMenu(menu); + } else { + return false; + } + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + if (!MenuItemUtils.isActivityDrawerOpen(this)) { + return super.onPrepareOptionsMenu(menu); + } else { + return false; + } + } + + public interface AudioplayerContentFragment { + public void onDataSetChanged(Playable media); + } + + @Override + protected int getContentViewResourceId() { + return R.layout.audioplayer_activity; + } + + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (drawerToggle != null && drawerToggle.onOptionsItemSelected(item)) { + return true; + } else { + return super.onOptionsItemSelected(item); + } + } + + private DBReader.NavDrawerData navDrawerData; + private AsyncTask<Void, Void, DBReader.NavDrawerData> loadTask; + + private void loadData() { + loadTask = new AsyncTask<Void, Void, DBReader.NavDrawerData>() { + @Override + protected DBReader.NavDrawerData doInBackground(Void... params) { + return DBReader.getNavDrawerData(AudioplayerActivity.this); + } + + @Override + protected void onPostExecute(DBReader.NavDrawerData result) { + super.onPostExecute(result); + navDrawerData = result; + if (navAdapter != null) { + navAdapter.notifyDataSetChanged(); + } + } + }; + loadTask.execute(); + } + + private void cancelLoadTask() { + if (loadTask != null) { + loadTask.cancel(true); + } + } + + private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() { + + @Override + public void update(EventDistributor eventDistributor, Integer arg) { + if ((EventDistributor.FEED_LIST_UPDATE & arg) != 0) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Received contentUpdate Intent."); + loadData(); + } + } + }; + + private final NavListAdapter.ItemAccess itemAccess = new NavListAdapter.ItemAccess() { + @Override + public int getCount() { + if (navDrawerData != null) { + return navDrawerData.feeds.size(); + } else { + return 0; + } + } + + @Override + public Feed getItem(int position) { + if (navDrawerData != null && position < navDrawerData.feeds.size()) { + return navDrawerData.feeds.get(position); + } else { + return null; + } + } + + @Override + public int getSelectedItemIndex() { + return -1; + } + + @Override + public int getQueueSize() { + return (navDrawerData != null) ? navDrawerData.queueSize : 0; + } + + @Override + public int getNumberOfUnreadItems() { + return (navDrawerData != null) ? navDrawerData.numUnreadItems : 0; + } + }; +} diff --git a/app/src/main/java/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java new file mode 100644 index 000000000..5e3817796 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java @@ -0,0 +1,248 @@ +package de.danoeh.antennapod.activity; + +import android.content.Context; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.v4.app.NavUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.Spinner; +import android.widget.TextView; + +import org.apache.commons.lang3.StringUtils; +import org.jsoup.Jsoup; +import org.jsoup.examples.HtmlToPlainText; +import org.jsoup.nodes.Document; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.adapter.FeedItemlistDescriptionAdapter; +import de.danoeh.antennapod.core.asynctask.PicassoProvider; +import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator; +import de.danoeh.antennapod.core.feed.EventDistributor; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; + +/** + * Default implementation of OnlineFeedViewActivity. Shows the downloaded feed's items with their descriptions, + * a subscribe button and a spinner for choosing alternate feed URLs. + */ +public class DefaultOnlineFeedViewActivity extends OnlineFeedViewActivity { + private static final String TAG = "DefaultOnlineFeedViewActivity"; + + private static final int EVENTS = EventDistributor.DOWNLOAD_HANDLED | EventDistributor.DOWNLOAD_QUEUED | EventDistributor.FEED_LIST_UPDATE; + private volatile List<Feed> feeds; + private Feed feed; + private String selectedDownloadUrl; + + private Button subscribeButton; + + @Override + protected void onCreate(Bundle arg0) { + super.onCreate(arg0); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + Intent destIntent = new Intent(this, MainActivity.class); + if (NavUtils.shouldUpRecreateTask(this, destIntent)) { + startActivity(destIntent); + } else { + NavUtils.navigateUpFromSameTask(this); + } + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + protected void loadData() { + super.loadData(); + feeds = DBReader.getFeedList(this); + } + + @Override + protected void beforeShowFeedInformation(Feed feed, Map<String, String> alternateFeedUrls) { + super.beforeShowFeedInformation(feed, alternateFeedUrls); + + // remove HTML tags from descriptions + + if (BuildConfig.DEBUG) Log.d(TAG, "Removing HTML from shownotes"); + if (feed.getItems() != null) { + HtmlToPlainText formatter = new HtmlToPlainText(); + for (FeedItem item : feed.getItems()) { + if (item.getDescription() != null) { + Document description = Jsoup.parse(item.getDescription()); + item.setDescription(StringUtils.trim(formatter.getPlainText(description))); + } + } + } + } + + @Override + protected void showFeedInformation(final Feed feed, final Map<String, String> alternateFeedUrls) { + super.showFeedInformation(feed, alternateFeedUrls); + setContentView(R.layout.listview_activity); + + this.feed = feed; + this.selectedDownloadUrl = feed.getDownload_url(); + EventDistributor.getInstance().register(listener); + ListView listView = (ListView) findViewById(R.id.listview); + LayoutInflater inflater = (LayoutInflater) + getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View header = inflater.inflate(R.layout.onlinefeedview_header, listView, false); + listView.addHeaderView(header); + + listView.setAdapter(new FeedItemlistDescriptionAdapter(this, 0, feed.getItems())); + + ImageView cover = (ImageView) header.findViewById(R.id.imgvCover); + TextView title = (TextView) header.findViewById(R.id.txtvTitle); + TextView author = (TextView) header.findViewById(R.id.txtvAuthor); + TextView description = (TextView) header.findViewById(R.id.txtvDescription); + Spinner spAlternateUrls = (Spinner) header.findViewById(R.id.spinnerAlternateUrls); + + subscribeButton = (Button) header.findViewById(R.id.butSubscribe); + + if (feed.getImage() != null && StringUtils.isNoneBlank(feed.getImage().getDownload_url())) { + PicassoProvider.getDefaultPicassoInstance(this) + .load(feed.getImage().getDownload_url()) + .fit() + .into(cover); + } + + title.setText(feed.getTitle()); + author.setText(feed.getAuthor()); + description.setText(feed.getDescription()); + + subscribeButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + try { + Feed f = new Feed(selectedDownloadUrl, new Date(), feed.getTitle()); + f.setPreferences(feed.getPreferences()); + DefaultOnlineFeedViewActivity.this.feed = f; + + DownloadRequester.getInstance().downloadFeed( + DefaultOnlineFeedViewActivity.this, + f); + } catch (DownloadRequestException e) { + e.printStackTrace(); + DownloadRequestErrorDialogCreator.newRequestErrorDialog(DefaultOnlineFeedViewActivity.this, + e.getMessage()); + } + setSubscribeButtonState(feed); + } + }); + + if (alternateFeedUrls.isEmpty()) { + spAlternateUrls.setVisibility(View.GONE); + } else { + spAlternateUrls.setVisibility(View.VISIBLE); + + final List<String> alternateUrlsList = new ArrayList<String>(); + final List<String> alternateUrlsTitleList = new ArrayList<String>(); + + alternateUrlsList.add(feed.getDownload_url()); + alternateUrlsTitleList.add(feed.getTitle()); + + + alternateUrlsList.addAll(alternateFeedUrls.keySet()); + for (String url : alternateFeedUrls.keySet()) { + alternateUrlsTitleList.add(alternateFeedUrls.get(url)); + } + ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, alternateUrlsTitleList); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spAlternateUrls.setAdapter(adapter); + spAlternateUrls.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { + selectedDownloadUrl = alternateUrlsList.get(position); + } + + @Override + public void onNothingSelected(AdapterView<?> parent) { + + } + }); + + + } + setSubscribeButtonState(feed); + + } + + private boolean feedInFeedlist(Feed feed) { + if (feeds == null || feed == null) + return false; + for (Feed f : feeds) { + if (f.getIdentifyingValue().equals(feed.getIdentifyingValue())) { + return true; + } + } + return false; + } + + private void setSubscribeButtonState(Feed feed) { + if (subscribeButton != null && feed != null) { + if (DownloadRequester.getInstance().isDownloadingFile(feed.getDownload_url())) { + subscribeButton.setEnabled(false); + subscribeButton.setText(R.string.downloading_label); + } else if (feedInFeedlist(feed)) { + subscribeButton.setEnabled(false); + subscribeButton.setText(R.string.subscribed_label); + } else { + subscribeButton.setEnabled(true); + subscribeButton.setText(R.string.subscribe_label); + } + } + } + + EventDistributor.EventListener listener = new EventDistributor.EventListener() { + @Override + public void update(EventDistributor eventDistributor, Integer arg) { + if ((arg & EventDistributor.FEED_LIST_UPDATE) != 0) { + new AsyncTask<Void, Void, List<Feed>>() { + @Override + protected List<Feed> doInBackground(Void... params) { + return DBReader.getFeedList(DefaultOnlineFeedViewActivity.this); + } + + @Override + protected void onPostExecute(List<Feed> feeds) { + super.onPostExecute(feeds); + DefaultOnlineFeedViewActivity.this.feeds = feeds; + setSubscribeButtonState(feed); + } + }.execute(); + } else if ((arg & EVENTS) != 0) { + setSubscribeButtonState(feed); + } + } + }; + + @Override + protected void onStop() { + super.onStop(); + EventDistributor.getInstance().unregister(listener); + } +} + diff --git a/app/src/main/java/de/danoeh/antennapod/activity/DirectoryChooserActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/DirectoryChooserActivity.java new file mode 100644 index 000000000..559fa0574 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/activity/DirectoryChooserActivity.java @@ -0,0 +1,370 @@ +package de.danoeh.antennapod.activity; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.os.Environment; +import android.os.FileObserver; +import android.support.v4.app.NavUtils; +import android.support.v7.app.ActionBarActivity; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.*; +import android.widget.AdapterView.OnItemClickListener; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.preferences.UserPreferences; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; + +/** + * Let's the user choose a directory on the storage device. The selected folder + * will be sent back to the starting activity as an activity result. + */ +public class DirectoryChooserActivity extends ActionBarActivity { + private static final String TAG = "DirectoryChooserActivity"; + + private static final String CREATE_DIRECTORY_NAME = "AntennaPod"; + + public static final String RESULT_SELECTED_DIR = "selected_dir"; + public static final int RESULT_CODE_DIR_SELECTED = 1; + + private Button butConfirm; + private Button butCancel; + private ImageButton butNavUp; + private TextView txtvSelectedFolder; + private ListView listDirectories; + + private ArrayAdapter<String> listDirectoriesAdapter; + private ArrayList<String> filenames; + /** The directory that is currently being shown. */ + private File selectedDir; + private File[] filesInDir; + + private FileObserver fileObserver; + + @Override + protected void onCreate(Bundle savedInstanceState) { + setTheme(UserPreferences.getTheme()); + super.onCreate(savedInstanceState); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + setContentView(R.layout.directory_chooser); + butConfirm = (Button) findViewById(R.id.butConfirm); + butCancel = (Button) findViewById(R.id.butCancel); + butNavUp = (ImageButton) findViewById(R.id.butNavUp); + txtvSelectedFolder = (TextView) findViewById(R.id.txtvSelectedFolder); + listDirectories = (ListView) findViewById(R.id.directory_list); + + butConfirm.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + if (isValidFile(selectedDir)) { + if (selectedDir.list().length == 0) { + returnSelectedFolder(); + } else { + showNonEmptyDirectoryWarning(); + } + } + } + + private void showNonEmptyDirectoryWarning() { + AlertDialog.Builder adb = new AlertDialog.Builder( + DirectoryChooserActivity.this); + adb.setTitle(R.string.folder_not_empty_dialog_title); + adb.setMessage(R.string.folder_not_empty_dialog_msg); + adb.setNegativeButton(R.string.cancel_label, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + dialog.dismiss(); + } + }); + adb.setPositiveButton(R.string.confirm_label, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + dialog.dismiss(); + returnSelectedFolder(); + } + }); + adb.create().show(); + } + }); + + butCancel.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + setResult(Activity.RESULT_CANCELED); + finish(); + } + }); + + listDirectories.setOnItemClickListener(new OnItemClickListener() { + + @Override + public void onItemClick(AdapterView<?> adapter, View view, + int position, long id) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Selected index: " + position); + if (filesInDir != null && position >= 0 + && position < filesInDir.length) { + changeDirectory(filesInDir[position]); + } + } + }); + + butNavUp.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + File parent = null; + if (selectedDir != null + && (parent = selectedDir.getParentFile()) != null) { + changeDirectory(parent); + } + } + }); + + filenames = new ArrayList<String>(); + listDirectoriesAdapter = new ArrayAdapter<String>(this, + android.R.layout.simple_list_item_1, filenames); + listDirectories.setAdapter(listDirectoriesAdapter); + changeDirectory(Environment.getExternalStorageDirectory()); + } + + /** + * Finishes the activity and returns the selected folder as a result. The + * selected folder can also be null. + */ + private void returnSelectedFolder() { + if (selectedDir != null && BuildConfig.DEBUG) + Log.d(TAG, "Returning " + selectedDir.getAbsolutePath() + + " as result"); + Intent resultData = new Intent(); + if (selectedDir != null) { + resultData.putExtra(RESULT_SELECTED_DIR, + selectedDir.getAbsolutePath()); + } + setResult(RESULT_CODE_DIR_SELECTED, resultData); + finish(); + } + + @Override + protected void onPause() { + super.onPause(); + if (fileObserver != null) { + fileObserver.stopWatching(); + } + } + + @Override + protected void onResume() { + super.onResume(); + if (fileObserver != null) { + fileObserver.startWatching(); + } + } + + /** + * Change the directory that is currently being displayed. + * + * @param dir + * The file the activity should switch to. This File must be + * non-null and a directory, otherwise the displayed directory + * will not be changed + */ + private void changeDirectory(File dir) { + if (dir != null && dir.isDirectory()) { + File[] contents = dir.listFiles(); + if (contents != null) { + int numDirectories = 0; + for (File f : contents) { + if (f.isDirectory()) { + numDirectories++; + } + } + filesInDir = new File[numDirectories]; + filenames.clear(); + for (int i = 0, counter = 0; i < numDirectories; counter++) { + if (contents[counter].isDirectory()) { + filesInDir[i] = contents[counter]; + filenames.add(contents[counter].getName()); + i++; + } + } + Arrays.sort(filesInDir); + Collections.sort(filenames); + selectedDir = dir; + txtvSelectedFolder.setText(dir.getAbsolutePath()); + listDirectoriesAdapter.notifyDataSetChanged(); + fileObserver = createFileObserver(dir.getAbsolutePath()); + fileObserver.startWatching(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Changed directory to " + dir.getAbsolutePath()); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, + "Could not change folder: contents of dir were null"); + } + } else { + if (dir == null) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Could not change folder: dir was null"); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "Could not change folder: dir is no directory"); + } + } + refreshButtonState(); + } + + /** + * Changes the state of the buttons depending on the currently selected file + * or folder. + */ + private void refreshButtonState() { + if (selectedDir != null) { + butConfirm.setEnabled(isValidFile(selectedDir)); + supportInvalidateOptionsMenu(); + } + } + + /** Refresh the contents of the directory that is currently shown. */ + private void refreshDirectory() { + if (selectedDir != null) { + changeDirectory(selectedDir); + } + } + + /** Sets up a FileObserver to watch the current directory. */ + private FileObserver createFileObserver(String path) { + return new FileObserver(path, FileObserver.CREATE | FileObserver.DELETE + | FileObserver.MOVED_FROM | FileObserver.MOVED_TO) { + + @Override + public void onEvent(int event, String path) { + if (BuildConfig.DEBUG) + Log.d(TAG, "FileObserver received event " + event); + runOnUiThread(new Runnable() { + + @Override + public void run() { + refreshDirectory(); + } + }); + } + }; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + super.onPrepareOptionsMenu(menu); + menu.findItem(R.id.new_folder_item) + .setVisible(isValidFile(selectedDir)); + return true; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.directory_chooser, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + NavUtils.navigateUpFromSameTask(this); + return true; + case R.id.new_folder_item: + openNewFolderDialog(); + return true; + case R.id.set_to_default_folder_item: + selectedDir = null; + returnSelectedFolder(); + return true; + default: + return false; + } + } + + /** + * Shows a confirmation dialog that asks the user if he wants to create a + * new folder. + */ + private void openNewFolderDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.create_folder_label); + builder.setMessage(String.format(getString(R.string.create_folder_msg), + CREATE_DIRECTORY_NAME)); + builder.setNegativeButton(R.string.cancel_label, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }); + builder.setPositiveButton(R.string.confirm_label, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + int msg = createFolder(); + Toast t = Toast.makeText(DirectoryChooserActivity.this, + msg, Toast.LENGTH_SHORT); + t.show(); + } + }); + builder.create().show(); + } + + /** + * Creates a new folder in the current directory with the name + * CREATE_DIRECTORY_NAME. + */ + private int createFolder() { + if (selectedDir == null) { + return R.string.create_folder_error; + } else if (selectedDir.canWrite()) { + File newDir = new File(selectedDir, CREATE_DIRECTORY_NAME); + if (!newDir.exists()) { + boolean result = newDir.mkdir(); + if (result) { + return R.string.create_folder_success; + } else { + return R.string.create_folder_error; + } + } else { + return R.string.create_folder_error_already_exists; + } + } else { + return R.string.create_folder_error_no_write_access; + } + } + + /** Returns true if the selected file or directory would be valid selection. */ + private boolean isValidFile(File file) { + return (file != null && file.isDirectory() && file.canRead() && file + .canWrite()); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java new file mode 100644 index 000000000..365c4216d --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java @@ -0,0 +1,110 @@ +package de.danoeh.antennapod.activity; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.support.v7.app.ActionBarActivity; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; + +import org.apache.commons.lang3.Validate; + +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.service.download.DownloadRequest; +import de.danoeh.antennapod.core.storage.DownloadRequester; + +/** + * Shows a username and a password text field. + * The activity MUST be started with the ARG_DOWNlOAD_REQUEST argument set to a non-null value. + * Other arguments are optional. + * The activity's result will be the same DownloadRequest with the entered username and password. + */ +public class DownloadAuthenticationActivity extends ActionBarActivity { + private static final String TAG = "DownloadAuthenticationActivity"; + + /** + * The download request object that contains information about the resource that requires a username and a password + */ + public static final String ARG_DOWNLOAD_REQUEST = "request"; + /** + * True if the request should be sent to the DownloadRequester when this activity is finished, false otherwise. + * The default value is false. + */ + public static final String ARG_SEND_TO_DOWNLOAD_REQUESTER_BOOL = "send_to_downloadrequester"; + + public static final String RESULT_REQUEST = "request"; + + private EditText etxtUsername; + private EditText etxtPassword; + private Button butConfirm; + private Button butCancel; + private TextView txtvDescription; + + private DownloadRequest request; + private boolean sendToDownloadRequester; + + @Override + protected void onCreate(Bundle savedInstanceState) { + setTheme(UserPreferences.getTheme()); + super.onCreate(savedInstanceState); + getSupportActionBar().hide(); + setContentView(R.layout.download_authentication_activity); + + etxtUsername = (EditText) findViewById(R.id.etxtUsername); + etxtPassword = (EditText) findViewById(R.id.etxtPassword); + butConfirm = (Button) findViewById(R.id.butConfirm); + butCancel = (Button) findViewById(R.id.butCancel); + txtvDescription = (TextView) findViewById(R.id.txtvDescription); + + Validate.isTrue(getIntent().hasExtra(ARG_DOWNLOAD_REQUEST), "Download request missing"); + + request = getIntent().getParcelableExtra(ARG_DOWNLOAD_REQUEST); + sendToDownloadRequester = getIntent().getBooleanExtra(ARG_SEND_TO_DOWNLOAD_REQUESTER_BOOL, false); + + if (savedInstanceState != null) { + etxtUsername.setText(savedInstanceState.getString("username")); + etxtPassword.setText(savedInstanceState.getString("password")); + } + + txtvDescription.setText(txtvDescription.getText() + ":\n\n" + request.getTitle()); + + butCancel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + setResult(Activity.RESULT_CANCELED); + finish(); + } + }); + + butConfirm.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + String username = etxtUsername.getText().toString(); + String password = etxtPassword.getText().toString(); + request.setUsername(username); + request.setPassword(password); + Intent result = new Intent(); + result.putExtra(RESULT_REQUEST, request); + setResult(Activity.RESULT_OK, result); + + if (sendToDownloadRequester) { + if (BuildConfig.DEBUG) Log.d(TAG, "Sending request to DownloadRequester"); + DownloadRequester.getInstance().download(DownloadAuthenticationActivity.this, request); + } + finish(); + } + }); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putString("username", etxtUsername.getText().toString()); + outState.putString("password", etxtPassword.getText().toString()); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/activity/FeedInfoActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/FeedInfoActivity.java new file mode 100644 index 000000000..3000cfaeb --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/activity/FeedInfoActivity.java @@ -0,0 +1,192 @@ +package de.danoeh.antennapod.activity; + +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.v7.app.ActionBarActivity; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.widget.*; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.asynctask.PicassoProvider; +import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedPreferences; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.util.LangUtils; +import de.danoeh.antennapod.menuhandler.FeedMenuHandler; + +/** + * Displays information about a feed. + */ +public class FeedInfoActivity extends ActionBarActivity { + private static final String TAG = "FeedInfoActivity"; + + public static final String EXTRA_FEED_ID = "de.danoeh.antennapod.extra.feedId"; + + private Feed feed; + + private ImageView imgvCover; + private TextView txtvTitle; + private TextView txtvDescription; + private TextView txtvLanguage; + private TextView txtvAuthor; + private EditText etxtUsername; + private EditText etxtPassword; + private CheckBox cbxAutoDownload; + + @Override + protected void onCreate(Bundle savedInstanceState) { + setTheme(UserPreferences.getTheme()); + super.onCreate(savedInstanceState); + setContentView(R.layout.feedinfo); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + long feedId = getIntent().getLongExtra(EXTRA_FEED_ID, -1); + + imgvCover = (ImageView) findViewById(R.id.imgvCover); + txtvTitle = (TextView) findViewById(R.id.txtvTitle); + txtvDescription = (TextView) findViewById(R.id.txtvDescription); + txtvLanguage = (TextView) findViewById(R.id.txtvLanguage); + txtvAuthor = (TextView) findViewById(R.id.txtvAuthor); + cbxAutoDownload = (CheckBox) findViewById(R.id.cbxAutoDownload); + etxtUsername = (EditText) findViewById(R.id.etxtUsername); + etxtPassword = (EditText) findViewById(R.id.etxtPassword); + + AsyncTask<Long, Void, Feed> loadTask = new AsyncTask<Long, Void, Feed>() { + + @Override + protected Feed doInBackground(Long... params) { + return DBReader.getFeed(FeedInfoActivity.this, params[0]); + } + + @Override + protected void onPostExecute(Feed result) { + if (result != null) { + feed = result; + if (BuildConfig.DEBUG) + Log.d(TAG, "Language is " + feed.getLanguage()); + if (BuildConfig.DEBUG) + Log.d(TAG, "Author is " + feed.getAuthor()); + imgvCover.post(new Runnable() { + + @Override + public void run() { + PicassoProvider.getDefaultPicassoInstance(FeedInfoActivity.this) + .load(feed.getImageUri()) + .fit() + .into(imgvCover); + } + }); + + txtvTitle.setText(feed.getTitle()); + txtvDescription.setText(feed.getDescription()); + if (feed.getAuthor() != null) { + txtvAuthor.setText(feed.getAuthor()); + } + if (feed.getLanguage() != null) { + txtvLanguage.setText(LangUtils + .getLanguageString(feed.getLanguage())); + } + + cbxAutoDownload.setEnabled(UserPreferences.isEnableAutodownload()); + cbxAutoDownload.setChecked(feed.getPreferences().getAutoDownload()); + cbxAutoDownload.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean checked) { + feed.getPreferences().setAutoDownload(checked); + feed.savePreferences(FeedInfoActivity.this); + } + }); + + etxtUsername.setText(feed.getPreferences().getUsername()); + etxtPassword.setText(feed.getPreferences().getPassword()); + + etxtUsername.addTextChangedListener(authTextWatcher); + etxtPassword.addTextChangedListener(authTextWatcher); + + supportInvalidateOptionsMenu(); + + } else { + Log.e(TAG, "Activity was started with invalid arguments"); + } + } + }; + loadTask.execute(feedId); + } + + + private boolean authInfoChanged = false; + + private TextWatcher authTextWatcher = new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + + } + + @Override + public void afterTextChanged(Editable s) { + authInfoChanged = true; + } + }; + + @Override + protected void onPause() { + super.onPause(); + if (feed != null && authInfoChanged) { + Log.d(TAG, "Auth info changed, saving credentials"); + FeedPreferences prefs = feed.getPreferences(); + prefs.setUsername(etxtUsername.getText().toString()); + prefs.setPassword(etxtPassword.getText().toString()); + DBWriter.setFeedPreferences(this, prefs); + authInfoChanged = false; + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.feedinfo, menu); + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + super.onPrepareOptionsMenu(menu); + menu.findItem(R.id.support_item).setVisible( + feed != null && feed.getPaymentLink() != null); + menu.findItem(R.id.share_link_item).setVisible(feed != null &&feed.getLink() != null); + menu.findItem(R.id.visit_website_item).setVisible(feed != null && feed.getLink() != null); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + finish(); + return true; + default: + try { + return FeedMenuHandler.onOptionsItemClicked(this, item, feed); + } catch (DownloadRequestException e) { + e.printStackTrace(); + DownloadRequestErrorDialogCreator.newRequestErrorDialog(this, + e.getMessage()); + } + return super.onOptionsItemSelected(item); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/activity/FlattrAuthActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/FlattrAuthActivity.java new file mode 100644 index 000000000..f4a973fac --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/activity/FlattrAuthActivity.java @@ -0,0 +1,125 @@ +package de.danoeh.antennapod.activity; + + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.v7.app.ActionBarActivity; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.TextView; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.util.flattr.FlattrUtils; +import org.shredzone.flattr4j.exception.FlattrException; + +/** Guides the user through the authentication process */ + +public class FlattrAuthActivity extends ActionBarActivity { + private static final String TAG = "FlattrAuthActivity"; + + private TextView txtvExplanation; + private Button butAuthenticate; + private Button butReturn; + + private boolean authSuccessful; + + private static FlattrAuthActivity singleton; + + @Override + protected void onCreate(Bundle savedInstanceState) { + setTheme(UserPreferences.getTheme()); + super.onCreate(savedInstanceState); + singleton = this; + authSuccessful = false; + if (BuildConfig.DEBUG) Log.d(TAG, "Activity created"); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + setContentView(R.layout.flattr_auth); + txtvExplanation = (TextView) findViewById(R.id.txtvExplanation); + butAuthenticate = (Button) findViewById(R.id.but_authenticate); + butReturn = (Button) findViewById(R.id.but_return_home); + + butReturn.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(FlattrAuthActivity.this, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + } + }); + + butAuthenticate.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + try { + FlattrUtils.startAuthProcess(FlattrAuthActivity.this); + } catch (FlattrException e) { + e.printStackTrace(); + } + } + }); + } + + public static FlattrAuthActivity getInstance() { + return singleton; + } + + @Override + protected void onResume() { + super.onResume(); + if (BuildConfig.DEBUG) Log.d(TAG, "Activity resumed"); + Uri uri = getIntent().getData(); + if (uri != null) { + if (BuildConfig.DEBUG) Log.d(TAG, "Received uri"); + FlattrUtils.handleCallback(this, uri); + } + } + + public void handleAuthenticationSuccess() { + authSuccessful = true; + txtvExplanation.setText(R.string.flattr_auth_success); + butAuthenticate.setEnabled(false); + butReturn.setVisibility(View.VISIBLE); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + return true; + } + + + + @Override + protected void onPause() { + super.onPause(); + if (authSuccessful) { + finish(); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + if (authSuccessful) { + Intent intent = new Intent(this, PreferenceActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + } else { + finish(); + } + break; + default: + return false; + } + return true; + } + + +} diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java new file mode 100644 index 000000000..7029fd32c --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java @@ -0,0 +1,432 @@ +package de.danoeh.antennapod.activity; + +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.content.res.TypedArray; +import android.media.AudioManager; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Handler; +import android.support.v4.app.ActionBarDrawerToggle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; +import android.support.v4.widget.DrawerLayout; +import android.support.v7.app.ActionBar; +import android.support.v7.app.ActionBarActivity; +import android.util.Log; +import android.view.*; +import android.widget.AdapterView; +import android.widget.ListView; + +import org.apache.commons.lang3.Validate; + +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.adapter.NavListAdapter; +import de.danoeh.antennapod.core.feed.EventDistributor; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.fragment.*; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.util.StorageUtils; +import de.danoeh.antennapod.menuhandler.NavDrawerActivity; + +import java.util.List; + +/** + * The activity that is shown when the user launches the app. + */ +public class MainActivity extends ActionBarActivity implements NavDrawerActivity{ + private static final String TAG = "MainActivity"; + private static final int EVENTS = EventDistributor.DOWNLOAD_HANDLED + | EventDistributor.DOWNLOAD_QUEUED + | EventDistributor.FEED_LIST_UPDATE + | EventDistributor.UNREAD_ITEMS_UPDATE + | EventDistributor.QUEUE_UPDATE; + + public static final String PREF_NAME = "MainActivityPrefs"; + public static final String PREF_IS_FIRST_LAUNCH = "prefMainActivityIsFirstLaunch"; + + public static final String EXTRA_NAV_INDEX = "nav_index"; + public static final String EXTRA_NAV_TYPE = "nav_type"; + public static final String EXTRA_FRAGMENT_ARGS = "fragment_args"; + + public static final int POS_NEW = 0, + POS_QUEUE = 1, + POS_DOWNLOADS = 2, + POS_HISTORY = 3, + POS_ADD = 4; + + private ExternalPlayerFragment externalPlayerFragment; + private DrawerLayout drawerLayout; + + private ListView navList; + private NavListAdapter navAdapter; + + private ActionBarDrawerToggle drawerToggle; + + private CharSequence drawerTitle; + private CharSequence currentTitle; + + + @Override + public void onCreate(Bundle savedInstanceState) { + setTheme(UserPreferences.getTheme()); + super.onCreate(savedInstanceState); + supportRequestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + StorageUtils.checkStorageAvailability(this); + setContentView(R.layout.main); + setVolumeControlStream(AudioManager.STREAM_MUSIC); + + drawerTitle = currentTitle = getTitle(); + + drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); + navList = (ListView) findViewById(R.id.nav_list); + + TypedArray typedArray = obtainStyledAttributes(new int[]{R.attr.nav_drawer_toggle}); + drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, typedArray.getResourceId(0, 0), R.string.drawer_open, R.string.drawer_close) { + @Override + public void onDrawerOpened(View drawerView) { + super.onDrawerOpened(drawerView); + currentTitle = getSupportActionBar().getTitle(); + getSupportActionBar().setTitle(drawerTitle); + supportInvalidateOptionsMenu(); + } + + @Override + public void onDrawerClosed(View drawerView) { + super.onDrawerClosed(drawerView); + getSupportActionBar().setTitle(currentTitle); + supportInvalidateOptionsMenu(); + + } + }; + typedArray.recycle(); + + drawerLayout.setDrawerListener(drawerToggle); + FragmentManager fm = getSupportFragmentManager(); + + FragmentTransaction transaction = fm.beginTransaction(); + + Fragment mainFragment = fm.findFragmentByTag("main"); + if (mainFragment != null) { + transaction.replace(R.id.main_view, mainFragment); + } else { + loadFragment(NavListAdapter.VIEW_TYPE_NAV, POS_NEW, null); + } + + externalPlayerFragment = new ExternalPlayerFragment(); + transaction.replace(R.id.playerFragment, externalPlayerFragment); + transaction.commit(); + + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setHomeButtonEnabled(true); + + navAdapter = new NavListAdapter(itemAccess, this); + navList.setAdapter(navAdapter); + navList.setOnItemClickListener(navListClickListener); + + checkFirstLaunch(); + } + + private void checkFirstLaunch() { + SharedPreferences prefs = getSharedPreferences(PREF_NAME, MODE_PRIVATE); + if (prefs.getBoolean(PREF_IS_FIRST_LAUNCH, true)) { + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + drawerLayout.openDrawer(navList); + } + }, 1500); + + SharedPreferences.Editor edit = prefs.edit(); + edit.putBoolean(PREF_IS_FIRST_LAUNCH, false); + edit.commit(); + } + } + + public ActionBar getMainActivtyActionBar() { + return getSupportActionBar(); + } + + public boolean isDrawerOpen() { + return drawerLayout != null && navList != null && drawerLayout.isDrawerOpen(navList); + } + + public List<Feed> getFeeds() { + return (navDrawerData != null) ? navDrawerData.feeds : null; + } + + private void loadFragment(int viewType, int relPos, Bundle args) { + FragmentManager fragmentManager = getSupportFragmentManager(); + // clear back stack + for (int i = 0; i < fragmentManager.getBackStackEntryCount(); i++) { + fragmentManager.popBackStack(); + } + + FragmentTransaction fT = fragmentManager.beginTransaction(); + Fragment fragment = null; + if (viewType == NavListAdapter.VIEW_TYPE_NAV) { + switch (relPos) { + case POS_NEW: + fragment = new NewEpisodesFragment(); + break; + case POS_QUEUE: + fragment = new QueueFragment(); + break; + case POS_DOWNLOADS: + fragment = new DownloadsFragment(); + break; + case POS_HISTORY: + fragment = new PlaybackHistoryFragment(); + break; + case POS_ADD: + fragment = new AddFeedFragment(); + break; + + } + currentTitle = getString(NavListAdapter.NAV_TITLES[relPos]); + selectedNavListIndex = relPos; + + } else if (viewType == NavListAdapter.VIEW_TYPE_SUBSCRIPTION) { + Feed feed = itemAccess.getItem(relPos); + currentTitle = ""; + fragment = ItemlistFragment.newInstance(feed.getId()); + selectedNavListIndex = NavListAdapter.SUBSCRIPTION_OFFSET + relPos; + + } + if (fragment != null) { + if (args != null) { + fragment.setArguments(args); + } + fT.replace(R.id.main_view, fragment, "main"); + fragmentManager.popBackStack(); + } + fT.commit(); + getSupportActionBar().setTitle(currentTitle); + if (navAdapter != null) { + navAdapter.notifyDataSetChanged(); + } + } + + public void loadNavFragment(int position, Bundle args) { + loadFragment(NavListAdapter.VIEW_TYPE_NAV, position, args); + } + + public void loadFeedFragment(long feedID) { + if (navDrawerData != null) { + for (int i = 0; i < navDrawerData.feeds.size(); i++) { + if (navDrawerData.feeds.get(i).getId() == feedID) { + loadFragment(NavListAdapter.VIEW_TYPE_SUBSCRIPTION, i, null); + break; + } + } + } + } + + public void loadChildFragment(Fragment fragment) { + Validate.notNull(fragment); + FragmentManager fm = getSupportFragmentManager(); + fm.beginTransaction() + .replace(R.id.main_view, fragment, "main") + .addToBackStack(null) + .commit(); + } + + private AdapterView.OnItemClickListener navListClickListener = new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + int viewType = parent.getAdapter().getItemViewType(position); + if (viewType != NavListAdapter.VIEW_TYPE_SECTION_DIVIDER && position != selectedNavListIndex) { + int relPos = (viewType == NavListAdapter.VIEW_TYPE_NAV) ? position : position - NavListAdapter.SUBSCRIPTION_OFFSET; + loadFragment(viewType, relPos, null); + selectedNavListIndex = position; + navAdapter.notifyDataSetChanged(); + } + drawerLayout.closeDrawer(navList); + } + }; + + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + drawerToggle.syncState(); + if (savedInstanceState != null) { + currentTitle = savedInstanceState.getString("title"); + if (!drawerLayout.isDrawerOpen(navList)) { + getSupportActionBar().setTitle(currentTitle); + } + selectedNavListIndex = savedInstanceState.getInt("selectedNavIndex"); + } + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + drawerToggle.onConfigurationChanged(newConfig); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putString("title", getSupportActionBar().getTitle().toString()); + outState.putInt("selectedNavIndex", selectedNavListIndex); + + } + + @Override + protected void onPause() { + super.onPause(); + } + + @Override + protected void onResume() { + super.onResume(); + StorageUtils.checkStorageAvailability(this); + EventDistributor.getInstance().register(contentUpdate); + + Intent intent = getIntent(); + if (navDrawerData != null && intent.hasExtra(EXTRA_NAV_INDEX) && intent.hasExtra(EXTRA_NAV_TYPE)) { + handleNavIntent(); + } + + loadData(); + } + + @Override + protected void onStop() { + super.onStop(); + cancelLoadTask(); + EventDistributor.getInstance().unregister(contentUpdate); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (drawerToggle.onOptionsItemSelected(item)) { + return true; + } + switch (item.getItemId()) { + case R.id.show_preferences: + startActivity(new Intent(this, PreferenceActivity.class)); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + super.onPrepareOptionsMenu(menu); + return true; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.main, menu); + return true; + } + + private DBReader.NavDrawerData navDrawerData; + private AsyncTask<Void, Void, DBReader.NavDrawerData> loadTask; + private int selectedNavListIndex = 0; + + private NavListAdapter.ItemAccess itemAccess = new NavListAdapter.ItemAccess() { + @Override + public int getCount() { + if (navDrawerData != null) { + return navDrawerData.feeds.size(); + } else { + return 0; + } + } + + @Override + public Feed getItem(int position) { + if (navDrawerData != null && position < navDrawerData.feeds.size()) { + return navDrawerData.feeds.get(position); + } else { + return null; + } + } + + @Override + public int getSelectedItemIndex() { + return selectedNavListIndex; + } + + @Override + public int getQueueSize() { + return (navDrawerData != null) ? navDrawerData.queueSize : 0; + } + + @Override + public int getNumberOfUnreadItems() { + return (navDrawerData != null) ? navDrawerData.numUnreadItems : 0; + } + + + }; + + private void loadData() { + cancelLoadTask(); + loadTask = new AsyncTask<Void, Void, DBReader.NavDrawerData>() { + @Override + protected DBReader.NavDrawerData doInBackground(Void... params) { + return DBReader.getNavDrawerData(MainActivity.this); + } + + @Override + protected void onPostExecute(DBReader.NavDrawerData result) { + super.onPostExecute(navDrawerData); + boolean handleIntent = (navDrawerData == null); + + navDrawerData = result; + navAdapter.notifyDataSetChanged(); + + if (handleIntent) { + handleNavIntent(); + } + } + }; + loadTask.execute(); + } + + private void cancelLoadTask() { + if (loadTask != null) { + loadTask.cancel(true); + } + } + + private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() { + + @Override + public void update(EventDistributor eventDistributor, Integer arg) { + if ((EVENTS & arg) != 0) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Received contentUpdate Intent."); + loadData(); + } + } + }; + + private void handleNavIntent() { + Intent intent = getIntent(); + if (intent.hasExtra(EXTRA_NAV_INDEX) && intent.hasExtra(EXTRA_NAV_TYPE)) { + int index = intent.getIntExtra(EXTRA_NAV_INDEX, 0); + int type = intent.getIntExtra(EXTRA_NAV_TYPE, NavListAdapter.VIEW_TYPE_NAV); + Bundle args = intent.getBundleExtra(EXTRA_FRAGMENT_ARGS); + loadFragment(type, index, args); + } + setIntent(new Intent(MainActivity.this, MainActivity.class)); // to avoid handling the intent twice when the configuration changes + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + setIntent(intent); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java new file mode 100644 index 000000000..14cb2727f --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java @@ -0,0 +1,522 @@ +package de.danoeh.antennapod.activity; + +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.graphics.PixelFormat; +import android.media.AudioManager; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.support.v7.app.ActionBarActivity; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.Window; +import android.widget.ImageButton; +import android.widget.SeekBar; +import android.widget.SeekBar.OnSeekBarChangeListener; +import android.widget.TextView; +import com.doomonafireball.betterpickers.hmspicker.HmsPickerBuilder; +import com.doomonafireball.betterpickers.hmspicker.HmsPickerDialogFragment; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.dialog.TimeDialog; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.util.Converter; +import de.danoeh.antennapod.core.util.ShareUtils; +import de.danoeh.antennapod.core.util.StorageUtils; +import de.danoeh.antennapod.core.util.playback.MediaPlayerError; +import de.danoeh.antennapod.core.util.playback.Playable; +import de.danoeh.antennapod.core.util.playback.PlaybackController; + +/** + * Provides general features which are both needed for playing audio and video + * files. + */ +public abstract class MediaplayerActivity extends ActionBarActivity + implements OnSeekBarChangeListener { + private static final String TAG = "MediaplayerActivity"; + + protected PlaybackController controller; + + protected TextView txtvPosition; + protected TextView txtvLength; + protected SeekBar sbPosition; + protected ImageButton butPlay; + protected ImageButton butRev; + protected ImageButton butFF; + + private PlaybackController newPlaybackController() { + return new PlaybackController(this, false) { + + @Override + public void setupGUI() { + MediaplayerActivity.this.setupGUI(); + } + + @Override + public void onPositionObserverUpdate() { + MediaplayerActivity.this.onPositionObserverUpdate(); + } + + @Override + public void onBufferStart() { + MediaplayerActivity.this.onBufferStart(); + } + + @Override + public void onBufferEnd() { + MediaplayerActivity.this.onBufferEnd(); + } + + @Override + public void onBufferUpdate(float progress) { + MediaplayerActivity.this.onBufferUpdate(progress); + } + + @Override + public void handleError(int code) { + MediaplayerActivity.this.handleError(code); + } + + @Override + public void onReloadNotification(int code) { + MediaplayerActivity.this.onReloadNotification(code); + } + + @Override + public void onSleepTimerUpdate() { + supportInvalidateOptionsMenu(); + } + + @Override + public ImageButton getPlayButton() { + return butPlay; + } + + @Override + public void postStatusMsg(int msg) { + MediaplayerActivity.this.postStatusMsg(msg); + } + + @Override + public void clearStatusMsg() { + MediaplayerActivity.this.clearStatusMsg(); + } + + @Override + public boolean loadMediaInfo() { + return MediaplayerActivity.this.loadMediaInfo(); + } + + @Override + public void onAwaitingVideoSurface() { + MediaplayerActivity.this.onAwaitingVideoSurface(); + } + + @Override + public void onServiceQueried() { + MediaplayerActivity.this.onServiceQueried(); + } + + @Override + public void onShutdownNotification() { + finish(); + } + + @Override + public void onPlaybackEnd() { + finish(); + } + + @Override + public void onPlaybackSpeedChange() { + MediaplayerActivity.this.onPlaybackSpeedChange(); + } + + @Override + protected void setScreenOn(boolean enable) { + super.setScreenOn(enable); + MediaplayerActivity.this.setScreenOn(enable); + } + }; + + } + + protected void onPlaybackSpeedChange() { + + } + + protected void onServiceQueried() { + supportInvalidateOptionsMenu(); + } + + protected void chooseTheme() { + setTheme(UserPreferences.getTheme()); + } + + protected void setScreenOn(boolean enable) { + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + chooseTheme(); + super.onCreate(savedInstanceState); + + // subclasses might use this feature + supportRequestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + + if (BuildConfig.DEBUG) + Log.d(TAG, "Creating Activity"); + StorageUtils.checkStorageAvailability(this); + setVolumeControlStream(AudioManager.STREAM_MUSIC); + + orientation = getResources().getConfiguration().orientation; + getWindow().setFormat(PixelFormat.TRANSPARENT); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + + @Override + protected void onPause() { + super.onPause(); + controller.reinitServiceIfPaused(); + controller.pause(); + } + + /** + * Should be used to switch to another player activity if the mime type is + * not the correct one for the current activity. + */ + protected abstract void onReloadNotification(int notificationCode); + + /** + * Should be used to inform the user that the PlaybackService is currently + * buffering. + */ + protected abstract void onBufferStart(); + + /** + * Should be used to hide the view that was showing the 'buffering'-message. + */ + protected abstract void onBufferEnd(); + + protected void onBufferUpdate(float progress) { + if (sbPosition != null) { + sbPosition.setSecondaryProgress((int) progress + * sbPosition.getMax()); + } + } + + /** + * Current screen orientation. + */ + protected int orientation; + + @Override + protected void onStart() { + super.onStart(); + if (controller != null) { + controller.release(); + } + controller = newPlaybackController(); + } + + @Override + protected void onStop() { + super.onStop(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Activity stopped"); + if (controller != null) { + controller.release(); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Activity destroyed"); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.mediaplayer, menu); + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + super.onPrepareOptionsMenu(menu); + Playable media = controller.getMedia(); + + menu.findItem(R.id.support_item).setVisible( + media != null && media.getPaymentLink() != null && + (media instanceof FeedMedia) && + ((FeedMedia) media).getItem().getFlattrStatus().flattrable() + ); + menu.findItem(R.id.share_link_item).setVisible( + media != null && media.getWebsiteLink() != null); + menu.findItem(R.id.visit_website_item).setVisible( + media != null && media.getWebsiteLink() != null); + menu.findItem(R.id.skip_episode_item).setVisible(media != null); + boolean sleepTimerSet = controller.sleepTimerActive(); + boolean sleepTimerNotSet = controller.sleepTimerNotActive(); + menu.findItem(R.id.set_sleeptimer_item).setVisible(sleepTimerNotSet); + menu.findItem(R.id.disable_sleeptimer_item).setVisible(sleepTimerSet); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + Playable media = controller.getMedia(); + if (item.getItemId() == android.R.id.home) { + Intent intent = new Intent(MediaplayerActivity.this, + MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP + | Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + return true; + } else if (media != null) { + switch (item.getItemId()) { + case R.id.disable_sleeptimer_item: + if (controller.serviceAvailable()) { + AlertDialog.Builder stDialog = new AlertDialog.Builder(this); + stDialog.setTitle(R.string.sleep_timer_label); + stDialog.setMessage(getString(R.string.time_left_label) + + Converter.getDurationStringLong((int) controller + .getSleepTimerTimeLeft())); + stDialog.setPositiveButton( + R.string.disable_sleeptimer_label, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + dialog.dismiss(); + controller.disableSleepTimer(); + } + } + ); + stDialog.setNegativeButton(R.string.cancel_label, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + dialog.dismiss(); + } + } + ); + stDialog.create().show(); + } + break; + case R.id.set_sleeptimer_item: + if (controller.serviceAvailable()) { + int pickerStyle = (UserPreferences.getTheme() == R.style.Theme_AntennaPod_Light) ? + R.style.AntennaPodBetterPickerThemeLight : R.style.AntennaPodBetterPickerThemeDark; + if (Build.VERSION.SDK_INT > 10) { // TODO remove this as soon as dialog is shown correctly on 2.3 + HmsPickerBuilder hpb = new HmsPickerBuilder() + .setStyleResId(pickerStyle) + .setFragmentManager(getSupportFragmentManager()); + + hpb.addHmsPickerDialogHandler(new HmsPickerDialogFragment.HmsPickerDialogHandler() { + @Override + public void onDialogHmsSet(int ref, int hours, int minutes, int seconds) { + if (controller != null && controller.serviceAvailable()) { + controller.setSleepTimer((hours * 3600 + minutes * 60 + seconds) * 1000); + } + } + }); + hpb.show(); + } else { + TimeDialog td = new TimeDialog(this, + R.string.set_sleeptimer_label, + R.string.set_sleeptimer_label) { + + @Override + public void onTimeEntered(long millis) { + controller.setSleepTimer(millis); + } + }; + td.show(); + } + break; + + } + case R.id.visit_website_item: + Uri uri = Uri.parse(media.getWebsiteLink()); + startActivity(new Intent(Intent.ACTION_VIEW, uri)); + break; + case R.id.support_item: + if (media instanceof FeedMedia) { + FeedItem feedItem = ((FeedMedia) media).getItem(); + DBTasks.flattrItemIfLoggedIn(this, feedItem); + } + break; + case R.id.share_link_item: + ShareUtils.shareLink(this, media.getWebsiteLink()); + break; + case R.id.skip_episode_item: + sendBroadcast(new Intent( + PlaybackService.ACTION_SKIP_CURRENT_EPISODE)); + break; + default: + return false; + + } + return true; + } else { + return false; + } + } + + @Override + protected void onResume() { + super.onResume(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Resuming Activity"); + StorageUtils.checkStorageAvailability(this); + controller.init(); + } + + /** + * Called by 'handleStatus()' when the PlaybackService is waiting for + * a video surface. + */ + protected abstract void onAwaitingVideoSurface(); + + protected abstract void postStatusMsg(int resId); + + protected abstract void clearStatusMsg(); + + protected void onPositionObserverUpdate() { + if (controller != null) { + int currentPosition = controller.getPosition(); + int duration = controller.getDuration(); + if (currentPosition != PlaybackService.INVALID_TIME + && duration != PlaybackService.INVALID_TIME + && controller.getMedia() != null) { + txtvPosition.setText(Converter + .getDurationStringLong(currentPosition)); + txtvLength.setText(Converter.getDurationStringLong(duration)); + updateProgressbarPosition(currentPosition, duration); + } else { + Log.w(TAG, + "Could not react to position observer update because of invalid time"); + } + } + } + + private void updateProgressbarPosition(int position, int duration) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Updating progressbar info"); + float progress = ((float) position) / duration; + sbPosition.setProgress((int) (progress * sbPosition.getMax())); + } + + /** + * Load information about the media that is going to be played or currently + * being played. This method will be called when the activity is connected + * to the PlaybackService to ensure that the activity has the right + * FeedMedia object. + */ + protected boolean loadMediaInfo() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Loading media info"); + Playable media = controller.getMedia(); + if (media != null) { + txtvPosition.setText(Converter.getDurationStringLong((media + .getPosition()))); + + if (media.getDuration() != 0) { + txtvLength.setText(Converter.getDurationStringLong(media + .getDuration())); + float progress = ((float) media.getPosition()) + / media.getDuration(); + sbPosition.setProgress((int) (progress * sbPosition.getMax())); + } + return true; + } else { + return false; + } + } + + protected void setupGUI() { + setContentView(getContentViewResourceId()); + sbPosition = (SeekBar) findViewById(R.id.sbPosition); + txtvPosition = (TextView) findViewById(R.id.txtvPosition); + txtvLength = (TextView) findViewById(R.id.txtvLength); + butPlay = (ImageButton) findViewById(R.id.butPlay); + butRev = (ImageButton) findViewById(R.id.butRev); + butFF = (ImageButton) findViewById(R.id.butFF); + + // SEEKBAR SETUP + + sbPosition.setOnSeekBarChangeListener(this); + + // BUTTON SETUP + + butPlay.setOnClickListener(controller.newOnPlayButtonClickListener()); + + if (butFF != null) { + butFF.setOnClickListener(controller.newOnFFButtonClickListener()); + } + if (butRev != null) { + butRev.setOnClickListener(controller.newOnRevButtonClickListener()); + } + + } + + protected abstract int getContentViewResourceId(); + + void handleError(int errorCode) { + final AlertDialog.Builder errorDialog = new AlertDialog.Builder(this); + errorDialog.setTitle(R.string.error_label); + errorDialog + .setMessage(MediaPlayerError.getErrorString(this, errorCode)); + errorDialog.setNeutralButton("OK", + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + finish(); + } + } + ); + errorDialog.create().show(); + } + + float prog; + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, + boolean fromUser) { + if (controller != null) { + prog = controller.onSeekBarProgressChanged(seekBar, progress, fromUser, + txtvPosition); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + if (controller != null) { + controller.onSeekBarStartTrackingTouch(seekBar); + } + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + if (controller != null) { + controller.onSeekBarStopTrackingTouch(seekBar, prog); + } + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java new file mode 100644 index 000000000..d84e6cc03 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java @@ -0,0 +1,428 @@ +package de.danoeh.antennapod.activity; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnCancelListener; +import android.content.Intent; +import android.os.Bundle; +import android.support.v7.app.ActionBarActivity; +import android.util.Log; +import android.widget.ArrayAdapter; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.RelativeLayout; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.dialog.AuthenticationDialog; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedPreferences; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.service.download.DownloadRequest; +import de.danoeh.antennapod.core.service.download.DownloadStatus; +import de.danoeh.antennapod.core.service.download.Downloader; +import de.danoeh.antennapod.core.service.download.HttpDownloader; +import de.danoeh.antennapod.core.syndication.handler.FeedHandler; +import de.danoeh.antennapod.core.syndication.handler.FeedHandlerResult; +import de.danoeh.antennapod.core.syndication.handler.UnsupportedFeedtypeException; +import de.danoeh.antennapod.core.util.DownloadError; +import de.danoeh.antennapod.core.util.FileNameGenerator; +import de.danoeh.antennapod.core.util.StorageUtils; +import de.danoeh.antennapod.core.util.URLChecker; +import de.danoeh.antennapod.core.util.syndication.FeedDiscoverer; +import org.apache.commons.lang3.StringUtils; +import org.xml.sax.SAXException; + +import javax.xml.parsers.ParserConfigurationException; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * Downloads a feed from a feed URL and parses it. Subclasses can display the + * feed object that was parsed. This activity MUST be started with a given URL + * or an Exception will be thrown. + * <p/> + * If the feed cannot be downloaded or parsed, an error dialog will be displayed + * and the activity will finish as soon as the error dialog is closed. + */ +public abstract class OnlineFeedViewActivity extends ActionBarActivity { + private static final String TAG = "OnlineFeedViewActivity"; + public static final String ARG_FEEDURL = "arg.feedurl"; + + /** + * Optional argument: specify a title for the actionbar. + */ + public static final String ARG_TITLE = "title"; + + public static final int RESULT_ERROR = 2; + + private Feed feed; + private Map<String, String> alternateFeedUrls; + private Downloader downloader; + + private boolean isPaused; + + @Override + protected void onCreate(Bundle savedInstanceState) { + setTheme(UserPreferences.getTheme()); + super.onCreate(savedInstanceState); + + if (getIntent() != null && getIntent().hasExtra(ARG_TITLE)) { + getSupportActionBar().setTitle(getIntent().getStringExtra(ARG_TITLE)); + } + + StorageUtils.checkStorageAvailability(this); + + final String feedUrl; + if (getIntent().hasExtra(ARG_FEEDURL)) { + feedUrl = getIntent().getStringExtra(ARG_FEEDURL); + } else if (StringUtils.equals(getIntent().getAction(), Intent.ACTION_SEND) + || StringUtils.equals(getIntent().getAction(), Intent.ACTION_VIEW)) { + feedUrl = (StringUtils.equals(getIntent().getAction(), Intent.ACTION_SEND)) + ? getIntent().getStringExtra(Intent.EXTRA_TEXT) : getIntent().getDataString(); + + getSupportActionBar().setTitle(R.string.add_new_feed_label); + } else { + throw new IllegalArgumentException( + "Activity must be started with feedurl argument!"); + } + + if (BuildConfig.DEBUG) + Log.d(TAG, "Activity was started with url " + feedUrl); + setLoadingLayout(); + if (savedInstanceState == null) { + startFeedDownload(feedUrl, null, null); + } else { + startFeedDownload(feedUrl, savedInstanceState.getString("username"), savedInstanceState.getString("password")); + } + } + + @Override + protected void onResume() { + super.onResume(); + isPaused = false; + } + + @Override + protected void onPause() { + super.onPause(); + isPaused = true; + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (feed != null && feed.getPreferences() != null) { + outState.putString("username", feed.getPreferences().getUsername()); + outState.putString("password", feed.getPreferences().getPassword()); + } + } + + @Override + protected void onStop() { + super.onStop(); + if (downloader != null && !downloader.isFinished()) { + downloader.cancel(); + } + } + + private void resetIntent(String url, String title) { + Intent intent = new Intent(); + intent.putExtra(ARG_FEEDURL, url); + intent.putExtra(ARG_TITLE, title); + setIntent(intent); + } + + + private void onDownloadCompleted(final Downloader downloader) { + runOnUiThread(new Runnable() { + + @Override + public void run() { + if (BuildConfig.DEBUG) Log.d(TAG, "Download was completed"); + DownloadStatus status = downloader.getResult(); + if (status != null) { + if (!status.isCancelled()) { + if (status.isSuccessful()) { + parseFeed(); + } else if (status.getReason() == DownloadError.ERROR_UNAUTHORIZED) { + if (!isFinishing() && !isPaused) { + Dialog dialog = new FeedViewAuthenticationDialog(OnlineFeedViewActivity.this, + R.string.authentication_notification_title, downloader.getDownloadRequest().getSource()); + dialog.show(); + } + } else { + String errorMsg = status.getReason().getErrorString( + OnlineFeedViewActivity.this); + if (errorMsg != null + && status.getReasonDetailed() != null) { + errorMsg += " (" + + status.getReasonDetailed() + ")"; + } + showErrorDialog(errorMsg); + } + } + } else { + Log.wtf(TAG, + "DownloadStatus returned by Downloader was null"); + finish(); + } + } + }); + + } + + private void startFeedDownload(String url, String username, String password) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Starting feed download"); + url = URLChecker.prepareURL(url); + feed = new Feed(url, new Date()); + if (username != null && password != null) { + feed.setPreferences(new FeedPreferences(0, true, username, password)); + } + String fileUrl = new File(getExternalCacheDir(), + FileNameGenerator.generateFileName(feed.getDownload_url())) + .toString(); + feed.setFile_url(fileUrl); + final DownloadRequest request = new DownloadRequest(feed.getFile_url(), + feed.getDownload_url(), "OnlineFeed", 0, Feed.FEEDFILETYPE_FEED, username, password, true); + downloader = new HttpDownloader( + request); + new Thread() { + @Override + public void run() { + loadData(); + downloader.call(); + onDownloadCompleted(downloader); + } + }.start(); + + + } + + /** + * Displays a progress indicator. + */ + private void setLoadingLayout() { + RelativeLayout rl = new RelativeLayout(this); + RelativeLayout.LayoutParams rlLayoutParams = new RelativeLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.MATCH_PARENT); + + ProgressBar pb = new ProgressBar(this); + pb.setIndeterminate(true); + RelativeLayout.LayoutParams pbLayoutParams = new RelativeLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + pbLayoutParams.addRule(RelativeLayout.CENTER_IN_PARENT); + rl.addView(pb, pbLayoutParams); + addContentView(rl, rlLayoutParams); + } + + private void parseFeed() { + if (feed == null || feed.getFile_url() == null && feed.isDownloaded()) { + throw new IllegalStateException( + "feed must be non-null and downloaded when parseFeed is called"); + } + + if (BuildConfig.DEBUG) + Log.d(TAG, "Parsing feed"); + + Thread thread = new Thread() { + + @Override + public void run() { + String reasonDetailed = ""; + boolean successful = false; + FeedHandler handler = new FeedHandler(); + try { + FeedHandlerResult result = handler.parseFeed(feed); + feed = result.feed; + alternateFeedUrls = result.alternateFeedUrls; + successful = true; + } catch (SAXException e) { + e.printStackTrace(); + reasonDetailed = e.getMessage(); + } catch (IOException e) { + e.printStackTrace(); + reasonDetailed = e.getMessage(); + } catch (ParserConfigurationException e) { + e.printStackTrace(); + reasonDetailed = e.getMessage(); + } catch (UnsupportedFeedtypeException e) { + if (BuildConfig.DEBUG) Log.d(TAG, "Unsupported feed type detected"); + if (StringUtils.equalsIgnoreCase("html", e.getRootElement())) { + if (showFeedDiscoveryDialog(new File(feed.getFile_url()), feed.getDownload_url())) { + return; + } + } else { + e.printStackTrace(); + reasonDetailed = e.getMessage(); + } + } finally { + boolean rc = new File(feed.getFile_url()).delete(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Deleted feed source file. Result: " + rc); + } + + if (successful) { + beforeShowFeedInformation(feed, alternateFeedUrls); + runOnUiThread(new Runnable() { + @Override + public void run() { + showFeedInformation(feed, alternateFeedUrls); + } + }); + } else { + final String errorMsg = + DownloadError.ERROR_PARSER_EXCEPTION.getErrorString( + OnlineFeedViewActivity.this) + + " (" + reasonDetailed + ")"; + runOnUiThread(new Runnable() { + + @Override + public void run() { + showErrorDialog(errorMsg); + } + }); + } + } + }; + thread.start(); + } + + /** + * Can be used to load data asynchronously. + */ + protected void loadData() { + + } + + /** + * Called after the feed has been downloaded and parsed and before showFeedInformation is called. + * This method is executed on a background thread + */ + protected void beforeShowFeedInformation(Feed feed, Map<String, String> alternateFeedUrls) { + + } + + /** + * Called when feed parsed successfully. + * This method is executed on the GUI thread. + */ + protected void showFeedInformation(Feed feed, Map<String, String> alternateFeedUrls) { + + } + + private void showErrorDialog(String errorMsg) { + if (!isFinishing() && !isPaused) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.error_label); + if (errorMsg != null) { + builder.setMessage(getString(R.string.error_msg_prefix) + errorMsg); + } else { + builder.setMessage(R.string.error_msg_prefix); + } + builder.setNeutralButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + } + ); + builder.setOnCancelListener(new OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + setResult(RESULT_ERROR); + finish(); + } + }); + builder.show(); + } + } + + private boolean showFeedDiscoveryDialog(File feedFile, String baseUrl) { + FeedDiscoverer fd = new FeedDiscoverer(); + final Map<String, String> urlsMap; + try { + urlsMap = fd.findLinks(feedFile, baseUrl); + if (urlsMap == null || urlsMap.isEmpty()) { + return false; + } + } catch (IOException e) { + e.printStackTrace(); + return false; + } + + runOnUiThread(new Runnable() { + @Override + public void run() { + if (isPaused || isFinishing()) { + return; + } + + final List<String> titles = new ArrayList<String>(); + final List<String> urls = new ArrayList<String>(); + + urls.addAll(urlsMap.keySet()); + for (String url : urls) { + titles.add(urlsMap.get(url)); + } + + final ArrayAdapter<String> adapter = new ArrayAdapter<String>(OnlineFeedViewActivity.this, R.layout.ellipsize_start_listitem, R.id.txtvTitle, titles); + DialogInterface.OnClickListener onClickListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String selectedUrl = urls.get(which); + dialog.dismiss(); + resetIntent(selectedUrl, titles.get(which)); + startFeedDownload(selectedUrl, null, null); + } + }; + + AlertDialog.Builder ab = new AlertDialog.Builder(OnlineFeedViewActivity.this) + .setTitle(R.string.feeds_label) + .setCancelable(true) + .setOnCancelListener(new OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + finish(); + } + }) + .setAdapter(adapter, onClickListener); + ab.show(); + } + }); + + + return true; + } + + private class FeedViewAuthenticationDialog extends AuthenticationDialog { + + private String feedUrl; + + public FeedViewAuthenticationDialog(Context context, int titleRes, String feedUrl) { + super(context, titleRes, true, false, null, null); + this.feedUrl = feedUrl; + } + + @Override + protected void onCancelled() { + super.onCancelled(); + finish(); + } + + @Override + protected void onConfirmed(String username, String password, boolean saveUsernamePassword) { + startFeedDownload(feedUrl, username, password); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OpmlFeedChooserActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OpmlFeedChooserActivity.java new file mode 100644 index 000000000..8a9ec58d3 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/activity/OpmlFeedChooserActivity.java @@ -0,0 +1,134 @@ +package de.danoeh.antennapod.activity; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.view.MenuItemCompat; +import android.support.v7.app.ActionBarActivity; +import android.util.SparseBooleanArray; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.ListView; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.opml.OpmlElement; +import de.danoeh.antennapod.core.preferences.UserPreferences; + +import java.util.ArrayList; +import java.util.List; + +/** + * Displays the feeds that the OPML-Importer has read and lets the user choose + * which feeds he wants to import. + */ +public class OpmlFeedChooserActivity extends ActionBarActivity { + private static final String TAG = "OpmlFeedChooserActivity"; + + public static final String EXTRA_SELECTED_ITEMS = "de.danoeh.antennapod.selectedItems"; + + private Button butConfirm; + private Button butCancel; + private ListView feedlist; + private ArrayAdapter<String> listAdapter; + + @Override + protected void onCreate(Bundle savedInstanceState) { + setTheme(UserPreferences.getTheme()); + super.onCreate(savedInstanceState); + + setContentView(R.layout.opml_selection); + butConfirm = (Button) findViewById(R.id.butConfirm); + butCancel = (Button) findViewById(R.id.butCancel); + feedlist = (ListView) findViewById(R.id.feedlist); + + feedlist.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); + listAdapter = new ArrayAdapter<String>(this, + android.R.layout.simple_list_item_multiple_choice, + getTitleList()); + + feedlist.setAdapter(listAdapter); + + butCancel.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + setResult(RESULT_CANCELED); + finish(); + } + }); + + butConfirm.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + Intent intent = new Intent(); + SparseBooleanArray checked = feedlist.getCheckedItemPositions(); + + int checkedCount = 0; + // Get number of checked items + for (int i = 0; i < checked.size(); i++) { + if (checked.valueAt(i)) { + checkedCount++; + } + } + int[] selection = new int[checkedCount]; + for (int i = 0, collected = 0; collected < checkedCount; i++) { + if (checked.valueAt(i)) { + selection[collected] = checked.keyAt(i); + collected++; + } + } + intent.putExtra(EXTRA_SELECTED_ITEMS, selection); + setResult(RESULT_OK, intent); + finish(); + } + }); + + } + + private List<String> getTitleList() { + List<String> result = new ArrayList<String>(); + if (OpmlImportHolder.getReadElements() != null) { + for (OpmlElement element : OpmlImportHolder.getReadElements()) { + result.add(element.getText()); + } + + } + return result; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + MenuItemCompat.setShowAsAction(menu.add(Menu.NONE, R.id.select_all_item, Menu.NONE, + R.string.select_all_label), + MenuItemCompat.SHOW_AS_ACTION_IF_ROOM); + + MenuItemCompat.setShowAsAction(menu.add(Menu.NONE, R.id.deselect_all_item, Menu.NONE, + R.string.deselect_all_label), + MenuItemCompat.SHOW_AS_ACTION_IF_ROOM); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.select_all_item: + selectAllItems(true); + return true; + case R.id.deselect_all_item: + selectAllItems(false); + return true; + default: + return false; + } + } + + private void selectAllItems(boolean b) { + for (int i = 0; i < feedlist.getCount(); i++) { + feedlist.setItemChecked(i, b); + } + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportBaseActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportBaseActivity.java new file mode 100644 index 000000000..d974e0e1b --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportBaseActivity.java @@ -0,0 +1,90 @@ +package de.danoeh.antennapod.activity; + +import android.content.Intent; +import android.support.v7.app.ActionBarActivity; +import android.util.Log; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.asynctask.OpmlFeedQueuer; +import de.danoeh.antennapod.asynctask.OpmlImportWorker; +import de.danoeh.antennapod.core.opml.OpmlElement; + +import java.io.Reader; +import java.util.ArrayList; + +/** + * Base activity for Opml Import - e.g. with code what to do afterwards + * */ +public class OpmlImportBaseActivity extends ActionBarActivity { + + private static final String TAG = "OpmlImportBaseActivity"; + private OpmlImportWorker importWorker; + + /** + * Handles the choices made by the user in the OpmlFeedChooserActivity and + * starts the OpmlFeedQueuer if necessary. + */ + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Received result"); + if (resultCode == RESULT_CANCELED) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Activity was cancelled"); + if (finishWhenCanceled()) + finish(); + } else { + int[] selected = data + .getIntArrayExtra(OpmlFeedChooserActivity.EXTRA_SELECTED_ITEMS); + if (selected != null && selected.length > 0) { + OpmlFeedQueuer queuer = new OpmlFeedQueuer(this, selected) { + + @Override + protected void onPostExecute(Void result) { + super.onPostExecute(result); + Intent intent = new Intent(OpmlImportBaseActivity.this, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP + | Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + } + + }; + queuer.executeAsync(); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "No items were selected"); + } + } + } + + /** Starts the import process. */ + protected void startImport(Reader reader) { + + if (reader != null) { + importWorker = new OpmlImportWorker(this, reader) { + + @Override + protected void onPostExecute(ArrayList<OpmlElement> result) { + super.onPostExecute(result); + if (result != null) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Parsing was successful"); + OpmlImportHolder.setReadElements(result); + startActivityForResult(new Intent( + OpmlImportBaseActivity.this, + OpmlFeedChooserActivity.class), 0); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "Parser error occurred"); + } + } + }; + importWorker.executeAsync(); + } + } + + protected boolean finishWhenCanceled() { + return false; + } + + +} diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromIntentActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromIntentActivity.java new file mode 100644 index 000000000..e42072ead --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromIntentActivity.java @@ -0,0 +1,38 @@ +package de.danoeh.antennapod.activity; + +import android.app.AlertDialog; +import android.os.Bundle; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.util.LangUtils; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.URL; + +/** Lets the user start the OPML-import process. */ +public class OpmlImportFromIntentActivity extends OpmlImportBaseActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + setTheme(UserPreferences.getTheme()); + super.onCreate(savedInstanceState); + + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + try { + URL mOpmlURL = new URL(getIntent().getData().toString()); + BufferedReader in = new BufferedReader(new InputStreamReader(mOpmlURL.openStream(), + LangUtils.UTF_8)); + startImport(in); + } catch (Exception e) { + new AlertDialog.Builder(this).setMessage("Cannot open XML - Reason: " + e.getMessage()).show(); + } + + } + + @Override + protected boolean finishWhenCanceled() { + return true; + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java new file mode 100644 index 000000000..162a8f2e5 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java @@ -0,0 +1,171 @@ +package de.danoeh.antennapod.activity; + +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.util.LangUtils; +import de.danoeh.antennapod.core.util.StorageUtils; + +import java.io.*; + +/** + * Lets the user start the OPML-import process from a path + */ +public class OpmlImportFromPathActivity extends OpmlImportBaseActivity { + private static final String TAG = "OpmlImportFromPathActivity"; + private TextView txtvPath; + private Button butStart; + private String importPath; + + @Override + protected void onCreate(Bundle savedInstanceState) { + setTheme(UserPreferences.getTheme()); + super.onCreate(savedInstanceState); + + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + setContentView(R.layout.opml_import); + + txtvPath = (TextView) findViewById(R.id.txtvPath); + butStart = (Button) findViewById(R.id.butStartImport); + + butStart.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + checkFolderForFiles(); + } + + }); + } + + @Override + protected void onResume() { + super.onResume(); + StorageUtils.checkStorageAvailability(this); + setImportPath(); + } + + /** + * Sets the importPath variable and makes txtvPath display the import + * directory. + */ + private void setImportPath() { + File importDir = UserPreferences.getDataFolder(this, UserPreferences.IMPORT_DIR); + boolean success = true; + if (!importDir.exists()) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Import directory doesn't exist. Creating..."); + success = importDir.mkdir(); + if (!success) { + Log.e(TAG, "Could not create directory"); + } + } + if (success) { + txtvPath.setText(importDir.toString()); + importPath = importDir.toString(); + } else { + txtvPath.setText(R.string.opml_directory_error); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + finish(); + return true; + default: + return false; + } + } + + /** + * Looks at the contents of the import directory and decides what to do. If + * more than one file is in the directory, a dialog will be created to let + * the user choose which item to import + */ + private void checkFolderForFiles() { + File dir = new File(importPath); + if (dir.isDirectory()) { + File[] fileList = dir.listFiles(); + if (fileList.length == 1) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Found one file, choosing that one."); + startImport(fileList[0]); + } else if (fileList.length > 1) { + Log.w(TAG, "Import directory contains more than one file."); + askForFile(dir); + } else { + Log.e(TAG, "Import directory is empty"); + Toast toast = Toast + .makeText(this, R.string.opml_import_error_dir_empty, + Toast.LENGTH_LONG); + toast.show(); + } + } + } + + private void startImport(File file) { + Reader mReader = null; + try { + mReader = new InputStreamReader(new FileInputStream(file), + LangUtils.UTF_8); + if (BuildConfig.DEBUG) Log.d(TAG, "Parsing " + file.toString()); + startImport(mReader); + } catch (FileNotFoundException e) { + Log.d(TAG, "File not found which really should be there"); + // this should never happen as it is a file we have just chosen + } + } + + /** + * Asks the user to choose from a list of files in a directory and returns + * his choice. + */ + private void askForFile(File dir) { + final File[] fileList = dir.listFiles(); + String[] fileNames = dir.list(); + + AlertDialog.Builder dialog = new AlertDialog.Builder(this); + dialog.setTitle(R.string.choose_file_to_import_label); + dialog.setNeutralButton(android.R.string.cancel, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Dialog was cancelled"); + dialog.dismiss(); + } + }); + dialog.setItems(fileNames, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + if (BuildConfig.DEBUG) + Log.d(TAG, "File at index " + which + " was chosen"); + dialog.dismiss(); + startImport(fileList[which]); + } + }); + dialog.create().show(); + } + + +} diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportHolder.java b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportHolder.java new file mode 100644 index 000000000..7afa270cc --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportHolder.java @@ -0,0 +1,29 @@ +package de.danoeh.antennapod.activity; + +import de.danoeh.antennapod.core.opml.OpmlElement; + +import java.util.ArrayList; + +/** + * Hold infos gathered by Ompl-Import + * <p/> + * Created with IntelliJ IDEA. + * User: ligi + * Date: 1/23/13 + * Time: 2:15 PM + */ +public class OpmlImportHolder { + + private static ArrayList<OpmlElement> readElements; + + public static ArrayList<OpmlElement> getReadElements() { + return readElements; + } + + public static void setReadElements(ArrayList<OpmlElement> _readElements) { + readElements = _readElements; + } + + +} + diff --git a/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java new file mode 100644 index 000000000..95e352874 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java @@ -0,0 +1,532 @@ +package de.danoeh.antennapod.activity; + +import android.annotation.SuppressLint; +import android.app.ActionBar; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.res.Resources.Theme; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiManager; +import android.os.Bundle; +import android.os.Build; +import android.preference.CheckBoxPreference; +import android.preference.ListPreference; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceChangeListener; +import android.preference.Preference.OnPreferenceClickListener; +import android.preference.PreferenceScreen; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.Toast; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.asynctask.FlattrClickWorker; +import de.danoeh.antennapod.asynctask.OpmlExportWorker; +import de.danoeh.antennapod.dialog.AuthenticationDialog; +import de.danoeh.antennapod.dialog.AutoFlattrPreferenceDialog; +import de.danoeh.antennapod.dialog.GpodnetSetHostnameDialog; +import de.danoeh.antennapod.dialog.VariableSpeedDialog; +import de.danoeh.antennapod.core.preferences.GpodnetPreferences; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.util.flattr.FlattrStatus; +import de.danoeh.antennapod.core.util.flattr.FlattrUtils; +import de.danoeh.antennapod.core.util.flattr.SimpleFlattrThing; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * The main preference activity + */ +public class PreferenceActivity extends android.preference.PreferenceActivity { + private static final String TAG = "PreferenceActivity"; + + private static final String PREF_FLATTR_THIS_APP = "prefFlattrThisApp"; + private static final String PREF_FLATTR_SETTINGS = "prefFlattrSettings"; + private static final String PREF_FLATTR_AUTH = "pref_flattr_authenticate"; + private static final String PREF_FLATTR_REVOKE = "prefRevokeAccess"; + private static final String PREF_AUTO_FLATTR_PREFS = "prefAutoFlattrPrefs"; + private static final String PREF_OPML_EXPORT = "prefOpmlExport"; + private static final String PREF_ABOUT = "prefAbout"; + private static final String PREF_CHOOSE_DATA_DIR = "prefChooseDataDir"; + private static final String AUTO_DL_PREF_SCREEN = "prefAutoDownloadSettings"; + private static final String PREF_PLAYBACK_SPEED_LAUNCHER = "prefPlaybackSpeedLauncher"; + + private static final String PREF_GPODNET_LOGIN = "pref_gpodnet_authenticate"; + private static final String PREF_GPODNET_SETLOGIN_INFORMATION = "pref_gpodnet_setlogin_information"; + private static final String PREF_GPODNET_LOGOUT = "pref_gpodnet_logout"; + private static final String PREF_GPODNET_HOSTNAME = "pref_gpodnet_hostname"; + + private static final String PREF_EXPANDED_NOTIFICATION = "prefExpandNotify"; + private static final String PREF_PERSISTENT_NOTIFICATION = "prefPersistNotify"; + + private CheckBoxPreference[] selectedNetworks; + + @SuppressLint("NewApi") + @SuppressWarnings("deprecation") + @Override + public void onCreate(Bundle savedInstanceState) { + setTheme(UserPreferences.getTheme()); + super.onCreate(savedInstanceState); + + if (android.os.Build.VERSION.SDK_INT >= 11) { + @SuppressLint("AppCompatMethod") ActionBar ab = getActionBar(); + if (ab != null) { + ab.setDisplayHomeAsUpEnabled(true); + } + } + + addPreferencesFromResource(R.xml.preferences); + + if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + // disable expanded notification option on unsupported android versions + findPreference(PREF_EXPANDED_NOTIFICATION).setEnabled(false); + findPreference(PREF_EXPANDED_NOTIFICATION).setOnPreferenceClickListener( + new OnPreferenceClickListener() { + + @Override + public boolean onPreferenceClick(Preference preference) { + Toast toast = Toast.makeText(PreferenceActivity.this, R.string.pref_expand_notify_unsupport_toast, Toast.LENGTH_SHORT); + toast.show(); + return true; + } + } + ); + } + + findPreference(PREF_FLATTR_THIS_APP).setOnPreferenceClickListener( + new OnPreferenceClickListener() { + + @Override + public boolean onPreferenceClick(Preference preference) { + new FlattrClickWorker(PreferenceActivity.this, + new SimpleFlattrThing(PreferenceActivity.this.getString(R.string.app_name), + FlattrUtils.APP_URL, + new FlattrStatus(FlattrStatus.STATUS_QUEUE) + ) + ).executeAsync(); + + return true; + } + } + ); + + findPreference(PREF_FLATTR_REVOKE).setOnPreferenceClickListener( + new OnPreferenceClickListener() { + + @Override + public boolean onPreferenceClick(Preference preference) { + FlattrUtils.revokeAccessToken(PreferenceActivity.this); + checkItemVisibility(); + return true; + } + + } + ); + + findPreference(PREF_ABOUT).setOnPreferenceClickListener( + new OnPreferenceClickListener() { + + @Override + public boolean onPreferenceClick(Preference preference) { + PreferenceActivity.this.startActivity(new Intent( + PreferenceActivity.this, AboutActivity.class)); + return true; + } + + } + ); + + findPreference(PREF_OPML_EXPORT).setOnPreferenceClickListener( + new OnPreferenceClickListener() { + + @Override + public boolean onPreferenceClick(Preference preference) { + new OpmlExportWorker(PreferenceActivity.this) + .executeAsync(); + + return true; + } + } + ); + + findPreference(PREF_CHOOSE_DATA_DIR).setOnPreferenceClickListener( + new OnPreferenceClickListener() { + + @Override + public boolean onPreferenceClick(Preference preference) { + startActivityForResult( + new Intent(PreferenceActivity.this, + DirectoryChooserActivity.class), + DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED + ); + return true; + } + } + ); + findPreference(UserPreferences.PREF_THEME) + .setOnPreferenceChangeListener( + new OnPreferenceChangeListener() { + + @Override + public boolean onPreferenceChange( + Preference preference, Object newValue) { + Intent i = getIntent(); + i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK + | Intent.FLAG_ACTIVITY_NEW_TASK); + finish(); + startActivity(i); + return true; + } + } + ); + findPreference(UserPreferences.PREF_ENABLE_AUTODL) + .setOnPreferenceChangeListener(new OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (newValue instanceof Boolean) { + findPreference(UserPreferences.PREF_ENABLE_AUTODL_WIFI_FILTER).setEnabled((Boolean) newValue); + setSelectedNetworksEnabled((Boolean) newValue && UserPreferences.isEnableAutodownloadWifiFilter()); + } + return true; + } + }); + findPreference(UserPreferences.PREF_ENABLE_AUTODL_WIFI_FILTER) + .setOnPreferenceChangeListener( + new OnPreferenceChangeListener() { + + @Override + public boolean onPreferenceChange( + Preference preference, Object newValue) { + if (newValue instanceof Boolean) { + setSelectedNetworksEnabled((Boolean) newValue); + return true; + } else { + return false; + } + } + } + ); + findPreference(UserPreferences.PREF_EPISODE_CACHE_SIZE) + .setOnPreferenceChangeListener( + new OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object o) { + if (o instanceof String) { + setEpisodeCacheSizeText(UserPreferences.readEpisodeCacheSize((String) o)); + } + return true; + } + } + ); + findPreference(PREF_PLAYBACK_SPEED_LAUNCHER) + .setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + VariableSpeedDialog.showDialog(PreferenceActivity.this); + return true; + } + }); + findPreference(PREF_GPODNET_SETLOGIN_INFORMATION).setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + AuthenticationDialog dialog = new AuthenticationDialog(PreferenceActivity.this, + R.string.pref_gpodnet_setlogin_information_title, false, false, GpodnetPreferences.getUsername(), + null) { + + @Override + protected void onConfirmed(String username, String password, boolean saveUsernamePassword) { + GpodnetPreferences.setPassword(password); + } + }; + dialog.show(); + return true; + } + }); + findPreference(PREF_GPODNET_LOGOUT).setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + GpodnetPreferences.logout(); + Toast toast = Toast.makeText(PreferenceActivity.this, R.string.pref_gpodnet_logout_toast, Toast.LENGTH_SHORT); + toast.show(); + updateGpodnetPreferenceScreen(); + return true; + } + }); + findPreference(PREF_GPODNET_HOSTNAME).setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + GpodnetSetHostnameDialog.createDialog(PreferenceActivity.this).setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + updateGpodnetPreferenceScreen(); + } + }); + return true; + } + }); + + findPreference(PREF_AUTO_FLATTR_PREFS).setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + AutoFlattrPreferenceDialog.newAutoFlattrPreferenceDialog(PreferenceActivity.this, + new AutoFlattrPreferenceDialog.AutoFlattrPreferenceDialogInterface() { + @Override + public void onCancelled() { + + } + + @Override + public void onConfirmed(boolean autoFlattrEnabled, float autoFlattrValue) { + UserPreferences.setAutoFlattrSettings(PreferenceActivity.this, autoFlattrEnabled, autoFlattrValue); + checkItemVisibility(); + } + }); + return true; + } + }); + buildUpdateIntervalPreference(); + buildAutodownloadSelectedNetworsPreference(); + setSelectedNetworksEnabled(UserPreferences + .isEnableAutodownloadWifiFilter()); + } + + private void updateGpodnetPreferenceScreen() { + final boolean loggedIn = GpodnetPreferences.loggedIn(); + findPreference(PREF_GPODNET_LOGIN).setEnabled(!loggedIn); + findPreference(PREF_GPODNET_SETLOGIN_INFORMATION).setEnabled(loggedIn); + findPreference(PREF_GPODNET_LOGOUT).setEnabled(loggedIn); + findPreference(PREF_GPODNET_HOSTNAME).setSummary(GpodnetPreferences.getHostname()); + } + + private void buildUpdateIntervalPreference() { + ListPreference pref = (ListPreference) findPreference(UserPreferences.PREF_UPDATE_INTERVAL); + String[] values = getResources().getStringArray( + R.array.update_intervall_values); + String[] entries = new String[values.length]; + for (int x = 0; x < values.length; x++) { + Integer v = Integer.parseInt(values[x]); + switch (v) { + case 0: + entries[x] = getString(R.string.pref_update_interval_hours_manual); + break; + case 1: + entries[x] = v + + " " + + getString(R.string.pref_update_interval_hours_singular); + break; + default: + entries[x] = v + " " + + getString(R.string.pref_update_interval_hours_plural); + break; + + } + } + pref.setEntries(entries); + + } + + private void setSelectedNetworksEnabled(boolean b) { + if (selectedNetworks != null) { + for (Preference p : selectedNetworks) { + p.setEnabled(b); + } + } + } + + @Override + protected void onResume() { + super.onResume(); + checkItemVisibility(); + setEpisodeCacheSizeText(UserPreferences.getEpisodeCacheSize()); + setDataFolderText(); + updateGpodnetPreferenceScreen(); + } + + @SuppressWarnings("deprecation") + private void checkItemVisibility() { + + boolean hasFlattrToken = FlattrUtils.hasToken(); + + findPreference(PREF_FLATTR_SETTINGS).setEnabled(FlattrUtils.hasAPICredentials()); + findPreference(PREF_FLATTR_AUTH).setEnabled(!hasFlattrToken); + findPreference(PREF_FLATTR_REVOKE).setEnabled(hasFlattrToken); + findPreference(PREF_AUTO_FLATTR_PREFS).setEnabled(hasFlattrToken); + + findPreference(UserPreferences.PREF_ENABLE_AUTODL_WIFI_FILTER) + .setEnabled(UserPreferences.isEnableAutodownload()); + setSelectedNetworksEnabled(UserPreferences.isEnableAutodownload() + && UserPreferences.isEnableAutodownloadWifiFilter()); + + } + + private void setEpisodeCacheSizeText(int cacheSize) { + String s; + if (cacheSize == getResources().getInteger( + R.integer.episode_cache_size_unlimited)) { + s = getString(R.string.pref_episode_cache_unlimited); + } else { + s = Integer.toString(cacheSize) + + getString(R.string.episodes_suffix); + } + findPreference(UserPreferences.PREF_EPISODE_CACHE_SIZE).setSummary(s); + } + + private void setDataFolderText() { + File f = UserPreferences.getDataFolder(this, null); + if (f != null) { + findPreference(PREF_CHOOSE_DATA_DIR) + .setSummary(f.getAbsolutePath()); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + Intent destIntent = new Intent(this, MainActivity.class); + destIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(destIntent); + finish(); + return true; + default: + return false; + } + } + + @Override + protected void onApplyThemeResource(Theme theme, int resid, boolean first) { + theme.applyStyle(UserPreferences.getTheme(), true); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode == DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED) { + String dir = data + .getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR); + if (BuildConfig.DEBUG) + Log.d(TAG, "Setting data folder"); + UserPreferences.setDataFolder(dir); + } + } + + private void buildAutodownloadSelectedNetworsPreference() { + if (selectedNetworks != null) { + clearAutodownloadSelectedNetworsPreference(); + } + // get configured networks + WifiManager wifiservice = (WifiManager) getSystemService(Context.WIFI_SERVICE); + List<WifiConfiguration> networks = wifiservice.getConfiguredNetworks(); + + if (networks != null) { + selectedNetworks = new CheckBoxPreference[networks.size()]; + List<String> prefValues = Arrays.asList(UserPreferences + .getAutodownloadSelectedNetworks()); + PreferenceScreen prefScreen = (PreferenceScreen) findPreference(AUTO_DL_PREF_SCREEN); + OnPreferenceClickListener clickListener = new OnPreferenceClickListener() { + + @Override + public boolean onPreferenceClick(Preference preference) { + if (preference instanceof CheckBoxPreference) { + String key = preference.getKey(); + ArrayList<String> prefValuesList = new ArrayList<String>( + Arrays.asList(UserPreferences + .getAutodownloadSelectedNetworks()) + ); + boolean newValue = ((CheckBoxPreference) preference) + .isChecked(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Selected network " + key + + ". New state: " + newValue); + + int index = prefValuesList.indexOf(key); + if (index >= 0 && newValue == false) { + // remove network + prefValuesList.remove(index); + } else if (index < 0 && newValue == true) { + prefValuesList.add(key); + } + + UserPreferences.setAutodownloadSelectedNetworks( + PreferenceActivity.this, prefValuesList + .toArray(new String[prefValuesList + .size()]) + ); + return true; + } else { + return false; + } + } + }; + // create preference for each known network. attach listener and set + // value + for (int i = 0; i < networks.size(); i++) { + WifiConfiguration config = networks.get(i); + + CheckBoxPreference pref = new CheckBoxPreference(this); + String key = Integer.toString(config.networkId); + pref.setTitle(config.SSID); + pref.setKey(key); + pref.setOnPreferenceClickListener(clickListener); + pref.setPersistent(false); + pref.setChecked(prefValues.contains(key)); + selectedNetworks[i] = pref; + prefScreen.addPreference(pref); + } + } else { + Log.e(TAG, "Couldn't get list of configure Wi-Fi networks"); + } + } + + private void clearAutodownloadSelectedNetworsPreference() { + if (selectedNetworks != null) { + PreferenceScreen prefScreen = (PreferenceScreen) findPreference(AUTO_DL_PREF_SCREEN); + + for (int i = 0; i < selectedNetworks.length; i++) { + if (selectedNetworks[i] != null) { + prefScreen.removePreference(selectedNetworks[i]); + } + } + } + } + + @SuppressWarnings("deprecation") + @Override + public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, + Preference preference) { + super.onPreferenceTreeClick(preferenceScreen, preference); + if (preference != null) + if (preference instanceof PreferenceScreen) + if (((PreferenceScreen) preference).getDialog() != null) + ((PreferenceScreen) preference) + .getDialog() + .getWindow() + .getDecorView() + .setBackgroundDrawable( + this.getWindow().getDecorView() + .getBackground().getConstantState() + .newDrawable() + ); + return false; + } + + @Override + public void onBackPressed() { + // The default back button behavior has to be overwritten because changing the theme clears the back stack + Intent destIntent = new Intent(this, MainActivity.class); + destIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(destIntent); + finish(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/activity/StorageErrorActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/StorageErrorActivity.java new file mode 100644 index 000000000..173bec6b2 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/activity/StorageErrorActivity.java @@ -0,0 +1,75 @@ +package de.danoeh.antennapod.activity; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.support.v7.app.ActionBarActivity; +import android.util.Log; + +import org.apache.commons.lang3.StringUtils; + +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.util.StorageUtils; + +/** Is show if there is now external storage available. */ +public class StorageErrorActivity extends ActionBarActivity { + private static final String TAG = "StorageErrorActivity"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + setTheme(UserPreferences.getTheme()); + super.onCreate(savedInstanceState); + + setContentView(R.layout.storage_error); + } + + @Override + protected void onPause() { + super.onPause(); + try { + unregisterReceiver(mediaUpdate); + } catch (IllegalArgumentException e) { + + } + } + + @Override + protected void onResume() { + super.onResume(); + if (StorageUtils.storageAvailable(this)) { + leaveErrorState(); + } else { + registerReceiver(mediaUpdate, new IntentFilter( + Intent.ACTION_MEDIA_MOUNTED)); + } + } + + private void leaveErrorState() { + finish(); + startActivity(new Intent(this, MainActivity.class)); + } + + private BroadcastReceiver mediaUpdate = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + if (StringUtils.equals(intent.getAction(), Intent.ACTION_MEDIA_MOUNTED)) { + if (intent.getBooleanExtra("read-only", true)) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Media was mounted; Finishing activity"); + leaveErrorState(); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, + "Media seemed to have been mounted read only"); + } + } + } + + }; + +} diff --git a/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java new file mode 100644 index 000000000..d3df40afb --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java @@ -0,0 +1,359 @@ +package de.danoeh.antennapod.activity; + +import android.annotation.SuppressLint; +import android.content.Intent; +import android.graphics.drawable.ColorDrawable; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; +import android.util.Pair; +import android.view.MotionEvent; +import android.view.SurfaceHolder; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.SeekBar; + +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.feed.MediaType; +import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.core.service.playback.PlayerStatus; +import de.danoeh.antennapod.core.util.playback.ExternalMedia; +import de.danoeh.antennapod.core.util.playback.Playable; +import de.danoeh.antennapod.view.AspectRatioVideoView; + +/** + * Activity for playing video files. + */ +public class VideoplayerActivity extends MediaplayerActivity { + private static final String TAG = "VideoplayerActivity"; + + /** + * True if video controls are currently visible. + */ + private boolean videoControlsShowing = true; + private boolean videoSurfaceCreated = false; + private VideoControlsHider videoControlsToggler; + + private LinearLayout videoOverlay; + private AspectRatioVideoView videoview; + private ProgressBar progressIndicator; + + @Override + protected void chooseTheme() { + setTheme(R.style.Theme_AntennaPod_Dark); + } + + @SuppressLint("AppCompatMethod") + @Override + protected void onCreate(Bundle savedInstanceState) { + if (Build.VERSION.SDK_INT >= 11) { + requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY); + } + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + super.onCreate(savedInstanceState); + getSupportActionBar().setBackgroundDrawable(new ColorDrawable(0x80000000)); + } + + @Override + protected void onPause() { + super.onPause(); + if (videoControlsToggler != null) { + videoControlsToggler.cancel(true); + } + if (controller != null && controller.getStatus() == PlayerStatus.PLAYING) { + controller.pause(); + } + } + + @Override + protected void onResume() { + super.onResume(); + if (getIntent().getAction() != null + && getIntent().getAction().equals(Intent.ACTION_VIEW)) { + Intent intent = getIntent(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Received VIEW intent: " + + intent.getData().getPath()); + ExternalMedia media = new ExternalMedia(intent.getData().getPath(), + MediaType.VIDEO); + Intent launchIntent = new Intent(this, PlaybackService.class); + launchIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media); + launchIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED, + true); + launchIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, false); + launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY, + true); + startService(launchIntent); + } + } + + @Override + protected boolean loadMediaInfo() { + if (!super.loadMediaInfo()) { + return false; + } + Playable media = controller.getMedia(); + if (media != null) { + getSupportActionBar().setSubtitle(media.getEpisodeTitle()); + getSupportActionBar().setTitle(media.getFeedTitle()); + return true; + } + + return false; + } + + @Override + protected void setupGUI() { + super.setupGUI(); + videoOverlay = (LinearLayout) findViewById(R.id.overlay); + videoview = (AspectRatioVideoView) findViewById(R.id.videoview); + progressIndicator = (ProgressBar) findViewById(R.id.progressIndicator); + videoview.getHolder().addCallback(surfaceHolderCallback); + videoview.setOnTouchListener(onVideoviewTouched); + + setupVideoControlsToggler(); + getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, + WindowManager.LayoutParams.FLAG_FULLSCREEN); + } + + @Override + protected void onAwaitingVideoSurface() { + if (videoSurfaceCreated) { + if (BuildConfig.DEBUG) + Log.d(TAG, + "Videosurface already created, setting videosurface now"); + + Pair<Integer, Integer> videoSize = controller.getVideoSize(); + if (videoSize != null && videoSize.first > 0 && videoSize.second > 0) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Width,height of video: " + videoSize.first + ", " + videoSize.second); + videoview.setVideoSize(videoSize.first, videoSize.second); + } else { + Log.e(TAG, "Could not determine video size"); + } + controller.setVideoSurface(videoview.getHolder()); + } + } + + @Override + protected void postStatusMsg(int resId) { + if (resId == R.string.player_preparing_msg) { + progressIndicator.setVisibility(View.VISIBLE); + } else { + progressIndicator.setVisibility(View.INVISIBLE); + } + + } + + @Override + protected void clearStatusMsg() { + progressIndicator.setVisibility(View.INVISIBLE); + } + + View.OnTouchListener onVideoviewTouched = new View.OnTouchListener() { + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + if (videoControlsToggler != null) { + videoControlsToggler.cancel(true); + } + toggleVideoControlsVisibility(); + if (videoControlsShowing) { + setupVideoControlsToggler(); + } + + return true; + } else { + return false; + } + } + }; + + @SuppressLint("NewApi") + void setupVideoControlsToggler() { + if (videoControlsToggler != null) { + videoControlsToggler.cancel(true); + } + videoControlsToggler = new VideoControlsHider(); + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { + videoControlsToggler + .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + videoControlsToggler.execute(); + } + } + + private void toggleVideoControlsVisibility() { + if (videoControlsShowing) { + getSupportActionBar().hide(); + hideVideoControls(); + } else { + getSupportActionBar().show(); + showVideoControls(); + } + videoControlsShowing = !videoControlsShowing; + } + + /** + * Hides the videocontrols after a certain period of time. + */ + public class VideoControlsHider extends AsyncTask<Void, Void, Void> { + @Override + protected void onCancelled() { + videoControlsToggler = null; + } + + @Override + protected void onPostExecute(Void result) { + videoControlsToggler = null; + } + + private static final int WAITING_INTERVALL = 5000; + private static final String TAG = "VideoControlsToggler"; + + @Override + protected void onProgressUpdate(Void... values) { + if (videoControlsShowing) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Hiding video controls"); + getSupportActionBar().hide(); + hideVideoControls(); + videoControlsShowing = false; + } + } + + @Override + protected Void doInBackground(Void... params) { + try { + Thread.sleep(WAITING_INTERVALL); + } catch (InterruptedException e) { + return null; + } + publishProgress(); + return null; + } + + } + + private final SurfaceHolder.Callback surfaceHolderCallback = new SurfaceHolder.Callback() { + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, + int height) { + holder.setFixedSize(width, height); + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Videoview holder created"); + videoSurfaceCreated = true; + if (controller.getStatus() == PlayerStatus.PLAYING) { + if (controller.serviceAvailable()) { + controller.setVideoSurface(holder); + } else { + Log.e(TAG, + "Could'nt attach surface to mediaplayer - reference to service was null"); + } + } + + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Videosurface was destroyed"); + videoSurfaceCreated = false; + controller.notifyVideoSurfaceAbandoned(); + } + }; + + + @Override + protected void onReloadNotification(int notificationCode) { + if (notificationCode == PlaybackService.EXTRA_CODE_AUDIO) { + if (BuildConfig.DEBUG) + Log.d(TAG, + "ReloadNotification received, switching to Audioplayer now"); + finish(); + startActivity(new Intent(this, AudioplayerActivity.class)); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + super.onStartTrackingTouch(seekBar); + if (videoControlsToggler != null) { + videoControlsToggler.cancel(true); + } + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + super.onStopTrackingTouch(seekBar); + setupVideoControlsToggler(); + } + + @Override + protected void onBufferStart() { + progressIndicator.setVisibility(View.VISIBLE); + } + + @Override + protected void onBufferEnd() { + progressIndicator.setVisibility(View.INVISIBLE); + } + + @SuppressLint("NewApi") + private void showVideoControls() { + videoOverlay.setVisibility(View.VISIBLE); + butPlay.setVisibility(View.VISIBLE); + final Animation animation = AnimationUtils.loadAnimation(this, + R.anim.fade_in); + if (animation != null) { + videoOverlay.startAnimation(animation); + butPlay.startAnimation(animation); + } + if (Build.VERSION.SDK_INT >= 14) { + videoview.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE); + } + } + + @SuppressLint("NewApi") + private void hideVideoControls() { + final Animation animation = AnimationUtils.loadAnimation(this, + R.anim.fade_out); + if (animation != null) { + videoOverlay.startAnimation(animation); + butPlay.startAnimation(animation); + } + if (Build.VERSION.SDK_INT >= 14) { + videoview.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE); + } + videoOverlay.setVisibility(View.GONE); + butPlay.setVisibility(View.GONE); + } + + @Override + protected int getContentViewResourceId() { + return R.layout.videoplayer_activity; + } + + + @Override + protected void setScreenOn(boolean enable) { + super.setScreenOn(enable); + if (enable) { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } else { + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java new file mode 100644 index 000000000..d7b069b19 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java @@ -0,0 +1,372 @@ +package de.danoeh.antennapod.activity.gpoddernet; + +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.v4.app.NavUtils; +import android.support.v7.app.ActionBarActivity; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.*; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.core.gpoddernet.GpodnetService; +import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetDevice; +import de.danoeh.antennapod.core.preferences.GpodnetPreferences; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.service.GpodnetSyncService; + +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Guides the user through the authentication process + * Step 1: Request username and password from user + * Step 2: Choose device from a list of available devices or create a new one + * Step 3: Choose from a list of actions + */ +public class GpodnetAuthenticationActivity extends ActionBarActivity { + private static final String TAG = "GpodnetAuthenticationActivity"; + + private static final String CURRENT_STEP = "current_step"; + + private ViewFlipper viewFlipper; + + private static final int STEP_DEFAULT = -1; + private static final int STEP_LOGIN = 0; + private static final int STEP_DEVICE = 1; + private static final int STEP_FINISH = 2; + + private int currentStep = -1; + + private GpodnetService service; + private volatile String username; + private volatile String password; + private volatile GpodnetDevice selectedDevice; + + View[] views; + + @Override + protected void onCreate(Bundle savedInstanceState) { + setTheme(UserPreferences.getTheme()); + super.onCreate(savedInstanceState); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + setContentView(R.layout.gpodnetauth_activity); + service = new GpodnetService(); + + viewFlipper = (ViewFlipper) findViewById(R.id.viewflipper); + LayoutInflater inflater = (LayoutInflater) + getSystemService(Context.LAYOUT_INFLATER_SERVICE); + views = new View[]{ + inflater.inflate(R.layout.gpodnetauth_credentials, viewFlipper, false), + inflater.inflate(R.layout.gpodnetauth_device, viewFlipper, false), + inflater.inflate(R.layout.gpodnetauth_finish, viewFlipper, false) + }; + for (View view : views) { + viewFlipper.addView(view); + } + advance(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (service != null) { + service.shutdown(); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + NavUtils.navigateUpFromSameTask(this); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + } + + private void setupLoginView(View view) { + final EditText username = (EditText) view.findViewById(R.id.etxtUsername); + final EditText password = (EditText) view.findViewById(R.id.etxtPassword); + final Button login = (Button) view.findViewById(R.id.butLogin); + final TextView txtvError = (TextView) view.findViewById(R.id.txtvError); + final ProgressBar progressBar = (ProgressBar) view.findViewById(R.id.progBarLogin); + + login.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + + final String usernameStr = username.getText().toString(); + final String passwordStr = password.getText().toString(); + + if (BuildConfig.DEBUG) Log.d(TAG, "Checking login credentials"); + new AsyncTask<GpodnetService, Void, Void>() { + + volatile Exception exception; + + @Override + protected void onPreExecute() { + super.onPreExecute(); + login.setEnabled(false); + progressBar.setVisibility(View.VISIBLE); + txtvError.setVisibility(View.GONE); + + } + + @Override + protected void onPostExecute(Void aVoid) { + super.onPostExecute(aVoid); + login.setEnabled(true); + progressBar.setVisibility(View.GONE); + + if (exception == null) { + advance(); + } else { + txtvError.setText(exception.getMessage()); + txtvError.setVisibility(View.VISIBLE); + } + } + + @Override + protected Void doInBackground(GpodnetService... params) { + try { + params[0].authenticate(usernameStr, passwordStr); + GpodnetAuthenticationActivity.this.username = usernameStr; + GpodnetAuthenticationActivity.this.password = passwordStr; + } catch (GpodnetServiceException e) { + e.printStackTrace(); + exception = e; + } + return null; + } + }.execute(service); + } + }); + } + + private void setupDeviceView(View view) { + final EditText deviceID = (EditText) view.findViewById(R.id.etxtDeviceID); + final EditText caption = (EditText) view.findViewById(R.id.etxtCaption); + final Button createNewDevice = (Button) view.findViewById(R.id.butCreateNewDevice); + final Button chooseDevice = (Button) view.findViewById(R.id.butChooseExistingDevice); + final TextView txtvError = (TextView) view.findViewById(R.id.txtvError); + final ProgressBar progBarCreateDevice = (ProgressBar) view.findViewById(R.id.progbarCreateDevice); + final Spinner spinnerDevices = (Spinner) view.findViewById(R.id.spinnerChooseDevice); + + + // load device list + final AtomicReference<List<GpodnetDevice>> devices = new AtomicReference<List<GpodnetDevice>>(); + new AsyncTask<GpodnetService, Void, List<GpodnetDevice>>() { + + private volatile Exception exception; + + @Override + protected void onPreExecute() { + super.onPreExecute(); + chooseDevice.setEnabled(false); + spinnerDevices.setEnabled(false); + createNewDevice.setEnabled(false); + } + + @Override + protected void onPostExecute(List<GpodnetDevice> gpodnetDevices) { + super.onPostExecute(gpodnetDevices); + if (gpodnetDevices != null) { + List<String> deviceNames = new ArrayList<String>(); + for (GpodnetDevice device : gpodnetDevices) { + deviceNames.add(device.getCaption()); + } + spinnerDevices.setAdapter(new ArrayAdapter<String>(GpodnetAuthenticationActivity.this, + android.R.layout.simple_spinner_dropdown_item, deviceNames)); + spinnerDevices.setEnabled(true); + if (!deviceNames.isEmpty()) { + chooseDevice.setEnabled(true); + } + devices.set(gpodnetDevices); + createNewDevice.setEnabled(true); + } + } + + @Override + protected List<GpodnetDevice> doInBackground(GpodnetService... params) { + try { + return params[0].getDevices(username); + } catch (GpodnetServiceException e) { + e.printStackTrace(); + exception = e; + return null; + } + } + }.execute(service); + + + createNewDevice.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (checkDeviceIDText(deviceID, txtvError, devices.get())) { + final String deviceStr = deviceID.getText().toString(); + final String captionStr = caption.getText().toString(); + + new AsyncTask<GpodnetService, Void, GpodnetDevice>() { + + private volatile Exception exception; + + @Override + protected void onPreExecute() { + super.onPreExecute(); + createNewDevice.setEnabled(false); + chooseDevice.setEnabled(false); + progBarCreateDevice.setVisibility(View.VISIBLE); + txtvError.setVisibility(View.GONE); + } + + @Override + protected void onPostExecute(GpodnetDevice result) { + super.onPostExecute(result); + createNewDevice.setEnabled(true); + chooseDevice.setEnabled(true); + progBarCreateDevice.setVisibility(View.GONE); + if (exception == null) { + selectedDevice = result; + advance(); + } else { + txtvError.setText(exception.getMessage()); + txtvError.setVisibility(View.VISIBLE); + } + } + + @Override + protected GpodnetDevice doInBackground(GpodnetService... params) { + try { + params[0].configureDevice(username, deviceStr, captionStr, GpodnetDevice.DeviceType.MOBILE); + return new GpodnetDevice(deviceStr, captionStr, GpodnetDevice.DeviceType.MOBILE.toString(), 0); + } catch (GpodnetServiceException e) { + e.printStackTrace(); + exception = e; + } + return null; + } + }.execute(service); + } + } + }); + + deviceID.setText(generateDeviceID()); + chooseDevice.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + final int position = spinnerDevices.getSelectedItemPosition(); + if (position != AdapterView.INVALID_POSITION) { + selectedDevice = devices.get().get(position); + advance(); + } + } + }); + } + + + private String generateDeviceID() { + final int DEVICE_ID_LENGTH = 10; + StringBuilder buffer = new StringBuilder(DEVICE_ID_LENGTH); + SecureRandom random = new SecureRandom(); + for (int i = 0; i < DEVICE_ID_LENGTH; i++) { + buffer.append(random.nextInt(10)); + + } + return buffer.toString(); + } + + private boolean checkDeviceIDText(EditText deviceID, TextView txtvError, List<GpodnetDevice> devices) { + String text = deviceID.getText().toString(); + if (text.length() == 0) { + txtvError.setText(R.string.gpodnetauth_device_errorEmpty); + txtvError.setVisibility(View.VISIBLE); + return false; + } else { + if (devices != null) { + for (GpodnetDevice device : devices) { + if (device.getId().equals(text)) { + txtvError.setText(R.string.gpodnetauth_device_errorAlreadyUsed); + txtvError.setVisibility(View.VISIBLE); + return false; + } + } + txtvError.setVisibility(View.GONE); + return true; + } + return true; + } + + } + + private void setupFinishView(View view) { + final Button sync = (Button) view.findViewById(R.id.butSyncNow); + final Button back = (Button) view.findViewById(R.id.butGoMainscreen); + + sync.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + GpodnetSyncService.sendSyncIntent(GpodnetAuthenticationActivity.this); + finish(); + } + }); + back.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(GpodnetAuthenticationActivity.this, MainActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + } + }); + } + + private void writeLoginCredentials() { + if (BuildConfig.DEBUG) Log.d(TAG, "Writing login credentials"); + GpodnetPreferences.setUsername(username); + GpodnetPreferences.setPassword(password); + GpodnetPreferences.setDeviceID(selectedDevice.getId()); + } + + private void advance() { + if (currentStep < STEP_FINISH) { + + View view = views[currentStep + 1]; + if (currentStep == STEP_DEFAULT) { + setupLoginView(view); + } else if (currentStep == STEP_LOGIN) { + if (username == null || password == null) { + throw new IllegalStateException("Username and password must not be null here"); + } else { + setupDeviceView(view); + } + } else if (currentStep == STEP_DEVICE) { + if (selectedDevice == null) { + throw new IllegalStateException("Device must not be null here"); + } else { + writeLoginCredentials(); + setupFinishView(view); + } + } + if (currentStep != STEP_DEFAULT) { + viewFlipper.showNext(); + } + currentStep++; + } else { + finish(); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonCallback.java b/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonCallback.java new file mode 100644 index 000000000..a75789815 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonCallback.java @@ -0,0 +1,8 @@ +package de.danoeh.antennapod.adapter; + +import de.danoeh.antennapod.core.feed.FeedItem; + +public interface ActionButtonCallback { + /** Is called when the action button of a list item has been pressed. */ + abstract void onActionButtonPressed(FeedItem item); +} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonUtils.java b/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonUtils.java new file mode 100644 index 000000000..fecddeaf4 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonUtils.java @@ -0,0 +1,78 @@ +package de.danoeh.antennapod.adapter; + +import android.content.Context; +import android.content.res.TypedArray; +import android.view.View; +import android.widget.ImageButton; + +import org.apache.commons.lang3.Validate; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.storage.DownloadRequester; + +/** + * Utility methods for the action button that is displayed on the right hand side + * of a listitem. + */ +public class ActionButtonUtils { + + private final int[] labels; + private final TypedArray drawables; + private final Context context; + + public ActionButtonUtils(Context context) { + Validate.notNull(context); + + this.context = context; + drawables = context.obtainStyledAttributes(new int[]{ + R.attr.av_play, R.attr.navigation_cancel, R.attr.av_download, R.attr.navigation_chapters, R.attr.navigation_accept}); + labels = new int[]{R.string.play_label, R.string.cancel_download_label, R.string.download_label, R.string.mark_read_label}; + } + + /** + * Sets the displayed bitmap and content description of the given + * action button so that it matches the state of the FeedItem. + */ + public void configureActionButton(ImageButton butSecondary, FeedItem item) { + Validate.isTrue(butSecondary != null && item != null, "butSecondary or item was null"); + + final FeedMedia media = item.getMedia(); + if (media != null) { + final boolean isDownloadingMedia = DownloadRequester.getInstance().isDownloadingFile(media); + if (!media.isDownloaded()) { + if (isDownloadingMedia) { + // item is being downloaded + butSecondary.setVisibility(View.VISIBLE); + butSecondary.setImageDrawable(drawables + .getDrawable(1)); + butSecondary.setContentDescription(context.getString(labels[1])); + } else { + // item is not downloaded and not being downloaded + butSecondary.setVisibility(View.VISIBLE); + butSecondary.setImageDrawable(drawables.getDrawable(2)); + butSecondary.setContentDescription(context.getString(labels[2])); + } + } else { + // item is not being downloaded + butSecondary.setVisibility(View.VISIBLE); + if (media.isPlaying()) { + butSecondary.setImageDrawable(drawables.getDrawable(3)); + } else { + butSecondary + .setImageDrawable(drawables.getDrawable(0)); + } + butSecondary.setContentDescription(context.getString(labels[0])); + } + } else { + if (item.isRead()) { + butSecondary.setVisibility(View.INVISIBLE); + } else { + butSecondary.setVisibility(View.VISIBLE); + butSecondary.setImageDrawable(drawables.getDrawable(4)); + butSecondary.setContentDescription(context.getString(labels[3])); + } + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/AdapterUtils.java b/app/src/main/java/de/danoeh/antennapod/adapter/AdapterUtils.java new file mode 100644 index 000000000..c3902639a --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/adapter/AdapterUtils.java @@ -0,0 +1,57 @@ +package de.danoeh.antennapod.adapter; + +import android.content.res.Resources; +import android.view.View; +import android.widget.ProgressBar; +import android.widget.TextView; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.util.Converter; + +/** + * Utility methods for adapters + */ +public class AdapterUtils { + + private AdapterUtils() { + + } + + /** + * Updates the contents of the TextView that shows the current playback position and the ProgressBar. + */ + public static void updateEpisodePlaybackProgress(FeedItem item, Resources res, TextView txtvPos, ProgressBar episodeProgress) { + FeedMedia media = item.getMedia(); + episodeProgress.setVisibility(View.GONE); + if (media == null) { + txtvPos.setVisibility(View.GONE); + return; + } else { + txtvPos.setVisibility(View.VISIBLE); + } + + FeedItem.State state = item.getState(); + if (state == FeedItem.State.PLAYING + || state == FeedItem.State.IN_PROGRESS) { + if (media.getDuration() > 0) { + episodeProgress.setVisibility(View.VISIBLE); + episodeProgress + .setProgress((int) (((double) media + .getPosition()) / media.getDuration() * 100)); + txtvPos.setText(Converter + .getDurationStringLong(media.getDuration() + - media.getPosition())); + } + } else if (!media.isDownloaded()) { + txtvPos.setText(res.getString( + R.string.size_prefix) + + Converter.byteToString(media.getSize())); + } else { + txtvPos.setText(res.getString( + R.string.length_prefix) + + Converter.getDurationStringLong(media + .getDuration())); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/ChapterListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/ChapterListAdapter.java new file mode 100644 index 000000000..9e59a2a1a --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/adapter/ChapterListAdapter.java @@ -0,0 +1,180 @@ +package de.danoeh.antennapod.adapter; + +import android.content.Context; +import android.text.Layout; +import android.text.Selection; +import android.text.Spannable; +import android.text.Spanned; +import android.text.style.ClickableSpan; +import android.text.util.Linkify; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnTouchListener; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.util.ChapterUtils; +import de.danoeh.antennapod.core.util.Converter; +import de.danoeh.antennapod.core.util.playback.Playable; + +import java.util.List; + +public class ChapterListAdapter extends ArrayAdapter<Chapter> { + + private static final String TAG = "ChapterListAdapter"; + + private List<Chapter> chapters; + private Playable media; + + private int defaultTextColor; + + public ChapterListAdapter(Context context, int textViewResourceId, + List<Chapter> objects, Playable media) { + super(context, textViewResourceId, objects); + this.chapters = objects; + this.media = media; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + Holder holder; + + Chapter sc = getItem(position); + + // Inflate Layout + if (convertView == null) { + holder = new Holder(); + LayoutInflater inflater = (LayoutInflater) getContext() + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + convertView = inflater.inflate(R.layout.simplechapter_item, parent, false); + holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); + defaultTextColor = holder.title.getTextColors().getDefaultColor(); + holder.start = (TextView) convertView.findViewById(R.id.txtvStart); + holder.link = (TextView) convertView.findViewById(R.id.txtvLink); + convertView.setTag(holder); + } else { + holder = (Holder) convertView.getTag(); + + } + + holder.title.setText(sc.getTitle()); + holder.start.setText(Converter.getDurationStringLong((int) sc + .getStart())); + if (sc.getLink() != null) { + holder.link.setVisibility(View.VISIBLE); + holder.link.setText(sc.getLink()); + Linkify.addLinks(holder.link, Linkify.WEB_URLS); + } else { + holder.link.setVisibility(View.GONE); + } + holder.link.setMovementMethod(null); + holder.link.setOnTouchListener(new OnTouchListener() { + + @Override + public boolean onTouch(View v, MotionEvent event) { + // from + // http://stackoverflow.com/questions/7236840/android-textview-linkify-intercepts-with-parent-view-gestures + TextView widget = (TextView) v; + Object text = widget.getText(); + if (text instanceof Spanned) { + Spannable buffer = (Spannable) text; + + int action = event.getAction(); + + if (action == MotionEvent.ACTION_UP + || action == MotionEvent.ACTION_DOWN) { + int x = (int) event.getX(); + int y = (int) event.getY(); + + x -= widget.getTotalPaddingLeft(); + y -= widget.getTotalPaddingTop(); + + x += widget.getScrollX(); + y += widget.getScrollY(); + + Layout layout = widget.getLayout(); + int line = layout.getLineForVertical(y); + int off = layout.getOffsetForHorizontal(line, x); + + ClickableSpan[] link = buffer.getSpans(off, off, + ClickableSpan.class); + + if (link.length != 0) { + if (action == MotionEvent.ACTION_UP) { + link[0].onClick(widget); + } else if (action == MotionEvent.ACTION_DOWN) { + Selection.setSelection(buffer, + buffer.getSpanStart(link[0]), + buffer.getSpanEnd(link[0])); + } + return true; + } + } + + } + + return false; + + } + }); + Chapter current = ChapterUtils.getCurrentChapter(media); + if (current != null) { + if (current == sc) { + holder.title.setTextColor(convertView.getResources().getColor( + R.color.bright_blue)); + holder.start.setTextColor(convertView.getResources().getColor( + R.color.bright_blue)); + } else { + holder.title.setTextColor(defaultTextColor); + holder.start.setTextColor(defaultTextColor); + } + } else { + Log.w(TAG, "Could not find out what the current chapter is."); + } + + return convertView; + } + + static class Holder { + TextView title; + TextView start; + TextView link; + } + + @Override + public int getCount() { + // ignore invalid chapters + int counter = 0; + for (Chapter chapter : chapters) { + if (!ignoreChapter(chapter)) { + counter++; + } + } + return counter; + } + + private boolean ignoreChapter(Chapter c) { + return media.getDuration() > 0 && media.getDuration() < c.getStart(); + } + + @Override + public Chapter getItem(int position) { + int i = 0; + for (Chapter chapter : chapters) { + if (!ignoreChapter(chapter)) { + if (i == position) { + return chapter; + } else { + i++; + } + } + } + return super.getItem(position); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java b/app/src/main/java/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java new file mode 100644 index 000000000..800462023 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java @@ -0,0 +1,57 @@ +package de.danoeh.antennapod.adapter; + +import android.content.Context; +import android.widget.Toast; + +import org.apache.commons.lang3.Validate; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; + +/** + * Default implementation of an ActionButtonCallback + */ +public class DefaultActionButtonCallback implements ActionButtonCallback { + private static final String TAG = "DefaultActionButtonCallback"; + + private final Context context; + + public DefaultActionButtonCallback(Context context) { + Validate.notNull(context); + this.context = context; + } + + @Override + public void onActionButtonPressed(final FeedItem item) { + + + if (item.hasMedia()) { + final FeedMedia media = item.getMedia(); + boolean isDownloading = DownloadRequester.getInstance().isDownloadingFile(media); + if (!isDownloading && !media.isDownloaded()) { + try { + DBTasks.downloadFeedItems(context, item); + Toast.makeText(context, R.string.status_downloading_label, Toast.LENGTH_SHORT).show(); + } catch (DownloadRequestException e) { + e.printStackTrace(); + DownloadRequestErrorDialogCreator.newRequestErrorDialog(context, e.getMessage()); + } + } else if (isDownloading) { + DownloadRequester.getInstance().cancelDownload(context, media); + Toast.makeText(context, R.string.download_cancelled_msg, Toast.LENGTH_SHORT).show(); + } else { // media is downloaded + DBTasks.playMedia(context, media, true, true, false); + } + } else { + if (!item.isRead()) { + DBWriter.markItemRead(context, item, true, true); + } + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java new file mode 100644 index 000000000..f982e86ce --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java @@ -0,0 +1,112 @@ +package de.danoeh.antennapod.adapter; + +import android.content.Context; +import android.text.format.DateUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.TextView; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedImage; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.service.download.DownloadStatus; + +/** Displays a list of DownloadStatus entries. */ +public class DownloadLogAdapter extends BaseAdapter { + + private Context context; + + private ItemAccess itemAccess; + + public DownloadLogAdapter(Context context, ItemAccess itemAccess) { + super(); + this.itemAccess = itemAccess; + this.context = context; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + Holder holder; + DownloadStatus status = getItem(position); + if (convertView == null) { + holder = new Holder(); + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + convertView = inflater.inflate(R.layout.downloadlog_item, parent, false); + holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); + holder.type = (TextView) convertView.findViewById(R.id.txtvType); + holder.date = (TextView) convertView.findViewById(R.id.txtvDate); + holder.successful = (TextView) convertView + .findViewById(R.id.txtvStatus); + holder.reason = (TextView) convertView + .findViewById(R.id.txtvReason); + convertView.setTag(holder); + } else { + holder = (Holder) convertView.getTag(); + } + if (status.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { + holder.type.setText(R.string.download_type_feed); + } else if (status.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { + holder.type.setText(R.string.download_type_media); + } else if (status.getFeedfileType() == FeedImage.FEEDFILETYPE_FEEDIMAGE) { + holder.type.setText(R.string.download_type_image); + } + if (status.getTitle() != null) { + holder.title.setText(status.getTitle()); + } else { + holder.title.setText(R.string.download_log_title_unknown); + } + holder.date.setText(DateUtils.getRelativeTimeSpanString( + status.getCompletionDate().getTime(), + System.currentTimeMillis(), 0, 0)); + if (status.isSuccessful()) { + holder.successful.setTextColor(convertView.getResources().getColor( + R.color.download_success_green)); + holder.successful.setText(R.string.download_successful); + holder.reason.setVisibility(View.GONE); + } else { + holder.successful.setTextColor(convertView.getResources().getColor( + R.color.download_failed_red)); + holder.successful.setText(R.string.download_failed); + String reasonText = status.getReason().getErrorString(context); + if (status.getReasonDetailed() != null) { + reasonText += ": " + status.getReasonDetailed(); + } + holder.reason.setText(reasonText); + holder.reason.setVisibility(View.VISIBLE); + } + + return convertView; + } + + static class Holder { + TextView title; + TextView type; + TextView date; + TextView successful; + TextView reason; + } + + @Override + public int getCount() { + return itemAccess.getCount(); + } + + @Override + public DownloadStatus getItem(int position) { + return itemAccess.getItem(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + public static interface ItemAccess { + public int getCount(); + public DownloadStatus getItem(int position); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadedEpisodesListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadedEpisodesListAdapter.java new file mode 100644 index 000000000..8785916a0 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadedEpisodesListAdapter.java @@ -0,0 +1,122 @@ +package de.danoeh.antennapod.adapter; + +import android.content.Context; +import android.text.format.DateUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.asynctask.PicassoProvider; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.util.Converter; + +/** + * Shows a list of downloaded episodes + */ +public class DownloadedEpisodesListAdapter extends BaseAdapter { + + private final Context context; + private final ItemAccess itemAccess; + + private final int imageSize; + + public DownloadedEpisodesListAdapter(Context context, ItemAccess itemAccess) { + super(); + this.context = context; + this.itemAccess = itemAccess; + this.imageSize = (int) context.getResources().getDimension(R.dimen.thumbnail_length_downloaded_item); + } + + @Override + public int getCount() { + return itemAccess.getCount(); + } + + @Override + public FeedItem getItem(int position) { + return itemAccess.getItem(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + Holder holder; + final FeedItem item = (FeedItem) getItem(position); + if (item == null) return null; + + if (convertView == null) { + holder = new Holder(); + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + convertView = inflater.inflate(R.layout.downloaded_episodeslist_item, + parent, false); + holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); + holder.pubDate = (TextView) convertView + .findViewById(R.id.txtvPublished); + holder.butSecondary = (ImageButton) convertView + .findViewById(R.id.butSecondaryAction); + holder.imageView = (ImageView) convertView.findViewById(R.id.imgvImage); + holder.txtvSize = (TextView) convertView.findViewById(R.id.txtvSize); + convertView.setTag(holder); + } else { + holder = (Holder) convertView.getTag(); + } + + holder.title.setText(item.getTitle()); + holder.pubDate.setText(DateUtils.formatDateTime(context, item.getPubDate().getTime(), DateUtils.FORMAT_SHOW_DATE)); + holder.txtvSize.setText(Converter.byteToString(item.getMedia().getSize())); + FeedItem.State state = item.getState(); + + if (state == FeedItem.State.PLAYING) { + holder.butSecondary.setEnabled(false); + } else { + holder.butSecondary.setEnabled(true); + } + + holder.butSecondary.setFocusable(false); + holder.butSecondary.setTag(item); + holder.butSecondary.setOnClickListener(secondaryActionListener); + + + PicassoProvider.getMediaMetadataPicassoInstance(context) + .load(item.getImageUri()) + .fit() + .into(holder.imageView); + + return convertView; + } + + private View.OnClickListener secondaryActionListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + FeedItem item = (FeedItem) v.getTag(); + itemAccess.onFeedItemSecondaryAction(item); + } + }; + + + static class Holder { + TextView title; + TextView pubDate; + ImageView imageView; + TextView txtvSize; + ImageButton butSecondary; + } + + public interface ItemAccess { + int getCount(); + + FeedItem getItem(int position); + + void onFeedItemSecondaryAction(FeedItem item); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadlistAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadlistAdapter.java new file mode 100644 index 000000000..4257c6eb9 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadlistAdapter.java @@ -0,0 +1,142 @@ +package de.danoeh.antennapod.adapter; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ImageButton; +import android.widget.ProgressBar; +import android.widget.TextView; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.service.download.DownloadRequest; +import de.danoeh.antennapod.core.service.download.DownloadStatus; +import de.danoeh.antennapod.core.service.download.Downloader; +import de.danoeh.antennapod.core.util.Converter; +import de.danoeh.antennapod.core.util.ThemeUtils; + +public class DownloadlistAdapter extends BaseAdapter { + + public static final int SELECTION_NONE = -1; + + private int selectedItemIndex; + private ItemAccess itemAccess; + private Context context; + + public DownloadlistAdapter(Context context, + ItemAccess itemAccess) { + super(); + this.selectedItemIndex = SELECTION_NONE; + this.context = context; + this.itemAccess = itemAccess; + } + + @Override + public int getCount() { + return itemAccess.getCount(); + } + + @Override + public Downloader getItem(int position) { + return itemAccess.getItem(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + Holder holder; + Downloader downloader = getItem(position); + DownloadRequest request = downloader.getDownloadRequest(); + // Inflate layout + if (convertView == null) { + holder = new Holder(); + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + convertView = inflater.inflate(R.layout.downloadlist_item, parent, false); + holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); + holder.message = (TextView) convertView + .findViewById(R.id.txtvMessage); + holder.downloaded = (TextView) convertView + .findViewById(R.id.txtvDownloaded); + holder.percent = (TextView) convertView + .findViewById(R.id.txtvPercent); + holder.progbar = (ProgressBar) convertView + .findViewById(R.id.progProgress); + holder.butSecondary = (ImageButton) convertView + .findViewById(R.id.butSecondaryAction); + + convertView.setTag(holder); + } else { + holder = (Holder) convertView.getTag(); + } + + if (position == selectedItemIndex) { + convertView.setBackgroundColor(convertView.getResources().getColor( + ThemeUtils.getSelectionBackgroundColor())); + } else { + convertView.setBackgroundResource(0); + } + + holder.title.setText(request.getTitle()); + if (request.getStatusMsg() != 0) { + holder.message.setText(request.getStatusMsg()); + } + String strDownloaded = Converter.byteToString(request.getSoFar()); + if (request.getSize() != DownloadStatus.SIZE_UNKNOWN) { + strDownloaded += " / " + Converter.byteToString(request.getSize()); + holder.percent.setText(request.getProgressPercent() + "%"); + holder.progbar.setProgress(request.getProgressPercent()); + holder.percent.setVisibility(View.VISIBLE); + } else { + holder.progbar.setProgress(0); + holder.percent.setVisibility(View.INVISIBLE); + } + + holder.downloaded.setText(strDownloaded); + + holder.butSecondary.setFocusable(false); + holder.butSecondary.setTag(downloader); + holder.butSecondary.setOnClickListener(butSecondaryListener); + + return convertView; + } + + private View.OnClickListener butSecondaryListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + Downloader downloader = (Downloader) v.getTag(); + itemAccess.onSecondaryActionClick(downloader); + } + }; + + static class Holder { + TextView title; + TextView message; + TextView downloaded; + TextView percent; + ProgressBar progbar; + ImageButton butSecondary; + } + + public int getSelectedItemIndex() { + return selectedItemIndex; + } + + public void setSelectedItemIndex(int selectedItemIndex) { + this.selectedItemIndex = selectedItemIndex; + notifyDataSetChanged(); + } + + public interface ItemAccess { + public int getCount(); + + public Downloader getItem(int position); + + public void onSecondaryActionClick(Downloader downloader); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java new file mode 100644 index 000000000..8b1ed9112 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java @@ -0,0 +1,306 @@ +package de.danoeh.antennapod.adapter; + +import android.content.Context; +import android.content.res.TypedArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.BaseExpandableListAdapter; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.asynctask.PicassoProvider; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.Converter; + +/** + * Displays unread items and items in the queue in one combined list. The + * structure of this list is: [header] [queueItems] [header] [unreadItems]. + */ +public class ExternalEpisodesListAdapter extends BaseExpandableListAdapter { + private static final String TAG = "ExternalEpisodesListAdapter"; + + public static final int GROUP_POS_QUEUE = 0; + public static final int GROUP_POS_UNREAD = 1; + + private Context context; + private ItemAccess itemAccess; + + private ActionButtonCallback feedItemActionCallback; + private OnGroupActionClicked groupActionCallback; + + private final int imageSize; + + public ExternalEpisodesListAdapter(Context context, + ActionButtonCallback callback, + OnGroupActionClicked groupActionCallback, + ItemAccess itemAccess) { + super(); + this.context = context; + this.itemAccess = itemAccess; + this.feedItemActionCallback = callback; + this.groupActionCallback = groupActionCallback; + this.imageSize = (int) context.getResources().getDimension(R.dimen.thumbnail_length); + } + + @Override + public boolean areAllItemsEnabled() { + return true; + } + + @Override + public FeedItem getChild(int groupPosition, int childPosition) { + if (groupPosition == GROUP_POS_QUEUE) { + return itemAccess.getQueueItemAt(childPosition); + } else if (groupPosition == GROUP_POS_UNREAD) { + return itemAccess.getUnreadItemAt(childPosition); + } + return null; + } + + @Override + public long getChildId(int groupPosition, int childPosition) { + return childPosition; + } + + @Override + public View getChildView(int groupPosition, final int childPosition, + boolean isLastChild, View convertView, ViewGroup parent) { + Holder holder; + final FeedItem item = getChild(groupPosition, childPosition); + + if (convertView == null) { + holder = new Holder(); + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + convertView = inflater.inflate(R.layout.external_itemlist_item, + parent, false); + holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); + holder.feedTitle = (TextView) convertView + .findViewById(R.id.txtvFeedname); + holder.lenSize = (TextView) convertView + .findViewById(R.id.txtvLenSize); + holder.downloadStatus = (ImageView) convertView + .findViewById(R.id.imgvDownloadStatus); + holder.feedImage = (ImageView) convertView + .findViewById(R.id.imgvFeedimage); + holder.butAction = (ImageButton) convertView + .findViewById(R.id.butAction); + holder.statusPlaying = (View) convertView + .findViewById(R.id.statusPlaying); + holder.episodeProgress = (ProgressBar) convertView + .findViewById(R.id.pbar_episode_progress); + convertView.setTag(holder); + } else { + holder = (Holder) convertView.getTag(); + } + + holder.title.setText(item.getTitle()); + holder.feedTitle.setText(item.getFeed().getTitle()); + FeedItem.State state = item.getState(); + + if (groupPosition == GROUP_POS_QUEUE) { + switch (state) { + case PLAYING: + holder.statusPlaying.setVisibility(View.VISIBLE); + holder.episodeProgress.setVisibility(View.VISIBLE); + break; + case IN_PROGRESS: + holder.statusPlaying.setVisibility(View.GONE); + holder.episodeProgress.setVisibility(View.VISIBLE); + break; + case NEW: + holder.statusPlaying.setVisibility(View.GONE); + holder.episodeProgress.setVisibility(View.GONE); + break; + default: + holder.statusPlaying.setVisibility(View.GONE); + holder.episodeProgress.setVisibility(View.GONE); + break; + } + } else { + holder.statusPlaying.setVisibility(View.GONE); + holder.episodeProgress.setVisibility(View.GONE); + } + + FeedMedia media = item.getMedia(); + if (media != null) { + + if (state == FeedItem.State.PLAYING + || state == FeedItem.State.IN_PROGRESS) { + if (media.getDuration() > 0) { + holder.episodeProgress.setProgress((int) (((double) media + .getPosition()) / media.getDuration() * 100)); + holder.lenSize.setText(Converter + .getDurationStringLong(media.getDuration() + - media.getPosition())); + } + } else if (!media.isDownloaded()) { + holder.lenSize.setText(context.getString(R.string.size_prefix) + + Converter.byteToString(media.getSize())); + } else { + holder.lenSize.setText(context + .getString(R.string.length_prefix) + + Converter.getDurationStringLong(media.getDuration())); + } + + TypedArray drawables = context.obtainStyledAttributes(new int[]{ + R.attr.av_download, R.attr.navigation_refresh}); + final int[] labels = new int[]{R.string.status_downloaded_label, R.string.downloading_label}; + holder.lenSize.setVisibility(View.VISIBLE); + if (!media.isDownloaded()) { + if (DownloadRequester.getInstance().isDownloadingFile(media)) { + holder.downloadStatus.setVisibility(View.VISIBLE); + holder.downloadStatus.setImageDrawable(drawables + .getDrawable(1)); + holder.downloadStatus.setContentDescription(context.getString(labels[1])); + } else { + holder.downloadStatus.setVisibility(View.INVISIBLE); + } + } else { + holder.downloadStatus.setVisibility(View.VISIBLE); + holder.downloadStatus + .setImageDrawable(drawables.getDrawable(0)); + holder.downloadStatus.setContentDescription(context.getString(labels[0])); + } + } else { + holder.downloadStatus.setVisibility(View.INVISIBLE); + holder.lenSize.setVisibility(View.INVISIBLE); + } + + PicassoProvider.getMediaMetadataPicassoInstance(context) + .load(item.getImageUri()) + .fit() + .into(holder.feedImage); + + holder.butAction.setFocusable(false); + holder.butAction.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + feedItemActionCallback.onActionButtonPressed(item); + } + }); + + return convertView; + + } + + static class Holder { + TextView title; + TextView feedTitle; + TextView lenSize; + ImageView downloadStatus; + ImageView feedImage; + ImageButton butAction; + View statusPlaying; + ProgressBar episodeProgress; + } + + @Override + public int getChildrenCount(int groupPosition) { + if (groupPosition == GROUP_POS_QUEUE) { + return itemAccess.getQueueSize(); + } else if (groupPosition == GROUP_POS_UNREAD) { + return itemAccess.getUnreadItemsSize(); + } + return 0; + } + + @Override + public int getGroupCount() { + // Hide 'unread items' group if empty + if (itemAccess.getUnreadItemsSize() > 0) { + return 2; + } else { + return 1; + } + } + + @Override + public long getGroupId(int groupPosition) { + return groupPosition; + } + + @Override + public View getGroupView(final int groupPosition, boolean isExpanded, + View convertView, ViewGroup parent) { + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + convertView = inflater.inflate(R.layout.feeditemlist_header, parent, false); + TextView headerTitle = (TextView) convertView + .findViewById(0); + ImageButton actionButton = (ImageButton) convertView + .findViewById(R.id.butAction); + TextView numItems = (TextView) convertView.findViewById(0); + + String headerString = null; + int childrenCount = 0; + + if (groupPosition == 0) { + headerString = context.getString(R.string.queue_label); + childrenCount = getChildrenCount(GROUP_POS_QUEUE); + } else { + headerString = context.getString(R.string.waiting_list_label); + childrenCount = getChildrenCount(GROUP_POS_UNREAD); + } + headerTitle.setText(headerString); + if (childrenCount <= 0) { + numItems.setVisibility(View.INVISIBLE); + } else { + numItems.setVisibility(View.VISIBLE); + numItems.setText(Integer.toString(childrenCount)); + } + actionButton.setFocusable(false); + actionButton.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + groupActionCallback.onClick(getGroupId(groupPosition)); + } + }); + return convertView; + } + + @Override + public boolean isEmpty() { + return itemAccess.getUnreadItemsSize() == 0 + && itemAccess.getQueueSize() == 0; + } + + @Override + public Object getGroup(int groupPosition) { + return null; + } + + @Override + public boolean hasStableIds() { + return true; + } + + @Override + public boolean isChildSelectable(int groupPosition, int childPosition) { + return true; + } + + public interface OnGroupActionClicked { + public void onClick(long groupId); + } + + public static interface ItemAccess { + public int getQueueSize(); + + public int getUnreadItemsSize(); + + public FeedItem getQueueItemAt(int position); + + public FeedItem getUnreadItemAt(int position); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java new file mode 100644 index 000000000..2f69e6580 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java @@ -0,0 +1,219 @@ +package de.danoeh.antennapod.adapter; + +import android.content.Context; +import android.content.res.TypedArray; +import android.text.format.DateUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.*; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.feed.MediaType; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.ThemeUtils; + +/** + * List adapter for items of feeds that the user has already subscribed to. + */ +public class FeedItemlistAdapter extends BaseAdapter { + + private ActionButtonCallback callback; + private final ItemAccess itemAccess; + private final Context context; + private boolean showFeedtitle; + private int selectedItemIndex; + private final ActionButtonUtils actionButtonUtils; + + public static final int SELECTION_NONE = -1; + + public FeedItemlistAdapter(Context context, + ItemAccess itemAccess, + ActionButtonCallback callback, boolean showFeedtitle) { + super(); + this.callback = callback; + this.context = context; + this.itemAccess = itemAccess; + this.showFeedtitle = showFeedtitle; + this.selectedItemIndex = SELECTION_NONE; + this.actionButtonUtils = new ActionButtonUtils(context); + } + + @Override + public int getCount() { + return itemAccess.getCount(); + + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public FeedItem getItem(int position) { + return itemAccess.getItem(position); + } + + @Override + public View getView(final int position, View convertView, ViewGroup parent) { + Holder holder; + final FeedItem item = getItem(position); + + if (convertView == null) { + holder = new Holder(); + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + convertView = inflater.inflate(R.layout.feeditemlist_item, parent, false); + holder.title = (TextView) convertView + .findViewById(R.id.txtvItemname); + holder.lenSize = (TextView) convertView + .findViewById(R.id.txtvLenSize); + holder.butAction = (ImageButton) convertView + .findViewById(R.id.butSecondaryAction); + holder.published = (TextView) convertView + .findViewById(R.id.txtvPublished); + holder.inPlaylist = (ImageView) convertView + .findViewById(R.id.imgvInPlaylist); + holder.type = (ImageView) convertView.findViewById(R.id.imgvType); + holder.statusUnread = (View) convertView + .findViewById(R.id.statusUnread); + holder.episodeProgress = (ProgressBar) convertView + .findViewById(R.id.pbar_episode_progress); + + convertView.setTag(holder); + } else { + holder = (Holder) convertView.getTag(); + } + if (!(getItemViewType(position) == Adapter.IGNORE_ITEM_VIEW_TYPE)) { + convertView.setVisibility(View.VISIBLE); + if (position == selectedItemIndex) { + convertView.setBackgroundColor(convertView.getResources() + .getColor(ThemeUtils.getSelectionBackgroundColor())); + } else { + convertView.setBackgroundResource(0); + } + + StringBuilder buffer = new StringBuilder(item.getTitle()); + if (showFeedtitle) { + buffer.append("("); + buffer.append(item.getFeed().getTitle()); + buffer.append(")"); + } + holder.title.setText(buffer.toString()); + + FeedItem.State state = item.getState(); + switch (state) { + case PLAYING: + holder.statusUnread.setVisibility(View.GONE); + holder.episodeProgress.setVisibility(View.VISIBLE); + break; + case IN_PROGRESS: + holder.statusUnread.setVisibility(View.GONE); + holder.episodeProgress.setVisibility(View.VISIBLE); + break; + case NEW: + holder.statusUnread.setVisibility(View.VISIBLE); + break; + default: + holder.statusUnread.setVisibility(View.GONE); + break; + } + + holder.published.setText(DateUtils.formatDateTime(context, item.getPubDate().getTime(), DateUtils.FORMAT_SHOW_DATE)); + + + FeedMedia media = item.getMedia(); + if (media == null) { + holder.episodeProgress.setVisibility(View.GONE); + holder.inPlaylist.setVisibility(View.INVISIBLE); + holder.type.setVisibility(View.INVISIBLE); + holder.lenSize.setVisibility(View.INVISIBLE); + } else { + + AdapterUtils.updateEpisodePlaybackProgress(item, context.getResources(), holder.lenSize, holder.episodeProgress); + + if (((ItemAccess) itemAccess).isInQueue(item)) { + holder.inPlaylist.setVisibility(View.VISIBLE); + } else { + holder.inPlaylist.setVisibility(View.INVISIBLE); + } + + if (DownloadRequester.getInstance().isDownloadingFile( + item.getMedia())) { + holder.episodeProgress.setVisibility(View.VISIBLE); + holder.episodeProgress.setProgress(((ItemAccess) itemAccess).getItemDownloadProgressPercent(item)); + } + + TypedArray typeDrawables = context.obtainStyledAttributes( + new int[]{R.attr.type_audio, R.attr.type_video}); + final int[] labels = new int[]{R.string.media_type_audio_label, R.string.media_type_video_label}; + + MediaType mediaType = item.getMedia().getMediaType(); + if (mediaType == MediaType.AUDIO) { + holder.type.setImageDrawable(typeDrawables.getDrawable(0)); + holder.type.setContentDescription(context.getString(labels[0])); + holder.type.setVisibility(View.VISIBLE); + } else if (mediaType == MediaType.VIDEO) { + holder.type.setImageDrawable(typeDrawables.getDrawable(1)); + holder.type.setContentDescription(context.getString(labels[1])); + holder.type.setVisibility(View.VISIBLE); + } else { + holder.type.setImageBitmap(null); + holder.type.setVisibility(View.GONE); + } + } + + actionButtonUtils.configureActionButton(holder.butAction, item); + holder.butAction.setFocusable(false); + holder.butAction.setTag(item); + holder.butAction.setOnClickListener(butActionListener); + + } else { + convertView.setVisibility(View.GONE); + } + return convertView; + + } + + private final OnClickListener butActionListener = new OnClickListener() { + @Override + public void onClick(View v) { + FeedItem item = (FeedItem) v.getTag(); + callback.onActionButtonPressed(item); + } + }; + + static class Holder { + TextView title; + TextView published; + TextView lenSize; + ImageView type; + ImageView inPlaylist; + ImageButton butAction; + View statusUnread; + ProgressBar episodeProgress; + } + + public int getSelectedItemIndex() { + return selectedItemIndex; + } + + public void setSelectedItemIndex(int selectedItemIndex) { + this.selectedItemIndex = selectedItemIndex; + notifyDataSetChanged(); + } + + public static interface ItemAccess { + public boolean isInQueue(FeedItem item); + + int getItemDownloadProgressPercent(FeedItem item); + + int getCount(); + + FeedItem getItem(int position); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java new file mode 100644 index 000000000..9011c8b02 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java @@ -0,0 +1,55 @@ +package de.danoeh.antennapod.adapter; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.feed.FeedItem; + +import java.util.List; + +/** + * List adapter for showing a list of FeedItems with their title and description. + */ +public class FeedItemlistDescriptionAdapter extends ArrayAdapter<FeedItem> { + + public FeedItemlistDescriptionAdapter(Context context, int resource, List<FeedItem> objects) { + super(context, resource, objects); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + Holder holder; + + FeedItem item = getItem(position); + + // Inflate layout + if (convertView == null) { + holder = new Holder(); + LayoutInflater inflater = (LayoutInflater) getContext() + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + convertView = inflater.inflate(R.layout.itemdescription_listitem, parent, false); + holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); + holder.description = (TextView) convertView.findViewById(R.id.txtvDescription); + + convertView.setTag(holder); + } else { + holder = (Holder) convertView.getTag(); + } + + holder.title.setText(item.getTitle()); + if (item.getDescription() != null) { + holder.description.setText(item.getDescription()); + } + + return convertView; + } + + static class Holder { + TextView title; + TextView description; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java new file mode 100644 index 000000000..a917633e6 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java @@ -0,0 +1,229 @@ +package de.danoeh.antennapod.adapter; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import android.widget.TextView; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.asynctask.PicassoProvider; +import de.danoeh.antennapod.core.feed.Feed; + +/** + * BaseAdapter for the navigation drawer + */ +public class NavListAdapter extends BaseAdapter { + public static final int VIEW_TYPE_COUNT = 3; + public static final int VIEW_TYPE_NAV = 0; + public static final int VIEW_TYPE_SECTION_DIVIDER = 1; + public static final int VIEW_TYPE_SUBSCRIPTION = 2; + + public static final int[] NAV_TITLES = {R.string.all_episodes_label, R.string.queue_label, R.string.downloads_label, R.string.playback_history_label, R.string.add_feed_label}; + + private final Drawable[] drawables; + + public static final int SUBSCRIPTION_OFFSET = 1 + NAV_TITLES.length; + + private ItemAccess itemAccess; + private Context context; + + public NavListAdapter(ItemAccess itemAccess, Context context) { + this.itemAccess = itemAccess; + this.context = context; + + TypedArray ta = context.obtainStyledAttributes(new int[]{R.attr.ic_new, R.attr.stat_playlist, + R.attr.av_download, R.attr.device_access_time, R.attr.content_new}); + drawables = new Drawable[]{ta.getDrawable(0), ta.getDrawable(1), ta.getDrawable(2), + ta.getDrawable(3), ta.getDrawable(4)}; + ta.recycle(); + } + + @Override + public int getCount() { + return NAV_TITLES.length + 1 + itemAccess.getCount(); + } + + @Override + public Object getItem(int position) { + int viewType = getItemViewType(position); + if (viewType == VIEW_TYPE_NAV) { + return context.getString(NAV_TITLES[position]); + } else if (viewType == VIEW_TYPE_SECTION_DIVIDER) { + return context.getString(R.string.podcasts_label); + } else { + return itemAccess.getItem(position); + } + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public int getItemViewType(int position) { + if (0 <= position && position < NAV_TITLES.length) { + return VIEW_TYPE_NAV; + } else if (position < NAV_TITLES.length + 1) { + return VIEW_TYPE_SECTION_DIVIDER; + } else { + return VIEW_TYPE_SUBSCRIPTION; + } + } + + @Override + public int getViewTypeCount() { + return VIEW_TYPE_COUNT; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + int viewType = getItemViewType(position); + View v = null; + if (viewType == VIEW_TYPE_NAV) { + v = getNavView((String) getItem(position), position, convertView, parent); + } else if (viewType == VIEW_TYPE_SECTION_DIVIDER) { + v = getSectionDividerView((String) getItem(position), position, convertView, parent); + } else { + v = getFeedView(position - SUBSCRIPTION_OFFSET, convertView, parent); + } + if (v != null) { + TextView txtvTitle = (TextView) v.findViewById(R.id.txtvTitle); + if (position == itemAccess.getSelectedItemIndex()) { + txtvTitle.setTypeface(null, Typeface.BOLD); + } else { + txtvTitle.setTypeface(null, Typeface.NORMAL); + } + } + return v; + } + + private View getNavView(String title, int position, View convertView, ViewGroup parent) { + NavHolder holder; + if (convertView == null) { + holder = new NavHolder(); + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + convertView = inflater.inflate(R.layout.nav_listitem, parent, false); + + holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); + holder.count = (TextView) convertView.findViewById(R.id.txtvCount); + holder.image = (ImageView) convertView.findViewById(R.id.imgvCover); + convertView.setTag(holder); + } else { + holder = (NavHolder) convertView.getTag(); + } + + holder.title.setText(title); + + if (NAV_TITLES[position] == R.string.queue_label) { + int queueSize = itemAccess.getQueueSize(); + if (queueSize > 0) { + holder.count.setVisibility(View.VISIBLE); + holder.count.setText(String.valueOf(queueSize)); + } else { + holder.count.setVisibility(View.GONE); + } + } else if (NAV_TITLES[position] == R.string.all_episodes_label) { + int unreadItems = itemAccess.getNumberOfUnreadItems(); + if (unreadItems > 0) { + holder.count.setVisibility(View.VISIBLE); + holder.count.setText(String.valueOf(unreadItems)); + } else { + holder.count.setVisibility(View.GONE); + } + } else { + holder.count.setVisibility(View.GONE); + } + + holder.image.setImageDrawable(drawables[position]); + + return convertView; + } + + private View getSectionDividerView(String title, int position, View convertView, ViewGroup parent) { + SectionHolder holder; + if (convertView == null) { + holder = new SectionHolder(); + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + convertView = inflater.inflate(R.layout.nav_section_item, parent, false); + + holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); + convertView.setTag(holder); + } else { + holder = (SectionHolder) convertView.getTag(); + } + + holder.title.setText(title); + + convertView.setEnabled(false); + convertView.setOnClickListener(null); + + return convertView; + } + + private View getFeedView(int feedPos, View convertView, ViewGroup parent) { + FeedHolder holder; + Feed feed = itemAccess.getItem(feedPos); + + if (convertView == null) { + holder = new FeedHolder(); + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + convertView = inflater.inflate(R.layout.nav_feedlistitem, parent, false); + + holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); + holder.image = (ImageView) convertView.findViewById(R.id.imgvCover); + convertView.setTag(holder); + } else { + holder = (FeedHolder) convertView.getTag(); + } + + holder.title.setText(feed.getTitle()); + + PicassoProvider.getDefaultPicassoInstance(context) + .load(feed.getImageUri()) + .fit() + .into(holder.image); + + return convertView; + } + + static class NavHolder { + TextView title; + TextView count; + ImageView image; + } + + static class SectionHolder { + TextView title; + } + + static class FeedHolder { + TextView title; + ImageView image; + } + + + public interface ItemAccess { + public int getCount(); + + public Feed getItem(int position); + + public int getSelectedItemIndex(); + + public int getQueueSize(); + + public int getNumberOfUnreadItems(); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/NewEpisodesListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/NewEpisodesListAdapter.java new file mode 100644 index 000000000..a0829286c --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/adapter/NewEpisodesListAdapter.java @@ -0,0 +1,170 @@ +package de.danoeh.antennapod.adapter; + +import android.content.Context; +import android.text.format.DateUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.asynctask.PicassoProvider; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.Converter; + +/** + * List adapter for the list of new episodes + */ +public class NewEpisodesListAdapter extends BaseAdapter { + + private final Context context; + private final ItemAccess itemAccess; + private final ActionButtonCallback actionButtonCallback; + private final ActionButtonUtils actionButtonUtils; + + public NewEpisodesListAdapter(Context context, ItemAccess itemAccess, ActionButtonCallback actionButtonCallback) { + super(); + this.context = context; + this.itemAccess = itemAccess; + this.actionButtonUtils = new ActionButtonUtils(context); + this.actionButtonCallback = actionButtonCallback; + } + + @Override + public int getCount() { + return itemAccess.getCount(); + } + + @Override + public Object getItem(int position) { + return itemAccess.getItem(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public int getViewTypeCount() { + return 1; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + Holder holder; + final FeedItem item = (FeedItem) getItem(position); + if (item == null) return null; + + if (convertView == null) { + holder = new Holder(); + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + convertView = inflater.inflate(R.layout.new_episodes_listitem, + parent, false); + holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); + holder.pubDate = (TextView) convertView + .findViewById(R.id.txtvPublished); + holder.statusUnread = convertView.findViewById(R.id.statusUnread); + holder.butSecondary = (ImageButton) convertView + .findViewById(R.id.butSecondaryAction); + holder.queueStatus = (ImageView) convertView + .findViewById(R.id.imgvInPlaylist); + holder.downloadProgress = (ProgressBar) convertView + .findViewById(R.id.pbar_download_progress); + holder.imageView = (ImageView) convertView.findViewById(R.id.imgvImage); + holder.txtvDuration = (TextView) convertView.findViewById(R.id.txtvDuration); + convertView.setTag(holder); + } else { + holder = (Holder) convertView.getTag(); + } + + holder.title.setText(item.getTitle()); + holder.pubDate.setText(DateUtils.formatDateTime(context, item.getPubDate().getTime(), DateUtils.FORMAT_SHOW_DATE)); + if (item.isRead()) { + holder.statusUnread.setVisibility(View.GONE); + } else { + holder.statusUnread.setVisibility(View.VISIBLE); + } + + FeedMedia media = item.getMedia(); + if (media != null) { + final boolean isDownloadingMedia = DownloadRequester.getInstance().isDownloadingFile(media); + + if (media.getDuration() > 0) { + holder.txtvDuration.setText(Converter.getDurationStringLong(media.getDuration())); + } else { + holder.txtvDuration.setText(""); + } + + if (isDownloadingMedia) { + holder.downloadProgress.setVisibility(View.VISIBLE); + holder.txtvDuration.setVisibility(View.GONE); + } else { + holder.txtvDuration.setVisibility(View.VISIBLE); + holder.downloadProgress.setVisibility(View.GONE); + } + + if (!media.isDownloaded()) { + if (isDownloadingMedia) { + // item is being downloaded + holder.downloadProgress.setProgress(itemAccess.getItemDownloadProgressPercent(item)); + } + } + } + if (itemAccess.isInQueue(item)) { + holder.queueStatus.setVisibility(View.VISIBLE); + } else { + holder.queueStatus.setVisibility(View.INVISIBLE); + } + + actionButtonUtils.configureActionButton(holder.butSecondary, item); + holder.butSecondary.setFocusable(false); + holder.butSecondary.setTag(item); + holder.butSecondary.setOnClickListener(secondaryActionListener); + + PicassoProvider.getMediaMetadataPicassoInstance(context) + .load(item.getImageUri()) + .fit() + .into(holder.imageView); + + return convertView; + } + + private View.OnClickListener secondaryActionListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + FeedItem item = (FeedItem) v.getTag(); + actionButtonCallback.onActionButtonPressed(item); + } + }; + + + static class Holder { + TextView title; + TextView pubDate; + View statusUnread; + ImageView queueStatus; + ImageView imageView; + ProgressBar downloadProgress; + TextView txtvDuration; + ImageButton butSecondary; + } + + public interface ItemAccess { + + int getCount(); + + FeedItem getItem(int position); + + int getItemDownloadProgressPercent(FeedItem item); + + boolean isInQueue(FeedItem item); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/QueueListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/QueueListAdapter.java new file mode 100644 index 000000000..bc42ad063 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/adapter/QueueListAdapter.java @@ -0,0 +1,127 @@ +package de.danoeh.antennapod.adapter; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.*; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.asynctask.PicassoProvider; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.storage.DownloadRequester; + +/** + * List adapter for the queue. + */ +public class QueueListAdapter extends BaseAdapter { + + + private final Context context; + private final ItemAccess itemAccess; + private final ActionButtonCallback actionButtonCallback; + private final ActionButtonUtils actionButtonUtils; + + + public QueueListAdapter(Context context, ItemAccess itemAccess, ActionButtonCallback actionButtonCallback) { + super(); + this.context = context; + this.itemAccess = itemAccess; + this.actionButtonUtils = new ActionButtonUtils(context); + this.actionButtonCallback = actionButtonCallback; + } + + @Override + public int getCount() { + return itemAccess.getCount(); + } + + @Override + public Object getItem(int position) { + return itemAccess.getItem(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + Holder holder; + final FeedItem item = (FeedItem) getItem(position); + if (item == null) return null; + + if (convertView == null) { + holder = new Holder(); + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + convertView = inflater.inflate(R.layout.queue_listitem, + parent, false); + holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); + holder.butSecondary = (ImageButton) convertView + .findViewById(R.id.butSecondaryAction); + holder.position = (TextView) convertView.findViewById(R.id.txtvPosition); + holder.progress = (ProgressBar) convertView + .findViewById(R.id.pbar_download_progress); + holder.imageView = (ImageView) convertView.findViewById(R.id.imgvImage); + convertView.setTag(holder); + } else { + holder = (Holder) convertView.getTag(); + } + + holder.title.setText(item.getTitle()); + + AdapterUtils.updateEpisodePlaybackProgress(item, context.getResources(), holder.position, holder.progress); + + FeedMedia media = item.getMedia(); + if (media != null) { + final boolean isDownloadingMedia = DownloadRequester.getInstance().isDownloadingFile(media); + + if (!media.isDownloaded()) { + if (isDownloadingMedia) { + // item is being downloaded + holder.progress.setVisibility(View.VISIBLE); + holder.progress.setProgress(itemAccess.getItemDownloadProgressPercent(item)); + } + } + } + + actionButtonUtils.configureActionButton(holder.butSecondary, item); + holder.butSecondary.setFocusable(false); + holder.butSecondary.setTag(item); + holder.butSecondary.setOnClickListener(secondaryActionListener); + + PicassoProvider.getMediaMetadataPicassoInstance(context) + .load(item.getImageUri()) + .fit() + .into(holder.imageView); + + return convertView; + } + + private View.OnClickListener secondaryActionListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + FeedItem item = (FeedItem) v.getTag(); + actionButtonCallback.onActionButtonPressed(item); + } + }; + + + static class Holder { + TextView title; + ImageView imageView; + TextView position; + ProgressBar progress; + ImageButton butSecondary; + } + + public interface ItemAccess { + int getCount(); + + FeedItem getItem(int position); + + int getItemDownloadProgressPercent(FeedItem item); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/SearchlistAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/SearchlistAdapter.java new file mode 100644 index 000000000..79c1f6f99 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/adapter/SearchlistAdapter.java @@ -0,0 +1,110 @@ +package de.danoeh.antennapod.adapter; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.asynctask.PicassoProvider; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedComponent; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.SearchResult; + +/** + * List adapter for search activity. + */ +public class SearchlistAdapter extends BaseAdapter { + + private final Context context; + private final ItemAccess itemAccess; + + + public SearchlistAdapter(Context context, ItemAccess itemAccess) { + this.context = context; + this.itemAccess = itemAccess; + } + + @Override + public int getCount() { + return itemAccess.getCount(); + } + + @Override + public SearchResult getItem(int position) { + return itemAccess.getItem(position); + } + + @Override + public long getItemId(int position) { + return 0; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final Holder holder; + SearchResult result = getItem(position); + FeedComponent component = result.getComponent(); + + // Inflate Layout + if (convertView == null) { + holder = new Holder(); + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + convertView = inflater.inflate(R.layout.searchlist_item, parent, false); + holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); + holder.cover = (ImageView) convertView + .findViewById(R.id.imgvFeedimage); + holder.subtitle = (TextView) convertView + .findViewById(R.id.txtvSubtitle); + + convertView.setTag(holder); + } else { + holder = (Holder) convertView.getTag(); + } + if (component.getClass() == Feed.class) { + final Feed feed = (Feed) component; + holder.title.setText(feed.getTitle()); + holder.subtitle.setVisibility(View.GONE); + + PicassoProvider.getDefaultPicassoInstance(context) + .load(feed.getImageUri()) + .fit() + .into(holder.cover); + + } else if (component.getClass() == FeedItem.class) { + final FeedItem item = (FeedItem) component; + holder.title.setText(item.getTitle()); + if (result.getSubtitle() != null) { + holder.subtitle.setVisibility(View.VISIBLE); + holder.subtitle.setText(result.getSubtitle()); + } + + PicassoProvider.getDefaultPicassoInstance(context) + .load(item.getFeed().getImageUri()) + .fit() + .into(holder.cover); + + } + + return convertView; + } + + static class Holder { + ImageView cover; + TextView title; + TextView subtitle; + } + + public static interface ItemAccess { + int getCount(); + + SearchResult getItem(int position); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java new file mode 100644 index 000000000..7bee8e861 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java @@ -0,0 +1,68 @@ +package de.danoeh.antennapod.adapter.gpodnet; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import org.apache.commons.lang3.StringUtils; + +import java.util.List; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.asynctask.PicassoProvider; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast; + +/** + * Adapter for displaying a list of GPodnetPodcast-Objects. + */ +public class PodcastListAdapter extends ArrayAdapter<GpodnetPodcast> { + + public PodcastListAdapter(Context context, int resource, List<GpodnetPodcast> objects) { + super(context, resource, objects); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + Holder holder; + + GpodnetPodcast podcast = getItem(position); + + // Inflate Layout + if (convertView == null) { + holder = new Holder(); + LayoutInflater inflater = (LayoutInflater) getContext() + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + convertView = inflater.inflate(R.layout.gpodnet_podcast_listitem, parent, false); + holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); + holder.description = (TextView) convertView.findViewById(R.id.txtvDescription); + holder.image = (ImageView) convertView.findViewById(R.id.imgvCover); + + convertView.setTag(holder); + } else { + holder = (Holder) convertView.getTag(); + } + + holder.title.setText(podcast.getTitle()); + holder.description.setText(podcast.getDescription()); + + if (StringUtils.isNoneBlank(podcast.getLogoUrl())) { + PicassoProvider.getDefaultPicassoInstance(convertView.getContext()) + .load(podcast.getLogoUrl()) + .fit() + .into(holder.image); + } + + return convertView; + } + + static class Holder { + TextView title; + TextView description; + ImageView image; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlExportWorker.java b/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlExportWorker.java new file mode 100644 index 000000000..6bba956a6 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlExportWorker.java @@ -0,0 +1,118 @@ +package de.danoeh.antennapod.asynctask; + +import android.annotation.SuppressLint; +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.AsyncTask; +import android.util.Log; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; + +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.opml.OpmlWriter; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.util.LangUtils; + +/** + * Writes an OPML file into the export directory in the background. + */ +public class OpmlExportWorker extends AsyncTask<Void, Void, Void> { + private static final String TAG = "OpmlExportWorker"; + private static final String DEFAULT_OUTPUT_NAME = "antennapod-feeds.opml"; + public static final String EXPORT_DIR = "export/"; + + private Context context; + private File output; + + private ProgressDialog progDialog; + private Exception exception; + + public OpmlExportWorker(Context context, File output) { + this.context = context; + this.output = output; + } + + public OpmlExportWorker(Context context) { + this.context = context; + } + + @Override + protected Void doInBackground(Void... params) { + OpmlWriter opmlWriter = new OpmlWriter(); + if (output == null) { + output = new File( + UserPreferences.getDataFolder(context, EXPORT_DIR), + DEFAULT_OUTPUT_NAME); + if (output.exists()) { + Log.w(TAG, "Overwriting previously exported file."); + output.delete(); + } + } + OutputStreamWriter writer = null; + try { + writer = new OutputStreamWriter(new FileOutputStream(output), LangUtils.UTF_8); + opmlWriter.writeDocument(DBReader.getFeedList(context), writer); + } catch (IOException e) { + e.printStackTrace(); + exception = e; + } finally { + if (writer != null) { + try { + writer.close(); + } catch (IOException ioe) { + exception = ioe; + } + } + } + return null; + } + + @Override + protected void onPostExecute(Void result) { + progDialog.dismiss(); + AlertDialog.Builder alert = new AlertDialog.Builder(context) + .setNeutralButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + dialog.dismiss(); + } + }); + if (exception != null) { + alert.setTitle(R.string.export_error_label); + alert.setMessage(exception.getMessage()); + } else { + alert.setTitle(R.string.opml_export_success_title); + alert.setMessage(context + .getString(R.string.opml_export_success_sum) + + output.toString()); + } + alert.create().show(); + } + + @Override + protected void onPreExecute() { + progDialog = new ProgressDialog(context); + progDialog.setMessage(context.getString(R.string.exporting_label)); + progDialog.setIndeterminate(true); + progDialog.show(); + } + + @SuppressLint("NewApi") + public void executeAsync() { + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { + executeOnExecutor(THREAD_POOL_EXECUTOR); + } else { + execute(); + } + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlFeedQueuer.java b/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlFeedQueuer.java new file mode 100644 index 000000000..cb9197b8e --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlFeedQueuer.java @@ -0,0 +1,69 @@ +package de.danoeh.antennapod.asynctask; + +import android.annotation.SuppressLint; +import android.app.ProgressDialog; +import android.content.Context; +import android.os.AsyncTask; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.activity.OpmlImportHolder; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.opml.OpmlElement; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; + +import java.util.Arrays; +import java.util.Date; + +/** Queues items for download in the background. */ +public class OpmlFeedQueuer extends AsyncTask<Void, Void, Void> { + private Context context; + private ProgressDialog progDialog; + private int[] selection; + + public OpmlFeedQueuer(Context context, int[] selection) { + super(); + this.context = context; + this.selection = Arrays.copyOf(selection, selection.length); + } + + @Override + protected void onPostExecute(Void result) { + progDialog.dismiss(); + } + + @Override + protected void onPreExecute() { + progDialog = new ProgressDialog(context); + progDialog.setMessage(context.getString(R.string.processing_label)); + progDialog.setCancelable(false); + progDialog.setIndeterminate(true); + progDialog.show(); + } + + @Override + protected Void doInBackground(Void... params) { + DownloadRequester requester = DownloadRequester.getInstance(); + for (int idx = 0; idx < selection.length; idx++) { + OpmlElement element = OpmlImportHolder.getReadElements().get( + selection[idx]); + Feed feed = new Feed(element.getXmlUrl(), new Date(), + element.getText()); + try { + requester.downloadFeed(context.getApplicationContext(), feed); + } catch (DownloadRequestException e) { + e.printStackTrace(); + } + } + return null; + } + + @SuppressLint("NewApi") + public void executeAsync() { + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { + executeOnExecutor(THREAD_POOL_EXECUTOR); + } else { + execute(); + } + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlImportWorker.java b/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlImportWorker.java new file mode 100644 index 000000000..cfe0703ca --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlImportWorker.java @@ -0,0 +1,116 @@ +package de.danoeh.antennapod.asynctask; + +import android.annotation.SuppressLint; +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.os.AsyncTask; +import android.util.Log; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.opml.OpmlElement; +import de.danoeh.antennapod.core.opml.OpmlReader; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; + +public class OpmlImportWorker extends + AsyncTask<Void, Void, ArrayList<OpmlElement>> { + private static final String TAG = "OpmlImportWorker"; + + private Context context; + private Exception exception; + + private ProgressDialog progDialog; + + private Reader mReader; + + public OpmlImportWorker(Context context, Reader reader) { + super(); + this.context = context; + this.mReader=reader; + } + + @Override + protected ArrayList<OpmlElement> doInBackground(Void... params) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Starting background work"); + + if (mReader==null) { + return null; + } + + OpmlReader opmlReader = new OpmlReader(); + try { + ArrayList<OpmlElement> result = opmlReader.readDocument(mReader); + mReader.close(); + return result; + } catch (XmlPullParserException e) { + e.printStackTrace(); + exception = e; + return null; + } catch (IOException e) { + e.printStackTrace(); + exception = e; + return null; + } + + } + + @Override + protected void onPostExecute(ArrayList<OpmlElement> result) { + if (mReader != null) { + try { + mReader.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + progDialog.dismiss(); + if (exception != null) { + if (BuildConfig.DEBUG) + Log.d(TAG, + "An error occured while trying to parse the opml document"); + AlertDialog.Builder alert = new AlertDialog.Builder(context); + alert.setTitle(R.string.error_label); + alert.setMessage(context.getString(R.string.opml_reader_error) + + exception.getMessage()); + alert.setNeutralButton(android.R.string.ok, new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + + }); + alert.create().show(); + } + } + + @Override + protected void onPreExecute() { + progDialog = new ProgressDialog(context); + progDialog.setMessage(context.getString(R.string.reading_opml_label)); + progDialog.setIndeterminate(true); + progDialog.setCancelable(false); + progDialog.show(); + } + + public boolean wasSuccessful() { + return exception != null; + } + + @SuppressLint("NewApi") + public void executeAsync() { + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { + executeOnExecutor(THREAD_POOL_EXECUTOR); + } else { + execute(); + } + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/config/ApplicationCallbacksImpl.java b/app/src/main/java/de/danoeh/antennapod/config/ApplicationCallbacksImpl.java new file mode 100644 index 000000000..4d9be5d78 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/config/ApplicationCallbacksImpl.java @@ -0,0 +1,29 @@ +package de.danoeh.antennapod.config; + + +import android.app.Application; +import android.content.Context; +import android.content.Intent; + +import de.danoeh.antennapod.PodcastApp; +import de.danoeh.antennapod.activity.StorageErrorActivity; +import de.danoeh.antennapod.core.ApplicationCallbacks; +import de.danoeh.antennapod.core.preferences.UserPreferences; + +public class ApplicationCallbacksImpl implements ApplicationCallbacks { + + @Override + public Application getApplicationInstance() { + return PodcastApp.getInstance(); + } + + @Override + public Intent getStorageErrorActivity(Context context) { + return new Intent(context, StorageErrorActivity.class); + } + + @Override + public void setUpdateInterval(long updateInterval) { + UserPreferences.restartUpdateAlarm(updateInterval, updateInterval); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java b/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java new file mode 100644 index 000000000..169ae3ac8 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java @@ -0,0 +1,19 @@ +package de.danoeh.antennapod.config; + +import de.danoeh.antennapod.core.ClientConfig; + +/** + * Configures the ClientConfig class of the core package. + */ +public class ClientConfigurator { + + static { + ClientConfig.USER_AGENT = "AntennaPod/0.9.9.4"; + ClientConfig.applicationCallbacks = new ApplicationCallbacksImpl(); + ClientConfig.downloadServiceCallbacks = new DownloadServiceCallbacksImpl(); + ClientConfig.gpodnetCallbacks = new GpodnetCallbacksImpl(); + ClientConfig.playbackServiceCallbacks = new PlaybackServiceCallbacksImpl(); + ClientConfig.storageCallbacks = new StorageCallbacksImpl(); + ClientConfig.flattrCallbacks = new FlattrCallbacksImpl(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java b/app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java new file mode 100644 index 000000000..3a9e62435 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java @@ -0,0 +1,60 @@ +package de.danoeh.antennapod.config; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import de.danoeh.antennapod.activity.DownloadAuthenticationActivity; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.adapter.NavListAdapter; +import de.danoeh.antennapod.core.DownloadServiceCallbacks; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.service.download.DownloadRequest; +import de.danoeh.antennapod.fragment.DownloadsFragment; + + +public class DownloadServiceCallbacksImpl implements DownloadServiceCallbacks { + + @Override + public PendingIntent getNotificationContentIntent(Context context) { + Intent intent = new Intent(context, MainActivity.class); + intent.putExtra(MainActivity.EXTRA_NAV_TYPE, NavListAdapter.VIEW_TYPE_NAV); + intent.putExtra(MainActivity.EXTRA_NAV_INDEX, MainActivity.POS_DOWNLOADS); + Bundle args = new Bundle(); + args.putInt(DownloadsFragment.ARG_SELECTED_TAB, DownloadsFragment.POS_RUNNING); + intent.putExtra(MainActivity.EXTRA_FRAGMENT_ARGS, args); + + return PendingIntent.getActivity(context, 0, intent, + PendingIntent.FLAG_UPDATE_CURRENT); + } + + @Override + public PendingIntent getAuthentificationNotificationContentIntent(Context context, DownloadRequest request) { + final Intent activityIntent = new Intent(context.getApplicationContext(), DownloadAuthenticationActivity.class); + activityIntent.putExtra(DownloadAuthenticationActivity.ARG_DOWNLOAD_REQUEST, request); + activityIntent.putExtra(DownloadAuthenticationActivity.ARG_SEND_TO_DOWNLOAD_REQUESTER_BOOL, true); + return PendingIntent.getActivity(context.getApplicationContext(), 0, activityIntent, PendingIntent.FLAG_ONE_SHOT); + } + + @Override + public PendingIntent getReportNotificationContentIntent(Context context) { + Intent intent = new Intent(context, MainActivity.class); + intent.putExtra(MainActivity.EXTRA_NAV_TYPE, NavListAdapter.VIEW_TYPE_NAV); + intent.putExtra(MainActivity.EXTRA_NAV_INDEX, MainActivity.POS_DOWNLOADS); + Bundle args = new Bundle(); + args.putInt(DownloadsFragment.ARG_SELECTED_TAB, DownloadsFragment.POS_LOG); + intent.putExtra(MainActivity.EXTRA_FRAGMENT_ARGS, args); + return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + @Override + public void onFeedParsed(Context context, Feed feed) { + // do nothing + } + + @Override + public boolean shouldCreateReport() { + return true; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/config/FlattrCallbacksImpl.java b/app/src/main/java/de/danoeh/antennapod/config/FlattrCallbacksImpl.java new file mode 100644 index 000000000..3817db6de --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/config/FlattrCallbacksImpl.java @@ -0,0 +1,53 @@ +package de.danoeh.antennapod.config; + + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import org.shredzone.flattr4j.oauth.AccessToken; + +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.activity.FlattrAuthActivity; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.core.FlattrCallbacks; + +public class FlattrCallbacksImpl implements FlattrCallbacks { + private static final String TAG = "FlattrCallbacksImpl"; + + @Override + public boolean flattrEnabled() { + return true; + } + + @Override + public Intent getFlattrAuthenticationActivityIntent(Context context) { + return new Intent(context, FlattrAuthActivity.class); + } + + @Override + public PendingIntent getFlattrFailedNotificationContentIntent(Context context) { + return PendingIntent.getActivity(context, 0, new Intent(context, MainActivity.class), 0); + } + + @Override + public String getFlattrAppKey() { + return BuildConfig.FLATTR_APP_KEY; + } + + @Override + public String getFlattrAppSecret() { + return BuildConfig.FLATTR_APP_SECRET; + } + + @Override + public void handleFlattrAuthenticationSuccess(AccessToken token) { + FlattrAuthActivity instance = FlattrAuthActivity.getInstance(); + if (instance != null) { + instance.handleAuthenticationSuccess(); + } else { + Log.e(TAG, "FlattrAuthActivity instance was null"); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/config/GpodnetCallbacksImpl.java b/app/src/main/java/de/danoeh/antennapod/config/GpodnetCallbacksImpl.java new file mode 100644 index 000000000..5f8da6894 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/config/GpodnetCallbacksImpl.java @@ -0,0 +1,22 @@ +package de.danoeh.antennapod.config; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; + +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.core.GpodnetCallbacks; + + +public class GpodnetCallbacksImpl implements GpodnetCallbacks { + @Override + public boolean gpodnetEnabled() { + return true; + } + + @Override + public PendingIntent getGpodnetSyncServiceErrorNotificationPendingIntent(Context context) { + return PendingIntent.getActivity(context, 0, new Intent(context, MainActivity.class), + PendingIntent.FLAG_UPDATE_CURRENT); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/config/PlaybackServiceCallbacksImpl.java b/app/src/main/java/de/danoeh/antennapod/config/PlaybackServiceCallbacksImpl.java new file mode 100644 index 000000000..997befe99 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/config/PlaybackServiceCallbacksImpl.java @@ -0,0 +1,32 @@ +package de.danoeh.antennapod.config; + +import android.content.Context; +import android.content.Intent; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.AudioplayerActivity; +import de.danoeh.antennapod.activity.VideoplayerActivity; +import de.danoeh.antennapod.core.PlaybackServiceCallbacks; +import de.danoeh.antennapod.core.feed.MediaType; + + +public class PlaybackServiceCallbacksImpl implements PlaybackServiceCallbacks { + @Override + public Intent getPlayerActivityIntent(Context context, MediaType mediaType) { + if (mediaType == MediaType.VIDEO) { + return new Intent(context, VideoplayerActivity.class); + } else { + return new Intent(context, AudioplayerActivity.class); + } + } + + @Override + public boolean useQueue() { + return true; + } + + @Override + public int getNotificationIconResource(Context context) { + return R.drawable.ic_stat_antenna_default; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/config/StorageCallbacksImpl.java b/app/src/main/java/de/danoeh/antennapod/config/StorageCallbacksImpl.java new file mode 100644 index 000000000..ec133aed1 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/config/StorageCallbacksImpl.java @@ -0,0 +1,107 @@ +package de.danoeh.antennapod.config; + + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.util.Log; + +import de.danoeh.antennapod.core.StorageCallbacks; +import de.danoeh.antennapod.core.storage.PodDBAdapter; + +public class StorageCallbacksImpl implements StorageCallbacks { + + @Override + public int getDatabaseVersion() { + return 12; + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + Log.w("DBAdapter", "Upgrading from version " + oldVersion + " to " + + newVersion + "."); + if (oldVersion <= 1) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + " ADD COLUMN " + + PodDBAdapter.KEY_TYPE + " TEXT"); + } + if (oldVersion <= 2) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_SIMPLECHAPTERS + + " ADD COLUMN " + PodDBAdapter.KEY_LINK + " TEXT"); + } + if (oldVersion <= 3) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + + " ADD COLUMN " + PodDBAdapter.KEY_ITEM_IDENTIFIER + " TEXT"); + } + if (oldVersion <= 4) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + " ADD COLUMN " + + PodDBAdapter.KEY_FEED_IDENTIFIER + " TEXT"); + } + if (oldVersion <= 5) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_DOWNLOAD_LOG + + " ADD COLUMN " + PodDBAdapter.KEY_REASON_DETAILED + " TEXT"); + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_DOWNLOAD_LOG + + " ADD COLUMN " + PodDBAdapter.KEY_DOWNLOADSTATUS_TITLE + " TEXT"); + } + if (oldVersion <= 6) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_SIMPLECHAPTERS + + " ADD COLUMN " + PodDBAdapter.KEY_CHAPTER_TYPE + " INTEGER"); + } + if (oldVersion <= 7) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA + + " ADD COLUMN " + PodDBAdapter.KEY_PLAYBACK_COMPLETION_DATE + + " INTEGER"); + } + if (oldVersion <= 8) { + final int KEY_ID_POSITION = 0; + final int KEY_MEDIA_POSITION = 1; + + // Add feeditem column to feedmedia table + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA + + " ADD COLUMN " + PodDBAdapter.KEY_FEEDITEM + + " INTEGER"); + Cursor feeditemCursor = db.query(PodDBAdapter.TABLE_NAME_FEED_ITEMS, + new String[]{PodDBAdapter.KEY_ID, PodDBAdapter.KEY_MEDIA}, "? > 0", + new String[]{PodDBAdapter.KEY_MEDIA}, null, null, null); + if (feeditemCursor.moveToFirst()) { + db.beginTransaction(); + ContentValues contentValues = new ContentValues(); + do { + long mediaId = feeditemCursor.getLong(KEY_MEDIA_POSITION); + contentValues.put(PodDBAdapter.KEY_FEEDITEM, feeditemCursor.getLong(KEY_ID_POSITION)); + db.update(PodDBAdapter.TABLE_NAME_FEED_MEDIA, contentValues, PodDBAdapter.KEY_ID + "=?", new String[]{String.valueOf(mediaId)}); + contentValues.clear(); + } while (feeditemCursor.moveToNext()); + db.setTransactionSuccessful(); + db.endTransaction(); + } + feeditemCursor.close(); + } + if (oldVersion <= 9) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + PodDBAdapter.KEY_AUTO_DOWNLOAD + + " INTEGER DEFAULT 1"); + } + if (oldVersion <= 10) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + PodDBAdapter.KEY_FLATTR_STATUS + + " INTEGER"); + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + + " ADD COLUMN " + PodDBAdapter.KEY_FLATTR_STATUS + + " INTEGER"); + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA + + " ADD COLUMN " + PodDBAdapter.KEY_PLAYED_DURATION + + " INTEGER"); + } + if (oldVersion <= 11) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + PodDBAdapter.KEY_USERNAME + + " TEXT"); + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + PodDBAdapter.KEY_PASSWORD + + " TEXT"); + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + + " ADD COLUMN " + PodDBAdapter.KEY_IMAGE + + " INTEGER"); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/AuthenticationDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/AuthenticationDialog.java new file mode 100644 index 000000000..bdb2d68ba --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/dialog/AuthenticationDialog.java @@ -0,0 +1,89 @@ +package de.danoeh.antennapod.dialog; + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.view.View; +import android.view.Window; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.EditText; +import de.danoeh.antennapod.R; + +/** + * Displays a dialog with a username and password text field and an optional checkbox to save username and preferences. + */ +public abstract class AuthenticationDialog extends Dialog { + + private final int titleRes; + private final boolean enableUsernameField; + private final boolean showSaveCredentialsCheckbox; + private final String usernameInitialValue; + private final String passwordInitialValue; + + public AuthenticationDialog(Context context, int titleRes, boolean enableUsernameField, boolean showSaveCredentialsCheckbox, String usernameInitialValue, String passwordInitialValue) { + super(context); + this.titleRes = titleRes; + this.enableUsernameField = enableUsernameField; + this.showSaveCredentialsCheckbox = showSaveCredentialsCheckbox; + this.usernameInitialValue = usernameInitialValue; + this.passwordInitialValue = passwordInitialValue; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.authentication_dialog); + final EditText etxtUsername = (EditText) findViewById(R.id.etxtUsername); + final EditText etxtPassword = (EditText) findViewById(R.id.etxtPassword); + final CheckBox saveUsernamePassword = (CheckBox) findViewById(R.id.chkSaveUsernamePassword); + final Button butConfirm = (Button) findViewById(R.id.butConfirm); + final Button butCancel = (Button) findViewById(R.id.butCancel); + + if (titleRes != 0) { + setTitle(titleRes); + } else { + requestWindowFeature(Window.FEATURE_NO_TITLE); + } + etxtUsername.setEnabled(enableUsernameField); + if (showSaveCredentialsCheckbox) { + saveUsernamePassword.setVisibility(View.VISIBLE); + } else { + saveUsernamePassword.setVisibility(View.GONE); + } + if (usernameInitialValue != null) { + etxtUsername.setText(usernameInitialValue); + } + if (passwordInitialValue != null) { + etxtPassword.setText(passwordInitialValue); + } + setOnCancelListener(new OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + onCancelled(); + } + }); + butCancel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + cancel(); + } + }); + butConfirm.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onConfirmed(etxtUsername.getText().toString(), + etxtPassword.getText().toString(), + showSaveCredentialsCheckbox && saveUsernamePassword.isChecked()); + dismiss(); + } + }); + } + + protected void onCancelled() { + + } + + protected abstract void onConfirmed(String username, String password, boolean saveUsernamePassword); +} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/AutoFlattrPreferenceDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/AutoFlattrPreferenceDialog.java new file mode 100644 index 000000000..1585f9b86 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/dialog/AutoFlattrPreferenceDialog.java @@ -0,0 +1,107 @@ +package de.danoeh.antennapod.dialog; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.view.View; +import android.widget.CheckBox; +import android.widget.SeekBar; +import android.widget.TextView; + +import org.apache.commons.lang3.Validate; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.preferences.UserPreferences; + +/** + * Creates a new AlertDialog that displays preferences for auto-flattring to the user. + */ +public class AutoFlattrPreferenceDialog { + + private AutoFlattrPreferenceDialog() { + } + + public static void newAutoFlattrPreferenceDialog(final Activity activity, final AutoFlattrPreferenceDialogInterface callback) { + Validate.notNull(activity); + Validate.notNull(callback); + + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + + @SuppressLint("InflateParams") View view = activity.getLayoutInflater().inflate(R.layout.autoflattr_preference_dialog, null); + final CheckBox chkAutoFlattr = (CheckBox) view.findViewById(R.id.chkAutoFlattr); + final SeekBar skbPercent = (SeekBar) view.findViewById(R.id.skbPercent); + final TextView txtvStatus = (TextView) view.findViewById(R.id.txtvStatus); + + chkAutoFlattr.setChecked(UserPreferences.isAutoFlattr()); + skbPercent.setEnabled(chkAutoFlattr.isChecked()); + txtvStatus.setEnabled(chkAutoFlattr.isChecked()); + + final int initialValue = (int) (UserPreferences.getAutoFlattrPlayedDurationThreshold() * 100.0f); + setStatusMsgText(activity, txtvStatus, initialValue); + skbPercent.setProgress(initialValue); + + chkAutoFlattr.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + skbPercent.setEnabled(chkAutoFlattr.isChecked()); + txtvStatus.setEnabled(chkAutoFlattr.isChecked()); + } + }); + + skbPercent.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + setStatusMsgText(activity, txtvStatus, progress); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + + } + }); + + builder.setTitle(R.string.pref_auto_flattr_title) + .setView(view) + .setPositiveButton(R.string.confirm_label, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + float progDouble = ((float) skbPercent.getProgress()) / 100.0f; + callback.onConfirmed(chkAutoFlattr.isChecked(), progDouble); + dialog.dismiss(); + } + }) + .setNegativeButton(R.string.cancel_label, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + callback.onCancelled(); + dialog.dismiss(); + } + }) + .setCancelable(false).show(); + } + + private static void setStatusMsgText(Context context, TextView txtvStatus, int progress) { + if (progress == 0) { + txtvStatus.setText(R.string.auto_flattr_ater_beginning); + } else if (progress == 100) { + txtvStatus.setText(R.string.auto_flattr_ater_end); + } else { + txtvStatus.setText(context.getString(R.string.auto_flattr_after_percent, progress)); + } + } + + public static interface AutoFlattrPreferenceDialogInterface { + public void onCancelled(); + + public void onConfirmed(boolean autoFlattrEnabled, float autoFlattrValue); + } + + +} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/FeedItemDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/FeedItemDialog.java new file mode 100644 index 000000000..613cd1fec --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/dialog/FeedItemDialog.java @@ -0,0 +1,434 @@ +package de.danoeh.antennapod.dialog; + +import android.annotation.TargetApi; +import android.app.Dialog; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.res.TypedArray; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.support.v7.widget.PopupMenu; +import android.text.TextUtils; +import android.util.Log; +import android.util.TypedValue; +import android.view.MenuItem; +import android.view.View; +import android.view.Window; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.ImageButton; +import android.widget.TextView; +import android.widget.Toast; + +import org.apache.commons.lang3.StringEscapeUtils; +import org.apache.commons.lang3.Validate; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Callable; + +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.adapter.DefaultActionButtonCallback; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.QueueAccess; +import de.danoeh.antennapod.core.util.ShownotesProvider; +import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; + +/** + * Shows information about a specific FeedItem and provides actions like playing, downloading, etc. + */ +public class FeedItemDialog extends Dialog { + private static final String TAG = "FeedItemDialog"; + + private FeedItem item; + private QueueAccess queue; + + private View header; + private TextView txtvTitle; + private WebView webvDescription; + private ImageButton butAction1; + private ImageButton butAction2; + private ImageButton butMore; + private PopupMenu popupMenu; + + public static FeedItemDialog newInstance(Context context, FeedItemDialogSavedInstance savedInstance) { + Validate.notNull(savedInstance); + FeedItemDialog dialog = newInstance(context, savedInstance.item, savedInstance.queueAccess); + if (savedInstance.isShowing) { + dialog.show(); + } + return dialog; + } + + public static FeedItemDialog newInstance(Context context, FeedItem item, QueueAccess queue) { + if (useDarkThemeWorkAround()) { + return new FeedItemDialog(context, R.style.Theme_AntennaPod_Dark, item, queue); + } else { + return new FeedItemDialog(context, item, queue); + } + } + + public FeedItemDialog(Context context, int theme, FeedItem item, QueueAccess queue) { + super(context, theme); + Validate.notNull(item); + Validate.notNull(queue); + this.item = item; + this.queue = queue; + } + + private FeedItemDialog(Context context, FeedItem item, QueueAccess queue) { + this(context, 0, item, queue); + } + + /** + * Returns true if the dialog should use a dark theme. This has to be done on Gingerbread devices + * because dialogs are only available in a dark theme. + */ + private static boolean useDarkThemeWorkAround() { + return Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1 + && UserPreferences.getTheme() != R.style.Theme_AntennaPod_Dark; + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_NO_TITLE); + setContentView(R.layout.feeditem_dialog); + + txtvTitle = (TextView) findViewById(R.id.txtvTitle); + header = findViewById(R.id.header); + webvDescription = (WebView) findViewById(R.id.webview); + butAction1 = (ImageButton) findViewById(R.id.butAction1); + butAction2 = (ImageButton) findViewById(R.id.butAction2); + butMore = (ImageButton) findViewById(R.id.butMoreActions); + popupMenu = new PopupMenu(getContext(), butMore); + + webvDescription.setWebViewClient(new WebViewClient()); + + if (Build.VERSION.SDK_INT >= 14) { // ellipsize is causing problems on old versions, see #448 + txtvTitle.setEllipsize(TextUtils.TruncateAt.END); + } + + txtvTitle.setText(item.getTitle()); + + if (UserPreferences.getTheme() == R.style.Theme_AntennaPod_Dark) { + if (Build.VERSION.SDK_INT >= 11 + && Build.VERSION.SDK_INT <= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { + webvDescription.setLayerType(View.LAYER_TYPE_SOFTWARE, null); + } + webvDescription.setBackgroundColor(getContext().getResources().getColor( + R.color.black)); + } + webvDescription.getSettings().setUseWideViewPort(false); + webvDescription.getSettings().setLayoutAlgorithm( + WebSettings.LayoutAlgorithm.NARROW_COLUMNS); + webvDescription.getSettings().setLoadWithOverviewMode(true); + webvDescription.setWebViewClient(new WebViewClient() { + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + try { + getContext().startActivity(intent); + } catch (ActivityNotFoundException e) { + e.printStackTrace(); + return false; + } + return true; + } + }); + + loadDescriptionWebview(item); + + butAction1.setOnClickListener(new View.OnClickListener() { + DefaultActionButtonCallback actionButtonCallback = new DefaultActionButtonCallback(getContext()); + + @Override + + public void onClick(View v) { + actionButtonCallback.onActionButtonPressed(item); + FeedMedia media = item.getMedia(); + if (media != null && media.isDownloaded()) { + // playback was started, dialog should close itself + dismiss(); + } + + } + } + ); + + butAction2.setOnClickListener(new View.OnClickListener() + + { + @Override + public void onClick(View v) { + if (item.hasMedia()) { + FeedMedia media = item.getMedia(); + if (!media.isDownloaded()) { + DBTasks.playMedia(getContext(), media, true, true, true); + dismiss(); + } else { + DBWriter.deleteFeedMediaOfItem(getContext(), media.getId()); + } + } else if (item.getLink() != null) { + Uri uri = Uri.parse(item.getLink()); + getContext().startActivity(new Intent(Intent.ACTION_VIEW, uri)); + } + } + } + ); + + butMore.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + popupMenu.getMenu().clear(); + popupMenu.inflate(R.menu.feeditem_dialog); + if (item.hasMedia()) { + FeedItemMenuHandler.onPrepareMenu(popupMenuInterface, item, true, queue); + } else { + // these are already available via button1 and button2 + FeedItemMenuHandler.onPrepareMenu(popupMenuInterface, item, true, queue, + R.id.mark_read_item, R.id.visit_website_item); + } + popupMenu.show(); + } + } + ); + + popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem menuItem) { + + try { + return FeedItemMenuHandler.onMenuItemClicked(getContext(), menuItem.getItemId(), item); + } catch (DownloadRequestException e) { + e.printStackTrace(); + Toast.makeText(getContext(), e.getMessage(), Toast.LENGTH_LONG).show(); + return true; + } + } + } + ); + + updateMenuAppearance(); + } + + + private final FeedItemMenuHandler.MenuInterface popupMenuInterface = new FeedItemMenuHandler.MenuInterface() { + @Override + public void setItemVisibility(int id, boolean visible) { + MenuItem item = popupMenu.getMenu().findItem(id); + if (item != null) { + item.setVisible(visible); + } + } + }; + + public void updateMenuAppearance() { + if (item == null || queue == null) { + Log.w(TAG, "UpdateMenuAppearance called while item or queue was null"); + return; + } + FeedMedia media = item.getMedia(); + if (media == null) { + TypedArray drawables = getContext().obtainStyledAttributes(new int[]{R.attr.navigation_accept, + R.attr.location_web_site}); + + if (!item.isRead()) { + butAction1.setImageDrawable(drawables.getDrawable(0)); + butAction1.setContentDescription(getContext().getString(R.string.mark_read_label)); + butAction1.setVisibility(View.VISIBLE); + } else { + butAction1.setVisibility(View.INVISIBLE); + } + + if (item.getLink() != null) { + butAction2.setImageDrawable(drawables.getDrawable(1)); + butAction2.setContentDescription(getContext().getString(R.string.visit_website_label)); + } else { + butAction2.setEnabled(false); + } + + drawables.recycle(); + } else { + boolean isDownloading = DownloadRequester.getInstance().isDownloadingFile(media); + TypedArray drawables = getContext().obtainStyledAttributes(new int[]{R.attr.av_play, + R.attr.av_download, R.attr.action_stream, R.attr.content_discard, R.attr.navigation_cancel}); + + if (!media.isDownloaded()) { + butAction2.setImageDrawable(drawables.getDrawable(2)); + butAction2.setContentDescription(getContext().getString(R.string.stream_label)); + } else { + butAction2.setImageDrawable(drawables.getDrawable(3)); + butAction2.setContentDescription(getContext().getString(R.string.remove_episode_lable)); + } + + if (isDownloading) { + butAction1.setImageDrawable(drawables.getDrawable(4)); + butAction1.setContentDescription(getContext().getString(R.string.cancel_download_label)); + } else if (media.isDownloaded()) { + butAction1.setImageDrawable(drawables.getDrawable(0)); + butAction1.setContentDescription(getContext().getString(R.string.play_label)); + } else { + butAction1.setImageDrawable(drawables.getDrawable(1)); + butAction1.setContentDescription(getContext().getString(R.string.download_label)); + } + + drawables.recycle(); + } + } + + + private void loadDescriptionWebview(final ShownotesProvider shownotesProvider) { + AsyncTask<Void, Void, Void> loadTask = new AsyncTask<Void, Void, Void>() { + String data; + + + private String applyWebviewStyle(String textColor, String data) { + final String WEBVIEW_STYLE = "<html><head><style type=\"text/css\"> @font-face { font-family: 'Roboto-Light'; src: url('file:///android_asset/Roboto-Light.ttf'); } * { color: %s; font-family: roboto-Light; font-size: 11pt; } a { font-style: normal; text-decoration: none; font-weight: normal; color: #00A8DF; } img { display: block; margin: 10 auto; max-width: %s; height: auto; } body { margin: %dpx %dpx %dpx %dpx; }</style></head><body>%s</body></html>"; + final int pageMargin = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, 8, getContext().getResources() + .getDisplayMetrics() + ); + return String.format(WEBVIEW_STYLE, textColor, "100%", pageMargin, + pageMargin, pageMargin, pageMargin, data); + } + + + @Override + protected void onPostExecute(Void result) { + super.onPostExecute(result); + // /webvDescription.loadData(url, "text/html", "utf-8"); + if (FeedItemDialog.this.isShowing() && webvDescription != null) { + webvDescription.loadDataWithBaseURL(null, data, "text/html", + "utf-8", "about:blank"); + if (BuildConfig.DEBUG) + Log.d(TAG, "Webview loaded"); + } + } + + + @Override + protected Void doInBackground(Void... params) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Loading Webview"); + try { + Callable<String> shownotesLoadTask = shownotesProvider.loadShownotes(); + final String shownotes = shownotesLoadTask.call(); + + data = StringEscapeUtils.unescapeHtml4(shownotes); + TypedArray res = getContext() + .getTheme() + .obtainStyledAttributes( + new int[]{android.R.attr.textColorPrimary}); + int colorResource; + if (useDarkThemeWorkAround()) { + colorResource = getContext().getResources().getColor(R.color.black); + } else { + colorResource = res.getColor(0, 0); + } + String colorString = String.format("#%06X", + 0xFFFFFF & colorResource); + Log.i(TAG, "text color: " + colorString); + res.recycle(); + data = applyWebviewStyle(colorString, data); + + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + }; + loadTask.execute(); + } + + /** + * Convenience method that calls setQueue() and setItemFromCollection() with + * the given arguments. + * + * @return true if one of the calls to setItemFromCollection returned true, + * false otherwise. + */ + public boolean updateContent(QueueAccess queue, List<FeedItem>... collections) { + setQueue(queue); + + boolean setItemFromCollectionResult = false; + if (collections != null) { + for (List<FeedItem> list : collections) { + setItemFromCollectionResult |= setItemFromCollection(list); + } + } + if (isShowing()) { + updateMenuAppearance(); + } + + return setItemFromCollectionResult; + } + + + public void setItem(FeedItem item) { + Validate.notNull(item); + this.item = item; + } + + /** + * Finds the FeedItem of this dialog in a collection and updates its state from that + * collection. + * + * @return true if the FeedItem was found, false otherwise. + */ + public boolean setItemFromCollection(Collection<FeedItem> items) { + for (FeedItem item : items) { + if (item.getId() == this.item.getId()) { + setItem(item); + return true; + } + } + return false; + } + + public void setQueue(QueueAccess queue) { + Validate.notNull(queue); + this.queue = queue; + } + + public FeedItem getItem() { + return item; + } + + public QueueAccess getQueue() { + return queue; + } + + public FeedItemDialogSavedInstance save() { + return new FeedItemDialogSavedInstance(item, queue, isShowing()); + } + + /** + * Used to save the FeedItemDialog's state across configuration changes + */ + public static class FeedItemDialogSavedInstance { + final FeedItem item; + final QueueAccess queueAccess; + final boolean isShowing; + + private FeedItemDialogSavedInstance(FeedItem item, QueueAccess queueAccess, boolean isShowing) { + this.item = item; + this.queueAccess = queueAccess; + this.isShowing = isShowing; + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/GpodnetSetHostnameDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/GpodnetSetHostnameDialog.java new file mode 100644 index 000000000..16fb77f2a --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/dialog/GpodnetSetHostnameDialog.java @@ -0,0 +1,67 @@ +package de.danoeh.antennapod.dialog; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.text.Editable; +import android.text.InputType; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.LinearLayout; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.gpoddernet.GpodnetService; +import de.danoeh.antennapod.core.preferences.GpodnetPreferences; + +/** + * Creates a dialog that lets the user change the hostname for the gpodder.net service. + */ +public class GpodnetSetHostnameDialog { + private static final String TAG = "GpodnetSetHostnameDialog"; + + public static AlertDialog createDialog(final Context context) { + AlertDialog.Builder dialog = new AlertDialog.Builder(context); + final EditText et = new EditText(context); + et.setText(GpodnetPreferences.getHostname()); + et.setInputType(InputType.TYPE_TEXT_VARIATION_URI); + dialog.setTitle(R.string.pref_gpodnet_sethostname_title) + .setView(setupContentView(context, et)) + .setPositiveButton(R.string.confirm_label, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + final Editable e = et.getText(); + if (e != null) { + GpodnetPreferences.setHostname(e.toString()); + } + dialog.dismiss(); + } + }) + .setNegativeButton(R.string.cancel_label, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }) + .setNeutralButton(R.string.pref_gpodnet_sethostname_use_default_host, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + GpodnetPreferences.setHostname(GpodnetService.DEFAULT_BASE_HOST); + dialog.dismiss(); + } + }) + .setCancelable(true); + return dialog.show(); + } + + private static View setupContentView(Context context, EditText et) { + LinearLayout ll = new LinearLayout(context); + ll.addView(et); + LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) et.getLayoutParams(); + if (params != null) { + params.setMargins(8, 8, 8, 8); + params.width = ViewGroup.LayoutParams.MATCH_PARENT; + params.height = ViewGroup.LayoutParams.MATCH_PARENT; + } + return ll; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/TimeDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/TimeDialog.java new file mode 100644 index 000000000..6561d501e --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/dialog/TimeDialog.java @@ -0,0 +1,138 @@ +package de.danoeh.antennapod.dialog; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; +import android.view.View; +import android.view.Window; +import android.view.inputmethod.InputMethodManager; +import android.widget.*; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.R; + +import java.util.concurrent.TimeUnit; + +public abstract class TimeDialog extends Dialog { + private static final String TAG = "TimeDialog"; + + private static final int DEFAULT_SPINNER_POSITION = 1; + + private Context context; + + private EditText etxtTime; + private Spinner spTimeUnit; + private Button butConfirm; + private Button butCancel; + + private TimeUnit[] units = {TimeUnit.SECONDS, TimeUnit.MINUTES, + TimeUnit.HOURS}; + + public TimeDialog(Context context, int titleTextId, int leftButtonTextId) { + super(context); + this.context = context; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_NO_TITLE); + String[] spinnerContent = new String[]{context.getString(R.string.time_unit_seconds), + context.getString(R.string.time_unit_minutes), + context.getString(R.string.time_unit_hours)}; + + setContentView(R.layout.time_dialog); + etxtTime = (EditText) findViewById(R.id.etxtTime); + spTimeUnit = (Spinner) findViewById(R.id.spTimeUnit); + butConfirm = (Button) findViewById(R.id.butConfirm); + butCancel = (Button) findViewById(R.id.butCancel); + + butConfirm.setText(R.string.set_sleeptimer_label); + butCancel.setText(R.string.cancel_label); + setTitle(R.string.set_sleeptimer_label); + ArrayAdapter<String> spinnerAdapter = new ArrayAdapter<String>( + this.getContext(), android.R.layout.simple_spinner_item, + spinnerContent); + spinnerAdapter + .setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spTimeUnit.setAdapter(spinnerAdapter); + spTimeUnit.setSelection(DEFAULT_SPINNER_POSITION); + butCancel.setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View v) { + dismiss(); + } + }); + butConfirm.setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View v) { + try { + long input = readTimeMillis(); + onTimeEntered(input); + dismiss(); + } catch (NumberFormatException e) { + e.printStackTrace(); + Toast toast = Toast.makeText(context, + R.string.time_dialog_invalid_input, + Toast.LENGTH_LONG); + toast.show(); + } + } + }); + etxtTime.addTextChangedListener(new TextWatcher() { + + @Override + public void afterTextChanged(Editable s) { + checkInputLength(s.length()); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, + int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, + int count) { + + } + }); + checkInputLength(etxtTime.getText().length()); + etxtTime.postDelayed(new Runnable() { + @Override + public void run() { + InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(etxtTime, InputMethodManager.SHOW_IMPLICIT); + } + }, 100); + + + + } + + private void checkInputLength(int length) { + if (length > 0) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Length is larger than 0, enabling confirm button"); + butConfirm.setEnabled(true); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "Length is smaller than 0, disabling confirm button"); + butConfirm.setEnabled(false); + } + } + + public abstract void onTimeEntered(long millis); + + private long readTimeMillis() { + TimeUnit selectedUnit = units[spTimeUnit.getSelectedItemPosition()]; + long value = Long.valueOf(etxtTime.getText().toString()); + return selectedUnit.toMillis(value); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java new file mode 100644 index 000000000..8eba51540 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java @@ -0,0 +1,100 @@ +package de.danoeh.antennapod.dialog; + +import android.app.AlertDialog; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Uri; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.preferences.UserPreferences; + +import java.util.Arrays; +import java.util.List; + +public class VariableSpeedDialog { + private VariableSpeedDialog() { + } + + public static void showDialog(final Context context) { + if (com.aocate.media.MediaPlayer.isPrestoLibraryInstalled(context)) { + showSpeedSelectorDialog(context); + } else { + showGetPluginDialog(context); + } + } + + private static void showGetPluginDialog(final Context context) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.no_playback_plugin_title); + builder.setMessage(R.string.no_playback_plugin_msg); + builder.setNegativeButton(R.string.close_label, null); + builder.setPositiveButton(R.string.download_plugin_label, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + try { + Intent playStoreIntent = new Intent( + Intent.ACTION_VIEW, + Uri.parse("market://details?id=com.falconware.prestissimo")); + context.startActivity(playStoreIntent); + } catch (ActivityNotFoundException e) { + // this is usually thrown on an emulator if the Android market is not installed + e.printStackTrace(); + } + } + }); + builder.create().show(); + } + + private static void showSpeedSelectorDialog(final Context context) { + final String[] speedValues = context.getResources().getStringArray( + R.array.playback_speed_values); + // According to Java spec these get initialized to false on creation + final boolean[] speedChecked = new boolean[speedValues.length]; + + // Build the "isChecked" array so that multiChoice dialog is + // populated correctly + List<String> selectedSpeedList = Arrays.asList(UserPreferences + .getPlaybackSpeedArray()); + for (int i = 0; i < speedValues.length; i++) { + speedChecked[i] = selectedSpeedList.contains(speedValues[i]); + } + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.set_playback_speed_label); + builder.setMultiChoiceItems(R.array.playback_speed_values, + speedChecked, new DialogInterface.OnMultiChoiceClickListener() { + @Override + public void onClick(DialogInterface dialog, int which, + boolean isChecked) { + speedChecked[which] = isChecked; + } + + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + int choiceCount = 0; + for (int i = 0; i < speedChecked.length; i++) { + if (speedChecked[i]) { + choiceCount++; + } + } + String[] newSpeedValues = new String[choiceCount]; + int newSpeedIndex = 0; + for (int i = 0; i < speedChecked.length; i++) { + if (speedChecked[i]) { + newSpeedValues[newSpeedIndex++] = speedValues[i]; + } + } + + UserPreferences.setPlaybackSpeedArray(newSpeedValues); + + } + }); + builder.create().show(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java new file mode 100644 index 000000000..f5ae5a777 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java @@ -0,0 +1,76 @@ +package de.danoeh.antennapod.fragment; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.DefaultOnlineFeedViewActivity; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.activity.OnlineFeedViewActivity; +import de.danoeh.antennapod.activity.OpmlImportFromPathActivity; +import de.danoeh.antennapod.fragment.gpodnet.GpodnetMainFragment; + +/** + * Provides actions for adding new podcast subscriptions + */ +public class AddFeedFragment extends Fragment { + private static final String TAG = "AddFeedFragment"; + + /** + * Preset value for url text field. + */ + public static final String ARG_FEED_URL = "feedurl"; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + View root = inflater.inflate(R.layout.addfeed, container, false); + + final EditText etxtFeedurl = (EditText) root.findViewById(R.id.etxtFeedurl); + + Bundle args = getArguments(); + if (args != null && args.getString(ARG_FEED_URL) != null) { + etxtFeedurl.setText(args.getString(ARG_FEED_URL)); + } + + Button butBrowserGpoddernet = (Button) root.findViewById(R.id.butBrowseGpoddernet); + Button butOpmlImport = (Button) root.findViewById(R.id.butOpmlImport); + Button butConfirm = (Button) root.findViewById(R.id.butConfirm); + + final MainActivity activity = (MainActivity) getActivity(); + activity.getMainActivtyActionBar().setTitle(R.string.add_feed_label); + + butBrowserGpoddernet.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + activity.loadChildFragment(new GpodnetMainFragment()); + } + }); + + butOpmlImport.setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View v) { + startActivity(new Intent(getActivity(), + OpmlImportFromPathActivity.class)); + } + }); + + butConfirm.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(getActivity(), DefaultOnlineFeedViewActivity.class); + intent.putExtra(OnlineFeedViewActivity.ARG_FEEDURL, etxtFeedurl.getText().toString()); + intent.putExtra(OnlineFeedViewActivity.ARG_TITLE, getString(R.string.add_feed_label)); + startActivity(intent); + } + }); + + return root; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java new file mode 100644 index 000000000..21e4cbd80 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java @@ -0,0 +1,196 @@ +package de.danoeh.antennapod.fragment; + +import android.app.Activity; +import android.content.Context; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.v4.app.ListFragment; +import android.view.View; +import android.widget.ListView; +import de.danoeh.antennapod.adapter.DownloadedEpisodesListAdapter; +import de.danoeh.antennapod.dialog.FeedItemDialog; +import de.danoeh.antennapod.core.feed.EventDistributor; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.util.QueueAccess; + +import java.util.List; + +/** + * Displays all running downloads and provides a button to delete them + */ +public class CompletedDownloadsFragment extends ListFragment { + private static final int EVENTS = + EventDistributor.DOWNLOAD_HANDLED | + EventDistributor.DOWNLOADLOG_UPDATE | + EventDistributor.QUEUE_UPDATE | + EventDistributor.UNREAD_ITEMS_UPDATE; + + private List<FeedItem> items; + private QueueAccess queue; + private DownloadedEpisodesListAdapter listAdapter; + + private boolean viewCreated = false; + private boolean itemsLoaded = false; + + private FeedItemDialog feedItemDialog; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + startItemLoader(); + } + + @Override + public void onStart() { + super.onStart(); + EventDistributor.getInstance().register(contentUpdate); + } + + @Override + public void onStop() { + super.onStop(); + EventDistributor.getInstance().unregister(contentUpdate); + stopItemLoader(); + } + + @Override + public void onDetach() { + super.onDetach(); + stopItemLoader(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + listAdapter = null; + viewCreated = false; + feedItemDialog = null; + stopItemLoader(); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + if (viewCreated && itemsLoaded) { + onFragmentLoaded(); + } + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + viewCreated = true; + if (itemsLoaded && getActivity() != null) { + onFragmentLoaded(); + } + } + + @Override + public void onListItemClick(ListView l, View v, int position, long id) { + super.onListItemClick(l, v, position, id); + FeedItem item = listAdapter.getItem(position - l.getHeaderViewsCount()); + if (item != null) { + feedItemDialog = FeedItemDialog.newInstance(getActivity(), item, queue); + feedItemDialog.show(); + } + + } + + private void onFragmentLoaded() { + if (listAdapter == null) { + listAdapter = new DownloadedEpisodesListAdapter(getActivity(), itemAccess); + setListAdapter(listAdapter); + } + setListShown(true); + listAdapter.notifyDataSetChanged(); + if (feedItemDialog != null) { + boolean res = feedItemDialog.updateContent(queue, items); + if (!res && feedItemDialog.isShowing()) { + feedItemDialog.dismiss(); + } + } + } + + private DownloadedEpisodesListAdapter.ItemAccess itemAccess = new DownloadedEpisodesListAdapter.ItemAccess() { + @Override + public int getCount() { + return (items != null) ? items.size() : 0; + } + + @Override + public FeedItem getItem(int position) { + return (items != null) ? items.get(position) : null; + } + + @Override + public void onFeedItemSecondaryAction(FeedItem item) { + DBWriter.deleteFeedMediaOfItem(getActivity(), item.getMedia().getId()); + } + }; + + private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() { + @Override + public void update(EventDistributor eventDistributor, Integer arg) { + if ((arg & EventDistributor.DOWNLOAD_QUEUED) != 0) { + if (feedItemDialog != null && feedItemDialog.isShowing()) { + feedItemDialog.updateMenuAppearance(); + } + } else if ((arg & EVENTS) != 0) { + startItemLoader(); + } + } + }; + + private ItemLoader itemLoader; + + private void startItemLoader() { + if (itemLoader != null) { + itemLoader.cancel(true); + } + itemLoader = new ItemLoader(); + itemLoader.execute(); + } + + private void stopItemLoader() { + if (itemLoader != null) { + itemLoader.cancel(true); + } + } + + private class ItemLoader extends AsyncTask<Void, Void, Object[]> { + + @Override + protected void onPreExecute() { + super.onPreExecute(); + if (!itemsLoaded && viewCreated) { + setListShown(false); + } + } + + @Override + protected void onPostExecute(Object[] results) { + super.onPostExecute(results); + if (results != null) { + items = (List<FeedItem>) results[0]; + queue = (QueueAccess) results[1]; + itemsLoaded = true; + if (viewCreated && getActivity() != null) { + onFragmentLoaded(); + } + } + } + + @Override + protected Object[] doInBackground(Void... params) { + Context context = getActivity(); + if (context != null) { + return new Object[]{DBReader.getDownloadedItems(context), + QueueAccess.IDListAccess(DBReader.getQueueIDList(context))}; + } + return null; + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java new file mode 100644 index 000000000..69bd2b099 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java @@ -0,0 +1,105 @@ +package de.danoeh.antennapod.fragment; + +import android.content.Context; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.AudioplayerActivity.AudioplayerContentFragment; +import de.danoeh.antennapod.core.asynctask.PicassoProvider; +import de.danoeh.antennapod.core.util.playback.Playable; + +/** + * Displays the cover and the title of a FeedItem. + */ +public class CoverFragment extends Fragment implements + AudioplayerContentFragment { + private static final String TAG = "CoverFragment"; + private static final String ARG_PLAYABLE = "arg.playable"; + + private Playable media; + + private ImageView imgvCover; + + private boolean viewCreated = false; + + public static CoverFragment newInstance(Playable item) { + CoverFragment f = new CoverFragment(); + if (item != null) { + Bundle args = new Bundle(); + args.putParcelable(ARG_PLAYABLE, item); + f.setArguments(args); + } + return f; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setRetainInstance(true); + Bundle args = getArguments(); + if (args != null) { + media = args.getParcelable(ARG_PLAYABLE); + } else { + Log.e(TAG, TAG + " was called with invalid arguments"); + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View root = inflater.inflate(R.layout.cover_fragment, container, false); + imgvCover = (ImageView) root.findViewById(R.id.imgvCover); + viewCreated = true; + return root; + } + + private void loadMediaInfo() { + if (media != null) { + imgvCover.post(new Runnable() { + + @Override + public void run() { + Context c = getActivity(); + if (c != null) { + PicassoProvider.getMediaMetadataPicassoInstance(c) + .load(media.getImageUri()) + .into(imgvCover); + } + } + }); + } else { + Log.w(TAG, "loadMediaInfo was called while media was null"); + } + } + + @Override + public void onStart() { + if (BuildConfig.DEBUG) + Log.d(TAG, "On Start"); + super.onStart(); + if (media != null) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Loading media info"); + loadMediaInfo(); + } else { + Log.w(TAG, "Unable to load media info: media was null"); + } + } + + @Override + public void onDataSetChanged(Playable media) { + this.media = media; + if (viewCreated) { + loadMediaInfo(); + } + + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java new file mode 100644 index 000000000..9c7fade67 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java @@ -0,0 +1,121 @@ +package de.danoeh.antennapod.fragment; + +import android.content.Context; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.v4.app.ListFragment; +import android.view.View; +import de.danoeh.antennapod.adapter.DownloadLogAdapter; +import de.danoeh.antennapod.core.feed.EventDistributor; +import de.danoeh.antennapod.core.service.download.DownloadStatus; +import de.danoeh.antennapod.core.storage.DBReader; + +import java.util.List; + +/** + * Shows the download log + */ +public class DownloadLogFragment extends ListFragment { + + private List<DownloadStatus> downloadLog; + private DownloadLogAdapter adapter; + + private boolean viewsCreated = false; + private boolean itemsLoaded = false; + + @Override + public void onStart() { + super.onStart(); + EventDistributor.getInstance().register(contentUpdate); + startItemLoader(); + } + + @Override + public void onStop() { + super.onStop(); + EventDistributor.getInstance().unregister(contentUpdate); + stopItemLoader(); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + viewsCreated = true; + if (itemsLoaded) { + onFragmentLoaded(); + } + } + + private void onFragmentLoaded() { + if (adapter == null) { + adapter = new DownloadLogAdapter(getActivity(), itemAccess); + setListAdapter(adapter); + } + setListShown(true); + adapter.notifyDataSetChanged(); + + } + + private DownloadLogAdapter.ItemAccess itemAccess = new DownloadLogAdapter.ItemAccess() { + + @Override + public int getCount() { + return (downloadLog != null) ? downloadLog.size() : 0; + } + + @Override + public DownloadStatus getItem(int position) { + return (downloadLog != null) ? downloadLog.get(position) : null; + } + }; + + private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() { + + @Override + public void update(EventDistributor eventDistributor, Integer arg) { + if ((arg & EventDistributor.DOWNLOADLOG_UPDATE) != 0) { + startItemLoader(); + } + } + }; + + private ItemLoader itemLoader; + + private void startItemLoader() { + if (itemLoader != null) { + itemLoader.cancel(true); + } + itemLoader = new ItemLoader(); + itemLoader.execute(); + } + + private void stopItemLoader() { + if (itemLoader != null) { + itemLoader.cancel(true); + } + } + + private class ItemLoader extends AsyncTask<Void, Void, List<DownloadStatus>> { + + @Override + protected void onPostExecute(List<DownloadStatus> downloadStatuses) { + super.onPostExecute(downloadStatuses); + if (downloadStatuses != null) { + downloadLog = downloadStatuses; + itemsLoaded = true; + if (viewsCreated) { + onFragmentLoaded(); + } + } + } + + @Override + protected List<DownloadStatus> doInBackground(Void... params) { + Context context = getActivity(); + if (context != null) { + return DBReader.getDownloadLog(context); + } + return null; + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadsFragment.java new file mode 100644 index 000000000..5a71cb36b --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadsFragment.java @@ -0,0 +1,145 @@ +package de.danoeh.antennapod.fragment; + +import android.app.Activity; +import android.content.res.Resources; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentPagerAdapter; +import android.support.v4.app.FragmentTransaction; +import android.support.v4.view.ViewPager; +import android.support.v7.app.ActionBar; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; + +/** + * Shows the CompletedDownloadsFragment and the RunningDownloadsFragment + */ +public class DownloadsFragment extends Fragment { + + public static final String ARG_SELECTED_TAB = "selected_tab"; + + public static final int POS_RUNNING = 0; + public static final int POS_COMPLETED = 1; + public static final int POS_LOG = 2; + + private ViewPager pager; + private MainActivity activity; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + View root = inflater.inflate(R.layout.pager_fragment, container, false); + pager = (ViewPager) root.findViewById(R.id.pager); + DownloadsPagerAdapter pagerAdapter = new DownloadsPagerAdapter(getChildFragmentManager(), getResources()); + pager.setAdapter(pagerAdapter); + final ActionBar actionBar = activity.getMainActivtyActionBar(); + actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); + ActionBar.TabListener tabListener = new ActionBar.TabListener() { + @Override + public void onTabSelected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) { + pager.setCurrentItem(tab.getPosition()); + } + + @Override + public void onTabUnselected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) { + + } + + @Override + public void onTabReselected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) { + + } + }; + actionBar.removeAllTabs(); + actionBar.addTab(actionBar.newTab() + .setText(R.string.downloads_running_label) + .setTabListener(tabListener)); + actionBar.addTab(actionBar.newTab() + .setText(R.string.downloads_completed_label) + .setTabListener(tabListener)); + actionBar.addTab(actionBar.newTab() + .setText(R.string.downloads_log_label) + .setTabListener(tabListener)); + + pager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { + @Override + public void onPageSelected(int position) { + super.onPageSelected(position); + actionBar.setSelectedNavigationItem(position); + } + }); + return root; + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + if (getArguments() != null) { + int tab = getArguments().getInt(ARG_SELECTED_TAB); + pager.setCurrentItem(tab, false); + } + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + this.activity = (MainActivity) activity; + } + + @Override + public void onDetach() { + super.onDetach(); + activity.getMainActivtyActionBar().removeAllTabs(); + activity.getMainActivtyActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD); + } + + public class DownloadsPagerAdapter extends FragmentPagerAdapter { + + + + + Resources resources; + + public DownloadsPagerAdapter(FragmentManager fm, Resources resources) { + super(fm); + this.resources = resources; + } + + @Override + public Fragment getItem(int position) { + switch (position) { + case POS_RUNNING: + return new RunningDownloadsFragment(); + case POS_COMPLETED: + return new CompletedDownloadsFragment(); + case POS_LOG: + return new DownloadLogFragment(); + default: + return null; + } + } + + @Override + public int getCount() { + return 3; + } + + @Override + public CharSequence getPageTitle(int position) { + switch (position) { + case POS_RUNNING: + return resources.getString(R.string.downloads_running_label); + case POS_COMPLETED: + return resources.getString(R.string.downloads_completed_label); + case POS_LOG: + return resources.getString(R.string.downloads_log_label); + default: + return super.getPageTitle(position); + } + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java new file mode 100644 index 000000000..99320cffa --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java @@ -0,0 +1,238 @@ +package de.danoeh.antennapod.fragment; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; + +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.asynctask.PicassoProvider; +import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.core.util.Converter; +import de.danoeh.antennapod.core.util.playback.Playable; +import de.danoeh.antennapod.core.util.playback.PlaybackController; + +/** + * Fragment which is supposed to be displayed outside of the MediaplayerActivity + * if the PlaybackService is running + */ +public class ExternalPlayerFragment extends Fragment { + private static final String TAG = "ExternalPlayerFragment"; + + private ViewGroup fragmentLayout; + private ImageView imgvCover; + private ViewGroup layoutInfo; + private TextView txtvTitle; + private ImageButton butPlay; + + private PlaybackController controller; + + public ExternalPlayerFragment() { + super(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View root = inflater.inflate(R.layout.external_player_fragment, + container, false); + fragmentLayout = (ViewGroup) root.findViewById(R.id.fragmentLayout); + imgvCover = (ImageView) root.findViewById(R.id.imgvCover); + layoutInfo = (ViewGroup) root.findViewById(R.id.layoutInfo); + txtvTitle = (TextView) root.findViewById(R.id.txtvTitle); + butPlay = (ImageButton) root.findViewById(R.id.butPlay); + + layoutInfo.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + if (BuildConfig.DEBUG) + Log.d(TAG, "layoutInfo was clicked"); + + if (controller.getMedia() != null) { + startActivity(PlaybackService.getPlayerActivityIntent( + getActivity(), controller.getMedia())); + } + } + }); + return root; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + controller = setupPlaybackController(); + butPlay.setOnClickListener(controller.newOnPlayButtonClickListener()); + } + + private PlaybackController setupPlaybackController() { + return new PlaybackController(getActivity(), true) { + + @Override + public void setupGUI() { + } + + @Override + public void onPositionObserverUpdate() { + } + + @Override + public void onReloadNotification(int code) { + } + + @Override + public void onBufferStart() { + // TODO Auto-generated method stub + + } + + @Override + public void onBufferEnd() { + // TODO Auto-generated method stub + + } + + @Override + public void onBufferUpdate(float progress) { + } + + @Override + public void onSleepTimerUpdate() { + } + + @Override + public void handleError(int code) { + } + + @Override + public ImageButton getPlayButton() { + return butPlay; + } + + @Override + public void postStatusMsg(int msg) { + } + + @Override + public void clearStatusMsg() { + } + + @Override + public boolean loadMediaInfo() { + ExternalPlayerFragment fragment = ExternalPlayerFragment.this; + if (fragment != null) { + return fragment.loadMediaInfo(); + } else { + return false; + } + } + + @Override + public void onAwaitingVideoSurface() { + } + + @Override + public void onServiceQueried() { + } + + @Override + public void onShutdownNotification() { + if (fragmentLayout != null) { + fragmentLayout.setVisibility(View.GONE); + } + controller = setupPlaybackController(); + if (butPlay != null) { + butPlay.setOnClickListener(controller + .newOnPlayButtonClickListener()); + } + + } + + @Override + public void onPlaybackEnd() { + if (fragmentLayout != null) { + fragmentLayout.setVisibility(View.GONE); + } + controller = setupPlaybackController(); + if (butPlay != null) { + butPlay.setOnClickListener(controller + .newOnPlayButtonClickListener()); + } + } + + @Override + public void onPlaybackSpeedChange() { + // TODO Auto-generated method stub + + } + }; + } + + @Override + public void onResume() { + super.onResume(); + controller.init(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Fragment is about to be destroyed"); + if (controller != null) { + controller.release(); + } + } + + @Override + public void onPause() { + super.onPause(); + if (controller != null) { + controller.pause(); + } + } + + private boolean loadMediaInfo() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Loading media info"); + if (controller.serviceAvailable()) { + Playable media = controller.getMedia(); + if (media != null) { + txtvTitle.setText(media.getEpisodeTitle()); + + PicassoProvider.getMediaMetadataPicassoInstance(getActivity()) + .load(media.getImageUri()) + .fit() + .into(imgvCover); + + fragmentLayout.setVisibility(View.VISIBLE); + if (controller.isPlayingVideo()) { + butPlay.setVisibility(View.GONE); + } else { + butPlay.setVisibility(View.VISIBLE); + } + return true; + } else { + Log.w(TAG, + "loadMediaInfo was called while the media object of playbackService was null!"); + return false; + } + } else { + Log.w(TAG, + "loadMediaInfo was called while playbackService was null!"); + return false; + } + } + + private String getPositionString(int position, int duration) { + return Converter.getDurationStringLong(position) + " / " + + Converter.getDurationStringLong(duration); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java new file mode 100644 index 000000000..f0e73a372 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java @@ -0,0 +1,476 @@ +package de.danoeh.antennapod.fragment; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.ClipData; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v7.app.ActionBarActivity; +import android.util.Log; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.WebSettings.LayoutAlgorithm; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.Toast; + +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.util.Converter; +import de.danoeh.antennapod.core.util.ShareUtils; +import de.danoeh.antennapod.core.util.ShownotesProvider; +import de.danoeh.antennapod.core.util.playback.Playable; +import de.danoeh.antennapod.core.util.playback.PlaybackController; +import de.danoeh.antennapod.core.util.playback.Timeline; + +/** + * Displays the description of a Playable object in a Webview. + */ +public class ItemDescriptionFragment extends Fragment { + + private static final String TAG = "ItemDescriptionFragment"; + + private static final String PREF = "ItemDescriptionFragmentPrefs"; + private static final String PREF_SCROLL_Y = "prefScrollY"; + private static final String PREF_PLAYABLE_ID = "prefPlayableId"; + + private static final String ARG_PLAYABLE = "arg.playable"; + private static final String ARG_FEEDITEM_ID = "arg.feeditem"; + + private static final String ARG_SAVE_STATE = "arg.saveState"; + private static final String ARG_HIGHLIGHT_TIMECODES = "arg.highlightTimecodes"; + + private WebView webvDescription; + + private ShownotesProvider shownotesProvider; + private Playable media; + + + private AsyncTask<Void, Void, Void> webViewLoader; + + /** + * URL that was selected via long-press. + */ + private String selectedURL; + + /** + * True if Fragment should save its state (e.g. scrolling position) in a + * shared preference. + */ + private boolean saveState; + + /** + * True if Fragment should highlight timecodes (e.g. time codes in the HH:MM:SS format). + */ + private boolean highlightTimecodes; + + public static ItemDescriptionFragment newInstance(Playable media, + boolean saveState, + boolean highlightTimecodes) { + ItemDescriptionFragment f = new ItemDescriptionFragment(); + Bundle args = new Bundle(); + args.putParcelable(ARG_PLAYABLE, media); + args.putBoolean(ARG_SAVE_STATE, saveState); + args.putBoolean(ARG_HIGHLIGHT_TIMECODES, highlightTimecodes); + f.setArguments(args); + return f; + } + + public static ItemDescriptionFragment newInstance(FeedItem item, boolean saveState, boolean highlightTimecodes) { + ItemDescriptionFragment f = new ItemDescriptionFragment(); + Bundle args = new Bundle(); + args.putLong(ARG_FEEDITEM_ID, item.getId()); + args.putBoolean(ARG_SAVE_STATE, saveState); + args.putBoolean(ARG_HIGHLIGHT_TIMECODES, highlightTimecodes); + f.setArguments(args); + return f; + } + + @SuppressLint("NewApi") + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Creating view"); + webvDescription = new WebView(getActivity()); + if (UserPreferences.getTheme() == R.style.Theme_AntennaPod_Dark) { + if (Build.VERSION.SDK_INT >= 11 + && Build.VERSION.SDK_INT <= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { + webvDescription.setLayerType(View.LAYER_TYPE_SOFTWARE, null); + } + webvDescription.setBackgroundColor(getResources().getColor( + R.color.black)); + } + webvDescription.getSettings().setUseWideViewPort(false); + webvDescription.getSettings().setLayoutAlgorithm( + LayoutAlgorithm.NARROW_COLUMNS); + webvDescription.getSettings().setLoadWithOverviewMode(true); + webvDescription.setOnLongClickListener(webViewLongClickListener); + webvDescription.setWebViewClient(new WebViewClient() { + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + if (Timeline.isTimecodeLink(url)) { + onTimecodeLinkSelected(url); + } else { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + try { + startActivity(intent); + } catch (ActivityNotFoundException e) { + e.printStackTrace(); + return true; + } + } + return true; + } + + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + if (BuildConfig.DEBUG) + Log.d(TAG, "Page finished"); + // Restoring the scroll position might not always work + view.postDelayed(new Runnable() { + + @Override + public void run() { + restoreFromPreference(); + } + + }, 50); + } + + }); + + registerForContextMenu(webvDescription); + return webvDescription; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + if (BuildConfig.DEBUG) + Log.d(TAG, "Fragment attached"); + } + + @Override + public void onDetach() { + super.onDetach(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Fragment detached"); + if (webViewLoader != null) { + webViewLoader.cancel(true); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Fragment destroyed"); + if (webViewLoader != null) { + webViewLoader.cancel(true); + } + } + + @SuppressLint("NewApi") + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (BuildConfig.DEBUG) + Log.d(TAG, "Creating fragment"); + Bundle args = getArguments(); + saveState = args.getBoolean(ARG_SAVE_STATE, false); + highlightTimecodes = args.getBoolean(ARG_HIGHLIGHT_TIMECODES, false); + + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + Bundle args = getArguments(); + if (args.containsKey(ARG_PLAYABLE)) { + media = args.getParcelable(ARG_PLAYABLE); + shownotesProvider = media; + startLoader(); + } else if (args.containsKey(ARG_FEEDITEM_ID)) { + AsyncTask<Void, Void, FeedItem> itemLoadTask = new AsyncTask<Void, Void, FeedItem>() { + + @Override + protected FeedItem doInBackground(Void... voids) { + return DBReader.getFeedItem(getActivity(), getArguments().getLong(ARG_FEEDITEM_ID)); + } + + @Override + protected void onPostExecute(FeedItem feedItem) { + super.onPostExecute(feedItem); + shownotesProvider = feedItem; + startLoader(); + } + }; + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { + itemLoadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + itemLoadTask.execute(); + } + } + + + } + + @Override + public void onResume() { + super.onResume(); + } + + @SuppressLint("NewApi") + private void startLoader() { + webViewLoader = createLoader(); + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { + webViewLoader.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + webViewLoader.execute(); + } + } + + private View.OnLongClickListener webViewLongClickListener = new View.OnLongClickListener() { + + @Override + public boolean onLongClick(View v) { + WebView.HitTestResult r = webvDescription.getHitTestResult(); + if (r != null + && r.getType() == WebView.HitTestResult.SRC_ANCHOR_TYPE) { + if (BuildConfig.DEBUG) + Log.d(TAG, + "Link of webview was long-pressed. Extra: " + + r.getExtra() + ); + selectedURL = r.getExtra(); + webvDescription.showContextMenu(); + return true; + } + selectedURL = null; + return false; + } + }; + + @SuppressWarnings("deprecation") + @SuppressLint("NewApi") + @Override + public boolean onContextItemSelected(MenuItem item) { + boolean handled = selectedURL != null; + if (selectedURL != null) { + switch (item.getItemId()) { + case R.id.open_in_browser_item: + Uri uri = Uri.parse(selectedURL); + getActivity() + .startActivity(new Intent(Intent.ACTION_VIEW, uri)); + break; + case R.id.share_url_item: + ShareUtils.shareLink(getActivity(), selectedURL); + break; + case R.id.copy_url_item: + if (android.os.Build.VERSION.SDK_INT >= 11) { + ClipData clipData = ClipData.newPlainText(selectedURL, + selectedURL); + android.content.ClipboardManager cm = (android.content.ClipboardManager) getActivity() + .getSystemService(Context.CLIPBOARD_SERVICE); + cm.setPrimaryClip(clipData); + } else { + android.text.ClipboardManager cm = (android.text.ClipboardManager) getActivity() + .getSystemService(Context.CLIPBOARD_SERVICE); + cm.setText(selectedURL); + } + Toast t = Toast.makeText(getActivity(), + R.string.copied_url_msg, Toast.LENGTH_SHORT); + t.show(); + break; + case R.id.go_to_position_item: + if (Timeline.isTimecodeLink(selectedURL)) { + onTimecodeLinkSelected(selectedURL); + } else { + Log.e(TAG, "Selected go_to_position_item, but URL was no timecode link: " + selectedURL); + } + break; + default: + handled = false; + break; + + } + selectedURL = null; + } + return handled; + + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, + ContextMenuInfo menuInfo) { + if (selectedURL != null) { + super.onCreateContextMenu(menu, v, menuInfo); + if (Timeline.isTimecodeLink(selectedURL)) { + menu.add(Menu.NONE, R.id.go_to_position_item, Menu.NONE, + R.string.go_to_position_label); + menu.setHeaderTitle(Converter.getDurationStringLong(Timeline.getTimecodeLinkTime(selectedURL))); + } else { + menu.add(Menu.NONE, R.id.open_in_browser_item, Menu.NONE, + R.string.open_in_browser_label); + menu.add(Menu.NONE, R.id.copy_url_item, Menu.NONE, + R.string.copy_url_label); + menu.add(Menu.NONE, R.id.share_url_item, Menu.NONE, + R.string.share_url_label); + menu.setHeaderTitle(selectedURL); + } + } + } + + private AsyncTask<Void, Void, Void> createLoader() { + return new AsyncTask<Void, Void, Void>() { + @Override + protected void onCancelled() { + super.onCancelled(); + if (getActivity() != null) { + ((ActionBarActivity) getActivity()) + .setSupportProgressBarIndeterminateVisibility(false); + } + webViewLoader = null; + } + + String data; + + @Override + protected void onPostExecute(Void result) { + super.onPostExecute(result); + // /webvDescription.loadData(url, "text/html", "utf-8"); + webvDescription.loadDataWithBaseURL(null, data, "text/html", + "utf-8", "about:blank"); + if (getActivity() != null) { + ((ActionBarActivity) getActivity()) + .setSupportProgressBarIndeterminateVisibility(false); + } + if (BuildConfig.DEBUG) + Log.d(TAG, "Webview loaded"); + webViewLoader = null; + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + if (getActivity() != null) { + ((ActionBarActivity) getActivity()) + .setSupportProgressBarIndeterminateVisibility(false); + } + } + + @Override + protected Void doInBackground(Void... params) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Loading Webview"); + try { + Activity activity = getActivity(); + if (activity != null) { + Timeline timeline = new Timeline(activity, shownotesProvider); + data = timeline.processShownotes(highlightTimecodes); + } else { + cancel(true); + } + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + }; + } + + @Override + public void onPause() { + super.onPause(); + savePreference(); + } + + private void savePreference() { + if (saveState) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Saving preferences"); + SharedPreferences prefs = getActivity().getSharedPreferences(PREF, + Activity.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + if (media != null && webvDescription != null) { + if (BuildConfig.DEBUG) + Log.d(TAG, + "Saving scroll position: " + + webvDescription.getScrollY() + ); + editor.putInt(PREF_SCROLL_Y, webvDescription.getScrollY()); + editor.putString(PREF_PLAYABLE_ID, media.getIdentifier() + .toString()); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, + "savePreferences was called while media or webview was null"); + editor.putInt(PREF_SCROLL_Y, -1); + editor.putString(PREF_PLAYABLE_ID, ""); + } + editor.commit(); + } + } + + private boolean restoreFromPreference() { + if (saveState) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Restoring from preferences"); + Activity activity = getActivity(); + if (activity != null) { + SharedPreferences prefs = activity.getSharedPreferences( + PREF, Activity.MODE_PRIVATE); + String id = prefs.getString(PREF_PLAYABLE_ID, ""); + int scrollY = prefs.getInt(PREF_SCROLL_Y, -1); + if (scrollY != -1 && media != null + && id.equals(media.getIdentifier().toString()) + && webvDescription != null) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Restored scroll Position: " + scrollY); + webvDescription.scrollTo(webvDescription.getScrollX(), + scrollY); + return true; + } + } + } + return false; + } + + private void onTimecodeLinkSelected(String link) { + int time = Timeline.getTimecodeLinkTime(link); + if (getActivity() != null && getActivity() instanceof ItemDescriptionFragmentCallback) { + PlaybackController pc = ((ItemDescriptionFragmentCallback) getActivity()).getPlaybackController(); + if (pc != null) { + pc.seekTo(time); + } + } + } + + public interface ItemDescriptionFragmentCallback { + public PlaybackController getPlaybackController(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemlistFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemlistFragment.java new file mode 100644 index 000000000..9eaeb56dd --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemlistFragment.java @@ -0,0 +1,456 @@ +package de.danoeh.antennapod.fragment; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Handler; +import android.support.v4.app.ListFragment; +import android.support.v7.app.ActionBarActivity; +import android.support.v7.widget.SearchView; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; + +import org.apache.commons.lang3.Validate; + +import java.util.List; + +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.FeedInfoActivity; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.adapter.DefaultActionButtonCallback; +import de.danoeh.antennapod.adapter.FeedItemlistAdapter; +import de.danoeh.antennapod.core.asynctask.DownloadObserver; +import de.danoeh.antennapod.core.asynctask.FeedRemover; +import de.danoeh.antennapod.core.asynctask.PicassoProvider; +import de.danoeh.antennapod.core.dialog.ConfirmationDialog; +import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator; +import de.danoeh.antennapod.dialog.FeedItemDialog; +import de.danoeh.antennapod.core.feed.EventDistributor; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.service.download.DownloadService; +import de.danoeh.antennapod.core.service.download.Downloader; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.QueueAccess; +import de.danoeh.antennapod.menuhandler.FeedMenuHandler; +import de.danoeh.antennapod.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.menuhandler.NavDrawerActivity; + +/** + * Displays a list of FeedItems. + */ +@SuppressLint("ValidFragment") +public class ItemlistFragment extends ListFragment { + private static final String TAG = "ItemlistFragment"; + + private static final int EVENTS = EventDistributor.DOWNLOAD_HANDLED + | EventDistributor.DOWNLOAD_QUEUED + | EventDistributor.QUEUE_UPDATE + | EventDistributor.UNREAD_ITEMS_UPDATE; + + public static final String EXTRA_SELECTED_FEEDITEM = "extra.de.danoeh.antennapod.activity.selected_feeditem"; + public static final String ARGUMENT_FEED_ID = "argument.de.danoeh.antennapod.feed_id"; + + protected FeedItemlistAdapter adapter; + + private long feedID; + private Feed feed; + protected QueueAccess queue; + + private boolean itemsLoaded = false; + private boolean viewsCreated = false; + + private DownloadObserver downloadObserver; + private List<Downloader> downloaderList; + + private FeedItemDialog feedItemDialog; + private FeedItemDialog.FeedItemDialogSavedInstance feedItemDialogSavedInstance; + + + /** + * Creates new ItemlistFragment which shows the Feeditems of a specific + * feed. Sets 'showFeedtitle' to false + * + * @param feedId The id of the feed to show + * @return the newly created instance of an ItemlistFragment + */ + public static ItemlistFragment newInstance(long feedId) { + ItemlistFragment i = new ItemlistFragment(); + Bundle b = new Bundle(); + b.putLong(ARGUMENT_FEED_ID, feedId); + i.setArguments(b); + return i; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setRetainInstance(true); + setHasOptionsMenu(true); + + Bundle args = getArguments(); + Validate.notNull(args); + feedID = args.getLong(ARGUMENT_FEED_ID); + } + + @Override + public void onStart() { + super.onStart(); + EventDistributor.getInstance().register(contentUpdate); + if (downloadObserver != null) { + downloadObserver.setActivity(getActivity()); + downloadObserver.onResume(); + } + if (viewsCreated && itemsLoaded) { + onFragmentLoaded(); + } + } + + @Override + public void onStop() { + super.onStop(); + EventDistributor.getInstance().unregister(contentUpdate); + stopItemLoader(); + } + + @Override + public void onResume() { + super.onResume(); + updateProgressBarVisibility(); + startItemLoader(); + } + + @Override + public void onDetach() { + super.onDetach(); + stopItemLoader(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + resetViewState(); + } + + private void resetViewState() { + adapter = null; + viewsCreated = false; + if (downloadObserver != null) { + downloadObserver.onPause(); + } + if (feedItemDialog != null) { + feedItemDialogSavedInstance = feedItemDialog.save(); + } + feedItemDialog = null; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + + if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { + FeedMenuHandler.onCreateOptionsMenu(inflater, menu); + + final SearchView sv = new SearchView(getActivity()); + MenuItemUtils.addSearchItem(menu, sv); + sv.setQueryHint(getString(R.string.search_hint)); + sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String s) { + sv.clearFocus(); + if (itemsLoaded) { + ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance(s, feed.getId())); + } + return true; + } + + @Override + public boolean onQueryTextChange(String s) { + return false; + } + }); + } + } + + @Override + public void onPrepareOptionsMenu(Menu menu) { + if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { + FeedMenuHandler.onPrepareOptionsMenu(menu, feed); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (!super.onOptionsItemSelected(item)) { + try { + if (!FeedMenuHandler.onOptionsItemClicked(getActivity(), item, feed)) { + switch (item.getItemId()) { + case R.id.remove_item: + final FeedRemover remover = new FeedRemover( + getActivity(), feed) { + @Override + protected void onPostExecute(Void result) { + super.onPostExecute(result); + ((MainActivity) getActivity()).loadNavFragment(MainActivity.POS_NEW, null); + } + }; + ConfirmationDialog conDialog = new ConfirmationDialog(getActivity(), + R.string.remove_feed_label, + R.string.feed_delete_confirmation_msg) { + + @Override + public void onConfirmButtonPressed( + DialogInterface dialog) { + dialog.dismiss(); + remover.executeAsync(); + } + }; + conDialog.createNewDialog().show(); + return true; + default: + return false; + + } + } else { + return true; + } + } catch (DownloadRequestException e) { + e.printStackTrace(); + DownloadRequestErrorDialogCreator.newRequestErrorDialog(getActivity(), e.getMessage()); + return true; + } + } else { + return true; + } + + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + ((ActionBarActivity) getActivity()).getSupportActionBar().setTitle(""); + + viewsCreated = true; + if (itemsLoaded) { + onFragmentLoaded(); + } + } + + @Override + public void onListItemClick(ListView l, View v, int position, long id) { + FeedItem selection = adapter.getItem(position - l.getHeaderViewsCount()); + feedItemDialog = FeedItemDialog.newInstance(getActivity(), selection, queue); + feedItemDialog.show(); + } + + private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() { + + @Override + public void update(EventDistributor eventDistributor, Integer arg) { + if ((EVENTS & arg) != 0) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Received contentUpdate Intent."); + if ((EventDistributor.DOWNLOAD_QUEUED & arg) != 0) { + updateProgressBarVisibility(); + } else { + startItemLoader(); + updateProgressBarVisibility(); + } + } + } + }; + + private void updateProgressBarVisibility() { + if (feed != null) { + if (DownloadService.isRunning + && DownloadRequester.getInstance().isDownloadingFile(feed)) { + ((ActionBarActivity) getActivity()) + .setSupportProgressBarIndeterminateVisibility(true); + } else { + ((ActionBarActivity) getActivity()) + .setSupportProgressBarIndeterminateVisibility(false); + } + getActivity().supportInvalidateOptionsMenu(); + } + } + + private void onFragmentLoaded() { + if (adapter == null) { + getListView().setAdapter(null); + setupHeaderView(); + adapter = new FeedItemlistAdapter(getActivity(), itemAccess, new DefaultActionButtonCallback(getActivity()), false); + setListAdapter(adapter); + downloadObserver = new DownloadObserver(getActivity(), new Handler(), downloadObserverCallback); + downloadObserver.onResume(); + } + setListShown(true); + adapter.notifyDataSetChanged(); + + if (feedItemDialog != null) { + feedItemDialog.updateContent(queue, feed.getItems()); + } else if (feedItemDialogSavedInstance != null) { + feedItemDialog = FeedItemDialog.newInstance(getActivity(), feedItemDialogSavedInstance); + } + getActivity().supportInvalidateOptionsMenu(); + } + + private DownloadObserver.Callback downloadObserverCallback = new DownloadObserver.Callback() { + @Override + public void onContentChanged() { + if (adapter != null) { + adapter.notifyDataSetChanged(); + } + if (feedItemDialog != null && feedItemDialog.isShowing()) { + feedItemDialog.updateMenuAppearance(); + } + } + + @Override + public void onDownloadDataAvailable(List<Downloader> downloaderList) { + ItemlistFragment.this.downloaderList = downloaderList; + if (adapter != null) { + adapter.notifyDataSetChanged(); + } + } + }; + + private void setupHeaderView() { + if (getListView() == null || feed == null) { + Log.e(TAG, "Unable to setup listview: listView = null or feed = null"); + return; + } + ListView lv = getListView(); + LayoutInflater inflater = (LayoutInflater) + getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View header = inflater.inflate(R.layout.feeditemlist_header, lv, false); + lv.addHeaderView(header); + + TextView txtvTitle = (TextView) header.findViewById(R.id.txtvTitle); + TextView txtvAuthor = (TextView) header.findViewById(R.id.txtvAuthor); + ImageView imgvCover = (ImageView) header.findViewById(R.id.imgvCover); + ImageButton butShowInfo = (ImageButton) header.findViewById(R.id.butShowInfo); + ImageButton butVisitWebsite = (ImageButton) header.findViewById(R.id.butVisitWebsite); + + txtvTitle.setText(feed.getTitle()); + txtvAuthor.setText(feed.getAuthor()); + + PicassoProvider.getDefaultPicassoInstance(getActivity()) + .load(feed.getImageUri()) + .fit() + .into(imgvCover); + + if (feed.getLink() == null) { + butVisitWebsite.setVisibility(View.INVISIBLE); + } else { + butVisitWebsite.setVisibility(View.VISIBLE); + butVisitWebsite.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Uri uri = Uri.parse(feed.getLink()); + startActivity(new Intent(Intent.ACTION_VIEW, uri)); + } + }); + } + butShowInfo.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (viewsCreated && itemsLoaded) { + Intent startIntent = new Intent(getActivity(), FeedInfoActivity.class); + startIntent.putExtra(FeedInfoActivity.EXTRA_FEED_ID, + feed.getId()); + startActivity(startIntent); + } + } + }); + } + + private FeedItemlistAdapter.ItemAccess itemAccess = new FeedItemlistAdapter.ItemAccess() { + + @Override + public FeedItem getItem(int position) { + return (feed != null) ? feed.getItemAtIndex(true, position) : null; + } + + @Override + public int getCount() { + return (feed != null) ? feed.getNumOfItems(true) : 0; + } + + @Override + public boolean isInQueue(FeedItem item) { + return (queue != null) && queue.contains(item.getId()); + } + + @Override + public int getItemDownloadProgressPercent(FeedItem item) { + if (downloaderList != null) { + for (Downloader downloader : downloaderList) { + if (downloader.getDownloadRequest().getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA + && downloader.getDownloadRequest().getFeedfileId() == item.getMedia().getId()) { + return downloader.getDownloadRequest().getProgressPercent(); + } + } + } + return 0; + } + }; + + private ItemLoader itemLoader; + + private void startItemLoader() { + if (itemLoader != null) { + itemLoader.cancel(true); + } + itemLoader = new ItemLoader(); + itemLoader.execute(feedID); + } + + private void stopItemLoader() { + if (itemLoader != null) { + itemLoader.cancel(true); + } + } + + private class ItemLoader extends AsyncTask<Long, Void, Object[]> { + @Override + protected Object[] doInBackground(Long... params) { + long feedID = params[0]; + Context context = getActivity(); + if (context != null) { + return new Object[]{DBReader.getFeed(context, feedID), + QueueAccess.IDListAccess(DBReader.getQueueIDList(context))}; + } else { + return null; + } + } + + @Override + protected void onPostExecute(Object[] res) { + super.onPostExecute(res); + if (res != null) { + feed = (Feed) res[0]; + queue = (QueueAccess) res[1]; + itemsLoaded = true; + if (viewsCreated) { + onFragmentLoaded(); + } + } + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java new file mode 100644 index 000000000..d126f2980 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java @@ -0,0 +1,425 @@ +package de.danoeh.antennapod.fragment; + +import android.app.Activity; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Handler; +import android.support.v4.app.Fragment; +import android.support.v7.app.ActionBarActivity; +import android.support.v7.widget.SearchView; +import android.view.*; +import android.widget.AdapterView; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; +import com.mobeta.android.dslv.DragSortListView; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.adapter.DefaultActionButtonCallback; +import de.danoeh.antennapod.adapter.NewEpisodesListAdapter; +import de.danoeh.antennapod.core.asynctask.DownloadObserver; +import de.danoeh.antennapod.dialog.FeedItemDialog; +import de.danoeh.antennapod.core.feed.EventDistributor; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.service.download.DownloadService; +import de.danoeh.antennapod.core.service.download.Downloader; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.QueueAccess; +import de.danoeh.antennapod.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.menuhandler.NavDrawerActivity; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Shows unread or recently published episodes + */ +public class NewEpisodesFragment extends Fragment { + private static final String TAG = "NewEpisodesFragment"; + private static final int EVENTS = EventDistributor.DOWNLOAD_HANDLED | + EventDistributor.DOWNLOAD_QUEUED | + EventDistributor.QUEUE_UPDATE | + EventDistributor.UNREAD_ITEMS_UPDATE; + + private static final int RECENT_EPISODES_LIMIT = 150; + private static final String PREF_NAME = "PrefNewEpisodesFragment"; + private static final String PREF_EPISODE_FILTER_BOOL = "newEpisodeFilterEnabled"; + + + private DragSortListView listView; + private NewEpisodesListAdapter listAdapter; + private TextView txtvEmpty; + private ProgressBar progLoading; + + private List<FeedItem> unreadItems; + private List<FeedItem> recentItems; + private QueueAccess queueAccess; + private List<Downloader> downloaderList; + + private boolean itemsLoaded = false; + private boolean viewsCreated = false; + private boolean showOnlyNewEpisodes = false; + + private AtomicReference<MainActivity> activity = new AtomicReference<MainActivity>(); + + private DownloadObserver downloadObserver = null; + + private FeedItemDialog feedItemDialog; + private FeedItemDialog.FeedItemDialogSavedInstance feedItemDialogSavedInstance; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setRetainInstance(true); + setHasOptionsMenu(true); + + updateShowOnlyEpisodes(); + } + + @Override + public void onResume() { + super.onResume(); + startItemLoader(); + } + + @Override + public void onStart() { + super.onStart(); + EventDistributor.getInstance().register(contentUpdate); + this.activity.set((MainActivity) getActivity()); + if (downloadObserver != null) { + downloadObserver.setActivity(getActivity()); + downloadObserver.onResume(); + } + if (viewsCreated && itemsLoaded) { + onFragmentLoaded(); + } + } + + @Override + public void onStop() { + super.onStop(); + EventDistributor.getInstance().unregister(contentUpdate); + stopItemLoader(); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + this.activity.set((MainActivity) getActivity()); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + resetViewState(); + } + + private void resetViewState() { + listAdapter = null; + activity.set(null); + viewsCreated = false; + if (downloadObserver != null) { + downloadObserver.onPause(); + } + if (feedItemDialog != null) { + feedItemDialogSavedInstance = feedItemDialog.save(); + } + feedItemDialog = null; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { + inflater.inflate(R.menu.new_episodes, menu); + + final SearchView sv = new SearchView(getActivity()); + MenuItemUtils.addSearchItem(menu, sv); + sv.setQueryHint(getString(R.string.search_hint)); + sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String s) { + sv.clearFocus(); + ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance(s)); + return true; + } + + @Override + public boolean onQueryTextChange(String s) { + return false; + } + }); + } + } + + @Override + public void onPrepareOptionsMenu(Menu menu) { + super.onPrepareOptionsMenu(menu); + if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { + menu.findItem(R.id.mark_all_read_item).setVisible(unreadItems != null && !unreadItems.isEmpty()); + menu.findItem(R.id.episode_filter_item).setChecked(showOnlyNewEpisodes); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (!super.onOptionsItemSelected(item)) { + switch (item.getItemId()) { + case R.id.refresh_item: + List<Feed> feeds = ((MainActivity) getActivity()).getFeeds(); + if (feeds != null) { + DBTasks.refreshAllFeeds(getActivity(), feeds); + } + return true; + case R.id.mark_all_read_item: + DBWriter.markAllItemsRead(getActivity()); + Toast.makeText(getActivity(), R.string.mark_all_read_msg, Toast.LENGTH_SHORT).show(); + return true; + case R.id.episode_filter_item: + boolean newVal = !item.isChecked(); + setShowOnlyNewEpisodes(newVal); + item.setChecked(newVal); + return true; + default: + return false; + } + } else { + return true; + } + + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + ((MainActivity) getActivity()).getSupportActionBar().setTitle(R.string.all_episodes_label); + + View root = inflater.inflate(R.layout.new_episodes_fragment, container, false); + + listView = (DragSortListView) root.findViewById(android.R.id.list); + txtvEmpty = (TextView) root.findViewById(android.R.id.empty); + progLoading = (ProgressBar) root.findViewById(R.id.progLoading); + + listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + FeedItem item = (FeedItem) listAdapter.getItem(position - listView.getHeaderViewsCount()); + if (item != null) { + feedItemDialog = FeedItemDialog.newInstance(activity.get(), item, queueAccess); + feedItemDialog.show(); + } + + } + }); + + final int secondColor = (UserPreferences.getTheme() == R.style.Theme_AntennaPod_Dark) ? R.color.swipe_refresh_secondary_color_dark : R.color.swipe_refresh_secondary_color_light; + + if (!itemsLoaded) { + progLoading.setVisibility(View.VISIBLE); + txtvEmpty.setVisibility(View.GONE); + } + + viewsCreated = true; + + if (itemsLoaded && activity.get() != null) { + onFragmentLoaded(); + } + + return root; + } + + private void onFragmentLoaded() { + if (listAdapter == null) { + listAdapter = new NewEpisodesListAdapter(activity.get(), itemAccess, new DefaultActionButtonCallback(activity.get())); + listView.setAdapter(listAdapter); + listView.setEmptyView(txtvEmpty); + downloadObserver = new DownloadObserver(activity.get(), new Handler(), downloadObserverCallback); + downloadObserver.onResume(); + } + if (feedItemDialog != null) { + feedItemDialog.updateContent(queueAccess, unreadItems, recentItems); + } else if (feedItemDialogSavedInstance != null) { + feedItemDialog = FeedItemDialog.newInstance(activity.get(), feedItemDialogSavedInstance); + } + listAdapter.notifyDataSetChanged(); + getActivity().supportInvalidateOptionsMenu(); + updateProgressBarVisibility(); + updateShowOnlyEpisodesListViewState(); + } + + private DownloadObserver.Callback downloadObserverCallback = new DownloadObserver.Callback() { + @Override + public void onContentChanged() { + if (listAdapter != null) { + listAdapter.notifyDataSetChanged(); + } + if (feedItemDialog != null && feedItemDialog.isShowing()) { + feedItemDialog.updateMenuAppearance(); + } + } + + @Override + public void onDownloadDataAvailable(List<Downloader> downloaderList) { + NewEpisodesFragment.this.downloaderList = downloaderList; + if (listAdapter != null) { + listAdapter.notifyDataSetChanged(); + } + } + }; + + private NewEpisodesListAdapter.ItemAccess itemAccess = new NewEpisodesListAdapter.ItemAccess() { + + @Override + public int getCount() { + if (itemsLoaded) { + return (showOnlyNewEpisodes) ? unreadItems.size() : recentItems.size(); + } + return 0; + } + + @Override + public FeedItem getItem(int position) { + if (itemsLoaded) { + return (showOnlyNewEpisodes) ? unreadItems.get(position) : recentItems.get(position); + } + return null; + } + + @Override + public int getItemDownloadProgressPercent(FeedItem item) { + if (downloaderList != null) { + for (Downloader downloader : downloaderList) { + if (downloader.getDownloadRequest().getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA + && downloader.getDownloadRequest().getFeedfileId() == item.getMedia().getId()) { + return downloader.getDownloadRequest().getProgressPercent(); + } + } + } + return 0; + } + + @Override + public boolean isInQueue(FeedItem item) { + if (itemsLoaded) { + return queueAccess.contains(item.getId()); + } else { + return false; + } + } + + + }; + + private void updateProgressBarVisibility() { + if (!viewsCreated) { + return; + } + ((ActionBarActivity) getActivity()) + .setSupportProgressBarIndeterminateVisibility(DownloadService.isRunning + && DownloadRequester.getInstance().isDownloadingFeeds()); + } + + private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() { + @Override + public void update(EventDistributor eventDistributor, Integer arg) { + if ((arg & EVENTS) != 0) { + startItemLoader(); + updateProgressBarVisibility(); + } + } + }; + + private void updateShowOnlyEpisodes() { + SharedPreferences prefs = getActivity().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + showOnlyNewEpisodes = prefs.getBoolean(PREF_EPISODE_FILTER_BOOL, false); + } + + private void setShowOnlyNewEpisodes(boolean newVal) { + showOnlyNewEpisodes = newVal; + SharedPreferences prefs = getActivity().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(PREF_EPISODE_FILTER_BOOL, showOnlyNewEpisodes); + editor.commit(); + if (itemsLoaded && viewsCreated) { + listAdapter.notifyDataSetChanged(); + activity.get().supportInvalidateOptionsMenu(); + updateShowOnlyEpisodesListViewState(); + } + } + + private void updateShowOnlyEpisodesListViewState() { + if (showOnlyNewEpisodes) { + listView.setEmptyView(null); + txtvEmpty.setVisibility(View.GONE); + } else { + listView.setEmptyView(txtvEmpty); + } + } + + private ItemLoader itemLoader; + + private void startItemLoader() { + if (itemLoader != null) { + itemLoader.cancel(true); + } + itemLoader = new ItemLoader(); + itemLoader.execute(); + } + + private void stopItemLoader() { + if (itemLoader != null) { + itemLoader.cancel(true); + } + } + + private class ItemLoader extends AsyncTask<Void, Void, Object[]> { + + @Override + protected void onPreExecute() { + super.onPreExecute(); + if (viewsCreated && !itemsLoaded) { + listView.setVisibility(View.GONE); + txtvEmpty.setVisibility(View.GONE); + progLoading.setVisibility(View.VISIBLE); + } + } + + @Override + protected Object[] doInBackground(Void... params) { + Context context = activity.get(); + if (context != null) { + return new Object[]{DBReader.getUnreadItemsList(context), + DBReader.getRecentlyPublishedEpisodes(context, RECENT_EPISODES_LIMIT), + QueueAccess.IDListAccess(DBReader.getQueueIDList(context))}; + } else { + return null; + } + } + + @Override + protected void onPostExecute(Object[] lists) { + super.onPostExecute(lists); + listView.setVisibility(View.VISIBLE); + progLoading.setVisibility(View.GONE); + + if (lists != null) { + unreadItems = (List<FeedItem>) lists[0]; + recentItems = (List<FeedItem>) lists[1]; + queueAccess = (QueueAccess) lists[2]; + itemsLoaded = true; + if (viewsCreated && activity.get() != null) { + onFragmentLoaded(); + } + } + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java new file mode 100644 index 000000000..4a07ce2b7 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java @@ -0,0 +1,288 @@ +package de.danoeh.antennapod.fragment; + +import android.app.Activity; +import android.content.Context; +import android.content.res.TypedArray; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Handler; +import android.support.v4.app.ListFragment; +import android.support.v4.view.MenuItemCompat; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.ListView; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.adapter.DefaultActionButtonCallback; +import de.danoeh.antennapod.adapter.FeedItemlistAdapter; +import de.danoeh.antennapod.core.asynctask.DownloadObserver; +import de.danoeh.antennapod.dialog.FeedItemDialog; +import de.danoeh.antennapod.core.feed.EventDistributor; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.service.download.Downloader; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.util.QueueAccess; +import de.danoeh.antennapod.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.menuhandler.NavDrawerActivity; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +public class PlaybackHistoryFragment extends ListFragment { + private static final String TAG = "PlaybackHistoryFragment"; + + private List<FeedItem> playbackHistory; + private QueueAccess queue; + private FeedItemlistAdapter adapter; + + private boolean itemsLoaded = false; + private boolean viewsCreated = false; + + private AtomicReference<Activity> activity = new AtomicReference<Activity>(); + + private DownloadObserver downloadObserver; + private List<Downloader> downloaderList; + + private FeedItemDialog feedItemDialog; + private FeedItemDialog.FeedItemDialogSavedInstance feedItemDialogSavedInstance; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setRetainInstance(true); + setHasOptionsMenu(true); + } + + @Override + public void onResume() { + super.onResume(); + startItemLoader(); + } + + @Override + public void onStart() { + super.onStart(); + EventDistributor.getInstance().register(contentUpdate); + } + + @Override + public void onStop() { + super.onStop(); + EventDistributor.getInstance().unregister(contentUpdate); + stopItemLoader(); + } + + @Override + public void onDetach() { + super.onDetach(); + stopItemLoader(); + activity.set(null); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + this.activity.set(activity); + if (downloadObserver != null) { + downloadObserver.setActivity(activity); + downloadObserver.onResume(); + } + if (viewsCreated && itemsLoaded) { + onFragmentLoaded(); + } + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + adapter = null; + viewsCreated = false; + if (downloadObserver != null) { + downloadObserver.onPause(); + } + if (feedItemDialog != null) { + feedItemDialogSavedInstance = feedItemDialog.save(); + } + feedItemDialog = null; + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + viewsCreated = true; + if (itemsLoaded) { + onFragmentLoaded(); + } + } + + @Override + public void onListItemClick(ListView l, View v, int position, long id) { + super.onListItemClick(l, v, position, id); + FeedItem item = adapter.getItem(position - l.getHeaderViewsCount()); + if (item != null) { + feedItemDialog = FeedItemDialog.newInstance(activity.get(), item, queue); + feedItemDialog.show(); + } + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { + MenuItem clearHistory = menu.add(Menu.NONE, R.id.clear_history_item, Menu.CATEGORY_CONTAINER, R.string.clear_history_label); + MenuItemCompat.setShowAsAction(clearHistory, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM); + TypedArray drawables = getActivity().obtainStyledAttributes(new int[]{R.attr.content_discard}); + clearHistory.setIcon(drawables.getDrawable(0)); + drawables.recycle(); + } + } + + @Override + public void onPrepareOptionsMenu(Menu menu) { + super.onPrepareOptionsMenu(menu); + if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { + menu.findItem(R.id.clear_history_item).setVisible(playbackHistory != null && !playbackHistory.isEmpty()); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (!super.onOptionsItemSelected(item)) { + switch(item.getItemId()) { + case R.id.clear_history_item: + DBWriter.clearPlaybackHistory(getActivity()); + return true; + default: + return false; + } + } else { + return true; + } + } + + private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() { + + @Override + public void update(EventDistributor eventDistributor, Integer arg) { + if ((arg & EventDistributor.PLAYBACK_HISTORY_UPDATE) != 0) { + startItemLoader(); + getActivity().supportInvalidateOptionsMenu(); + } + } + }; + + private void onFragmentLoaded() { + if (adapter == null) { + adapter = new FeedItemlistAdapter(getActivity(), itemAccess, new DefaultActionButtonCallback(activity.get()), true); + setListAdapter(adapter); + downloadObserver = new DownloadObserver(activity.get(), new Handler(), downloadObserverCallback); + downloadObserver.onResume(); + } + setListShown(true); + adapter.notifyDataSetChanged(); + if (feedItemDialog != null && feedItemDialog.isShowing()) { + feedItemDialog.updateContent(queue, playbackHistory); + } else if (feedItemDialogSavedInstance != null) { + feedItemDialog = FeedItemDialog.newInstance(activity.get(), feedItemDialogSavedInstance); + } + getActivity().supportInvalidateOptionsMenu(); + } + + private DownloadObserver.Callback downloadObserverCallback = new DownloadObserver.Callback() { + @Override + public void onContentChanged() { + if (adapter != null) { + adapter.notifyDataSetChanged(); + } + if (feedItemDialog != null && feedItemDialog.isShowing()) { + feedItemDialog.updateMenuAppearance(); + } + } + + @Override + public void onDownloadDataAvailable(List<Downloader> downloaderList) { + PlaybackHistoryFragment.this.downloaderList = downloaderList; + if (adapter != null) { + adapter.notifyDataSetChanged(); + } + } + }; + + private FeedItemlistAdapter.ItemAccess itemAccess = new FeedItemlistAdapter.ItemAccess() { + @Override + public boolean isInQueue(FeedItem item) { + return (queue != null) ? queue.contains(item.getId()) : false; + } + + @Override + public int getItemDownloadProgressPercent(FeedItem item) { + if (downloaderList != null) { + for (Downloader downloader : downloaderList) { + if (downloader.getDownloadRequest().getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA + && downloader.getDownloadRequest().getFeedfileId() == item.getMedia().getId()) { + return downloader.getDownloadRequest().getProgressPercent(); + } + } + } + return 0; + } + + @Override + public int getCount() { + return (playbackHistory != null) ? playbackHistory.size() : 0; + } + + @Override + public FeedItem getItem(int position) { + return (playbackHistory != null) ? playbackHistory.get(position) : null; + } + }; + + private ItemLoader itemLoader; + + private void startItemLoader() { + if (itemLoader != null) { + itemLoader.cancel(true); + } + itemLoader = new ItemLoader(); + itemLoader.execute(); + } + + private void stopItemLoader() { + if (itemLoader != null) { + itemLoader.cancel(true); + } + } + + private class ItemLoader extends AsyncTask<Void, Void, Object[]> { + + @Override + protected Object[] doInBackground(Void... params) { + Context context = activity.get(); + if (context != null) { + List<FeedItem> ph = DBReader.getPlaybackHistory(context); + DBReader.loadFeedDataOfFeedItemlist(context, ph); + return new Object[]{ph, + QueueAccess.IDListAccess(DBReader.getQueueIDList(context))}; + } else { + return null; + } + } + + @Override + protected void onPostExecute(Object[] res) { + super.onPostExecute(res); + if (res != null) { + playbackHistory = (List<FeedItem>) res[0]; + queue = (QueueAccess) res[1]; + itemsLoaded = true; + if (viewsCreated) { + onFragmentLoaded(); + } + } + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java new file mode 100644 index 000000000..3192a84de --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java @@ -0,0 +1,383 @@ +package de.danoeh.antennapod.fragment; + +import android.app.Activity; +import android.content.Context; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Handler; +import android.support.v4.app.Fragment; +import android.support.v7.widget.SearchView; +import android.util.Log; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import com.mobeta.android.dslv.DragSortListView; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.adapter.DefaultActionButtonCallback; +import de.danoeh.antennapod.adapter.QueueListAdapter; +import de.danoeh.antennapod.core.asynctask.DownloadObserver; +import de.danoeh.antennapod.dialog.FeedItemDialog; +import de.danoeh.antennapod.core.feed.EventDistributor; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.service.download.Downloader; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.util.QueueAccess; +import de.danoeh.antennapod.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.menuhandler.NavDrawerActivity; + +/** + * Shows all items in the queue + */ +public class QueueFragment extends Fragment { + private static final String TAG = "QueueFragment"; + private static final int EVENTS = EventDistributor.DOWNLOAD_HANDLED | + EventDistributor.DOWNLOAD_QUEUED | + EventDistributor.QUEUE_UPDATE; + + private DragSortListView listView; + private QueueListAdapter listAdapter; + private TextView txtvEmpty; + private ProgressBar progLoading; + + private List<FeedItem> queue; + private List<Downloader> downloaderList; + + private boolean itemsLoaded = false; + private boolean viewsCreated = false; + + private AtomicReference<Activity> activity = new AtomicReference<Activity>(); + + private DownloadObserver downloadObserver = null; + + private FeedItemDialog feedItemDialog; + private FeedItemDialog.FeedItemDialogSavedInstance feedItemDialogSavedInstance; + + /** + * Download observer updates won't result in an upate of the list adapter if this is true. + */ + private boolean blockDownloadObserverUpdate = false; + + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setRetainInstance(true); + setHasOptionsMenu(true); + } + + @Override + public void onResume() { + super.onResume(); + startItemLoader(); + } + + @Override + public void onStart() { + super.onStart(); + EventDistributor.getInstance().register(contentUpdate); + this.activity.set((MainActivity) getActivity()); + if (downloadObserver != null) { + downloadObserver.setActivity(getActivity()); + downloadObserver.onResume(); + } + if (viewsCreated && itemsLoaded) { + onFragmentLoaded(); + } + } + + @Override + public void onStop() { + super.onStop(); + EventDistributor.getInstance().unregister(contentUpdate); + stopItemLoader(); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + this.activity.set((MainActivity) activity); + } + + private void resetViewState() { + unregisterForContextMenu(listView); + listAdapter = null; + activity.set(null); + viewsCreated = false; + blockDownloadObserverUpdate = false; + if (downloadObserver != null) { + downloadObserver.onPause(); + } + if (feedItemDialog != null) { + feedItemDialogSavedInstance = feedItemDialog.save(); + } + feedItemDialog = null; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + resetViewState(); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { + final SearchView sv = new SearchView(getActivity()); + MenuItemUtils.addSearchItem(menu, sv); + sv.setQueryHint(getString(R.string.search_hint)); + sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String s) { + sv.clearFocus(); + ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance(s)); + return true; + } + + @Override + public boolean onQueryTextChange(String s) { + return false; + } + }); + } + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + AdapterView.AdapterContextMenuInfo adapterInfo = (AdapterView.AdapterContextMenuInfo) menuInfo; + FeedItem item = itemAccess.getItem(adapterInfo.position); + + MenuInflater inflater = getActivity().getMenuInflater(); + inflater.inflate(R.menu.queue_context, menu); + + if (item != null) { + menu.setHeaderTitle(item.getTitle()); + } + + menu.findItem(R.id.move_to_top_item).setEnabled(!queue.isEmpty() && queue.get(0) != item); + menu.findItem(R.id.move_to_bottom_item).setEnabled(!queue.isEmpty() && queue.get(queue.size() - 1) != item); + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + AdapterView.AdapterContextMenuInfo menuInfo = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); + FeedItem selectedItem = itemAccess.getItem(menuInfo.position); + + if (selectedItem == null) { + Log.i(TAG, "Selected item at position " + menuInfo.position + " was null, ignoring selection"); + return super.onContextItemSelected(item); + } + + switch (item.getItemId()) { + case R.id.move_to_top_item: + DBWriter.moveQueueItemToTop(getActivity(), selectedItem.getId(), true); + return true; + case R.id.move_to_bottom_item: + DBWriter.moveQueueItemToBottom(getActivity(), selectedItem.getId(), true); + return true; + case R.id.remove_from_queue_item: + DBWriter.removeQueueItem(getActivity(), selectedItem.getId(), false); + return true; + default: + return super.onContextItemSelected(item); + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + ((MainActivity) getActivity()).getSupportActionBar().setTitle(R.string.queue_label); + + View root = inflater.inflate(R.layout.queue_fragment, container, false); + listView = (DragSortListView) root.findViewById(android.R.id.list); + txtvEmpty = (TextView) root.findViewById(android.R.id.empty); + progLoading = (ProgressBar) root.findViewById(R.id.progLoading); + listView.setEmptyView(txtvEmpty); + + listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + FeedItem item = (FeedItem) listAdapter.getItem(position - listView.getHeaderViewsCount()); + if (item != null) { + feedItemDialog = FeedItemDialog.newInstance(activity.get(), item, QueueAccess.ItemListAccess(queue)); + feedItemDialog.show(); + } + } + }); + + listView.setDragSortListener(new DragSortListView.DragSortListener() { + @Override + public void drag(int from, int to) { + Log.d(TAG, "drag"); + blockDownloadObserverUpdate = true; + } + + @Override + public void drop(int from, int to) { + Log.d(TAG, "drop"); + blockDownloadObserverUpdate = false; + stopItemLoader(); + final FeedItem item = queue.remove(from); + queue.add(to, item); + listAdapter.notifyDataSetChanged(); + DBWriter.moveQueueItem(getActivity(), from, to, true); + } + + @Override + public void remove(int which) { + } + }); + + registerForContextMenu(listView); + + if (!itemsLoaded) { + progLoading.setVisibility(View.VISIBLE); + txtvEmpty.setVisibility(View.GONE); + } + + viewsCreated = true; + + if (itemsLoaded && activity.get() != null) { + onFragmentLoaded(); + } + + return root; + } + + private void onFragmentLoaded() { + if (listAdapter == null) { + listAdapter = new QueueListAdapter(activity.get(), itemAccess, new DefaultActionButtonCallback(activity.get())); + listView.setAdapter(listAdapter); + downloadObserver = new DownloadObserver(activity.get(), new Handler(), downloadObserverCallback); + downloadObserver.onResume(); + } + listAdapter.notifyDataSetChanged(); + if (feedItemDialog != null) { + feedItemDialog.updateContent(QueueAccess.ItemListAccess(queue), queue); + } else if (feedItemDialogSavedInstance != null) { + feedItemDialog = FeedItemDialog.newInstance(activity.get(), feedItemDialogSavedInstance); + } + } + + private DownloadObserver.Callback downloadObserverCallback = new DownloadObserver.Callback() { + @Override + public void onContentChanged() { + if (listAdapter != null && !blockDownloadObserverUpdate) { + listAdapter.notifyDataSetChanged(); + } + if (feedItemDialog != null && feedItemDialog.isShowing()) { + feedItemDialog.updateMenuAppearance(); + } + } + + @Override + public void onDownloadDataAvailable(List<Downloader> downloaderList) { + QueueFragment.this.downloaderList = downloaderList; + if (listAdapter != null && !blockDownloadObserverUpdate) { + listAdapter.notifyDataSetChanged(); + } + } + }; + + private QueueListAdapter.ItemAccess itemAccess = new QueueListAdapter.ItemAccess() { + @Override + public int getCount() { + return (itemsLoaded) ? queue.size() : 0; + } + + @Override + public FeedItem getItem(int position) { + return (itemsLoaded) ? queue.get(position) : null; + } + + @Override + public int getItemDownloadProgressPercent(FeedItem item) { + if (downloaderList != null) { + for (Downloader downloader : downloaderList) { + if (downloader.getDownloadRequest().getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA + && downloader.getDownloadRequest().getFeedfileId() == item.getMedia().getId()) { + return downloader.getDownloadRequest().getProgressPercent(); + } + } + } + return 0; + } + }; + + private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() { + @Override + public void update(EventDistributor eventDistributor, Integer arg) { + if ((arg & EVENTS) != 0) { + startItemLoader(); + } + } + }; + + private ItemLoader itemLoader; + + private void startItemLoader() { + if (itemLoader != null) { + itemLoader.cancel(true); + } + itemLoader = new ItemLoader(); + itemLoader.execute(); + } + + private void stopItemLoader() { + if (itemLoader != null) { + itemLoader.cancel(true); + } + } + + private class ItemLoader extends AsyncTask<Void, Void, List<FeedItem>> { + @Override + protected void onPreExecute() { + super.onPreExecute(); + if (viewsCreated && !itemsLoaded) { + listView.setVisibility(View.GONE); + txtvEmpty.setVisibility(View.GONE); + progLoading.setVisibility(View.VISIBLE); + } + } + + @Override + protected void onPostExecute(List<FeedItem> feedItems) { + super.onPostExecute(feedItems); + listView.setVisibility(View.VISIBLE); + progLoading.setVisibility(View.GONE); + + if (feedItems != null) { + queue = feedItems; + itemsLoaded = true; + if (viewsCreated && activity.get() != null) { + onFragmentLoaded(); + } + } + } + + @Override + protected List<FeedItem> doInBackground(Void... params) { + Context context = activity.get(); + if (context != null) { + return DBReader.getQueue(context); + } + return null; + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java new file mode 100644 index 000000000..514b05efd --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java @@ -0,0 +1,69 @@ +package de.danoeh.antennapod.fragment; + +import android.os.Bundle; +import android.os.Handler; +import android.support.v4.app.ListFragment; +import android.view.View; +import de.danoeh.antennapod.adapter.DownloadlistAdapter; +import de.danoeh.antennapod.core.asynctask.DownloadObserver; +import de.danoeh.antennapod.core.service.download.Downloader; +import de.danoeh.antennapod.core.storage.DownloadRequester; + +import java.util.List; + +/** + * Displays all running downloads and provides actions to cancel them + */ +public class RunningDownloadsFragment extends ListFragment { + private static final String TAG = "RunningDownloadsFragment"; + + private DownloadObserver downloadObserver; + private List<Downloader> downloaderList; + + + @Override + public void onDetach() { + super.onDetach(); + if (downloadObserver != null) { + downloadObserver.onPause(); + } + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + final DownloadlistAdapter downloadlistAdapter = new DownloadlistAdapter(getActivity(), itemAccess); + setListAdapter(downloadlistAdapter); + + downloadObserver = new DownloadObserver(getActivity(), new Handler(), new DownloadObserver.Callback() { + @Override + public void onContentChanged() { + downloadlistAdapter.notifyDataSetChanged(); + } + + @Override + public void onDownloadDataAvailable(List<Downloader> downloaderList) { + RunningDownloadsFragment.this.downloaderList = downloaderList; + downloadlistAdapter.notifyDataSetChanged(); + } + }); + downloadObserver.onResume(); + } + + private DownloadlistAdapter.ItemAccess itemAccess = new DownloadlistAdapter.ItemAccess() { + @Override + public int getCount() { + return (downloaderList != null) ? downloaderList.size() : 0; + } + + @Override + public Downloader getItem(int position) { + return (downloaderList != null) ? downloaderList.get(position) : null; + } + + @Override + public void onSecondaryActionClick(Downloader downloader) { + DownloadRequester.getInstance().cancelDownload(getActivity(), downloader.getDownloadRequest().getSource()); + } + }; +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java new file mode 100644 index 000000000..7419b42ab --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java @@ -0,0 +1,258 @@ +package de.danoeh.antennapod.fragment; + +import android.content.Context; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.v4.app.ListFragment; +import android.support.v4.view.MenuItemCompat; +import android.support.v7.app.ActionBarActivity; +import android.support.v7.widget.SearchView; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.ListView; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.adapter.SearchlistAdapter; +import de.danoeh.antennapod.dialog.FeedItemDialog; +import de.danoeh.antennapod.core.feed.*; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.FeedSearcher; +import de.danoeh.antennapod.core.util.QueueAccess; +import de.danoeh.antennapod.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.menuhandler.NavDrawerActivity; + +import java.util.List; + +/** + * Performs a search operation on all feeds or one specific feed and displays the search result. + */ +public class SearchFragment extends ListFragment { + private static final String TAG = "SearchFragment"; + + private static final String ARG_QUERY = "query"; + private static final String ARG_FEED = "feed"; + + private SearchlistAdapter searchAdapter; + private List<SearchResult> searchResults; + + private boolean viewCreated = false; + private boolean itemsLoaded = false; + + private QueueAccess queue; + + private FeedItemDialog feedItemDialog; + private FeedItemDialog.FeedItemDialogSavedInstance feedItemDialogSavedInstance; + + /** + * Create a new SearchFragment that searches all feeds. + */ + public static SearchFragment newInstance(String query) { + if (query == null) query = ""; + SearchFragment fragment = new SearchFragment(); + Bundle args = new Bundle(); + args.putString(ARG_QUERY, query); + args.putLong(ARG_FEED, 0); + fragment.setArguments(args); + return fragment; + } + + /** + * Create a new SearchFragment that searches one specific feed. + */ + public static SearchFragment newInstance(String query, long feed) { + SearchFragment fragment = newInstance(query); + fragment.getArguments().putLong(ARG_FEED, feed); + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setRetainInstance(true); + setHasOptionsMenu(true); + startSearchTask(); + } + + @Override + public void onStart() { + super.onStart(); + EventDistributor.getInstance().register(contentUpdate); + } + + @Override + public void onStop() { + super.onStop(); + stopSearchTask(); + EventDistributor.getInstance().unregister(contentUpdate); + } + + @Override + public void onDetach() { + super.onDetach(); + stopSearchTask(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + searchAdapter = null; + viewCreated = false; + if (feedItemDialog != null) { + feedItemDialogSavedInstance = feedItemDialog.save(); + } + feedItemDialog = null; + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + ((ActionBarActivity) getActivity()).getSupportActionBar().setTitle(R.string.search_label); + viewCreated = true; + if (itemsLoaded) { + onFragmentLoaded(); + } + } + + @Override + public void onListItemClick(ListView l, View v, int position, long id) { + super.onListItemClick(l, v, position, id); + SearchResult result = (SearchResult) l.getAdapter().getItem(position); + FeedComponent comp = result.getComponent(); + if (comp.getClass() == Feed.class) { + ((MainActivity)getActivity()).loadFeedFragment(comp.getId()); + } else { + if (comp.getClass() == FeedItem.class) { + feedItemDialog = FeedItemDialog.newInstance(getActivity(), (FeedItem) comp, queue); + feedItemDialog.show(); + } + } + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { + MenuItem item = menu.add(Menu.NONE, R.id.search_item, Menu.NONE, R.string.search_label); + MenuItemCompat.setShowAsAction(item, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM); + final SearchView sv = new SearchView(getActivity()); + sv.setQueryHint(getString(R.string.search_hint)); + sv.setQuery(getArguments().getString(ARG_QUERY), false); + sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String s) { + getArguments().putString(ARG_QUERY, s); + itemsLoaded = false; + startSearchTask(); + return true; + } + + @Override + public boolean onQueryTextChange(String s) { + return false; + } + }); + MenuItemCompat.setActionView(item, sv); + } + } + + private final EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() { + @Override + public void update(EventDistributor eventDistributor, Integer arg) { + if ((arg & (EventDistributor.DOWNLOAD_QUEUED)) != 0 && feedItemDialog != null) { + feedItemDialog.updateMenuAppearance(); + } + if ((arg & (EventDistributor.UNREAD_ITEMS_UPDATE + | EventDistributor.DOWNLOAD_HANDLED + | EventDistributor.QUEUE_UPDATE)) != 0) { + startSearchTask(); + } + } + }; + + private void onFragmentLoaded() { + if (searchAdapter == null) { + searchAdapter = new SearchlistAdapter(getActivity(), itemAccess); + setListAdapter(searchAdapter); + } + searchAdapter.notifyDataSetChanged(); + setListShown(true); + if (feedItemDialog != null && feedItemDialog.isShowing()) { + feedItemDialog.setQueue(queue); + for (SearchResult result : searchResults) { + FeedComponent comp = result.getComponent(); + if (comp.getClass() == FeedItem.class && ((FeedItem) comp).getId() == feedItemDialog.getItem().getId()) { + feedItemDialog.setItem((FeedItem) comp); + } + } + feedItemDialog.updateMenuAppearance(); + } else if (feedItemDialogSavedInstance != null) { + feedItemDialog = FeedItemDialog.newInstance(getActivity(), feedItemDialogSavedInstance); + } + } + + private final SearchlistAdapter.ItemAccess itemAccess = new SearchlistAdapter.ItemAccess() { + @Override + public int getCount() { + return (searchResults != null) ? searchResults.size() : 0; + } + + @Override + public SearchResult getItem(int position) { + return (searchResults != null) ? searchResults.get(position) : null; + } + }; + + private SearchTask searchTask; + + private void startSearchTask() { + if (searchTask != null) { + searchTask.cancel(true); + } + searchTask = new SearchTask(); + searchTask.execute(getArguments()); + } + + private void stopSearchTask() { + if (searchTask != null) { + searchTask.cancel(true); + } + } + + private class SearchTask extends AsyncTask<Bundle, Void, Object[]> { + @Override + protected Object[] doInBackground(Bundle... params) { + String query = params[0].getString(ARG_QUERY); + long feed = params[0].getLong(ARG_FEED); + Context context = getActivity(); + if (context != null) { + return new Object[]{FeedSearcher.performSearch(context, query, feed), + QueueAccess.IDListAccess(DBReader.getQueueIDList(context))}; + } else { + return null; + } + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + if (viewCreated && !itemsLoaded) { + setListShown(false); + } + } + + @Override + protected void onPostExecute(Object[] results) { + super.onPostExecute(results); + if (results != null) { + itemsLoaded = true; + searchResults = (List<SearchResult>) results[0]; + queue = (QueueAccess) results[1]; + if (viewCreated) { + onFragmentLoaded(); + } + } + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/GpodnetMainFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/GpodnetMainFragment.java new file mode 100644 index 000000000..ec8f69368 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/GpodnetMainFragment.java @@ -0,0 +1,131 @@ +package de.danoeh.antennapod.fragment.gpodnet; + +import android.app.Activity; +import android.content.res.Resources; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentPagerAdapter; +import android.support.v4.app.FragmentTransaction; +import android.support.v4.view.ViewPager; +import android.support.v7.app.ActionBar; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; + +/** + * Main navigation hub for gpodder.net podcast directory + */ +public class GpodnetMainFragment extends Fragment { + + private ViewPager pager; + private MainActivity activity; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + View root = inflater.inflate(R.layout.pager_fragment, container, false); + pager = (ViewPager) root.findViewById(R.id.pager); + GpodnetPagerAdapter pagerAdapter = new GpodnetPagerAdapter(getChildFragmentManager(), getResources()); + pager.setAdapter(pagerAdapter); + final ActionBar actionBar = activity.getMainActivtyActionBar(); + actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); + ActionBar.TabListener tabListener = new ActionBar.TabListener() { + @Override + public void onTabSelected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) { + pager.setCurrentItem(tab.getPosition()); + } + + @Override + public void onTabUnselected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) { + + } + + @Override + public void onTabReselected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) { + + } + }; + actionBar.removeAllTabs(); + actionBar.addTab(actionBar.newTab() + .setText(R.string.gpodnet_taglist_header) + .setTabListener(tabListener)); + actionBar.addTab(actionBar.newTab() + .setText(R.string.gpodnet_toplist_header) + .setTabListener(tabListener)); + actionBar.setTitle(R.string.gpodnet_main_label); + + pager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { + @Override + public void onPageSelected(int position) { + super.onPageSelected(position); + actionBar.setSelectedNavigationItem(position); + } + }); + return root; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + activity.getMainActivtyActionBar().removeAllTabs(); + activity.getMainActivtyActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + this.activity = (MainActivity) activity; + } + + public class GpodnetPagerAdapter extends FragmentPagerAdapter { + + + private static final int NUM_PAGES = 2; + private static final int POS_TAGS = 0; + private static final int POS_TOPLIST = 1; + private static final int POS_SUGGESTIONS = 2; + + Resources resources; + + public GpodnetPagerAdapter(FragmentManager fm, Resources resources) { + super(fm); + this.resources = resources; + } + + @Override + public Fragment getItem(int i) { + switch (i) { + case POS_TAGS: + return new TagListFragment(); + case POS_TOPLIST: + return new PodcastTopListFragment(); + case POS_SUGGESTIONS: + return new SuggestionListFragment(); + default: + return null; + } + } + + @Override + public CharSequence getPageTitle(int position) { + switch (position) { + case POS_TAGS: + return getString(R.string.gpodnet_taglist_header); + case POS_TOPLIST: + return getString(R.string.gpodnet_toplist_header); + case POS_SUGGESTIONS: + return getString(R.string.gpodnet_suggestions_header); + default: + return super.getPageTitle(position); + } + } + + @Override + public int getCount() { + return NUM_PAGES; + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java new file mode 100644 index 000000000..15a0b55b1 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java @@ -0,0 +1,167 @@ +package de.danoeh.antennapod.fragment.gpodnet; + +import android.content.Context; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.util.Log; +import android.view.*; +import android.widget.*; + +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.DefaultOnlineFeedViewActivity; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.activity.OnlineFeedViewActivity; +import de.danoeh.antennapod.adapter.gpodnet.PodcastListAdapter; +import de.danoeh.antennapod.core.gpoddernet.GpodnetService; +import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast; +import de.danoeh.antennapod.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.menuhandler.NavDrawerActivity; + +import java.util.List; + +/** + * Displays a list of GPodnetPodcast-Objects in a GridView + */ +public abstract class PodcastListFragment extends Fragment { + private static final String TAG = "PodcastListFragment"; + + private GridView gridView; + private ProgressBar progressBar; + private TextView txtvError; + private Button butRetry; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + if (!MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { + final android.support.v7.widget.SearchView sv = new android.support.v7.widget.SearchView(getActivity()); + MenuItemUtils.addSearchItem(menu, sv); + sv.setQueryHint(getString(R.string.gpodnet_search_hint)); + sv.setOnQueryTextListener(new android.support.v7.widget.SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String s) { + sv.clearFocus(); + ((MainActivity) getActivity()).loadChildFragment(SearchListFragment.newInstance(s)); + return true; + } + + @Override + public boolean onQueryTextChange(String s) { + return false; + } + }); + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View root = inflater.inflate(R.layout.gpodnet_podcast_list, container, false); + + gridView = (GridView) root.findViewById(R.id.gridView); + progressBar = (ProgressBar) root.findViewById(R.id.progressBar); + txtvError = (TextView) root.findViewById(R.id.txtvError); + butRetry = (Button) root.findViewById(R.id.butRetry); + + gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + onPodcastSelected((GpodnetPodcast) gridView.getAdapter().getItem(position)); + } + }); + butRetry.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + loadData(); + } + }); + + loadData(); + return root; + } + + protected void onPodcastSelected(GpodnetPodcast selection) { + if (BuildConfig.DEBUG) Log.d(TAG, "Selected podcast: " + selection.toString()); + Intent intent = new Intent(getActivity(), DefaultOnlineFeedViewActivity.class); + intent.putExtra(OnlineFeedViewActivity.ARG_FEEDURL, selection.getUrl()); + intent.putExtra(DefaultOnlineFeedViewActivity.ARG_TITLE, getString(R.string.gpodnet_main_label)); + startActivity(intent); + } + + protected abstract List<GpodnetPodcast> loadPodcastData(GpodnetService service) throws GpodnetServiceException; + + protected final void loadData() { + AsyncTask<Void, Void, List<GpodnetPodcast>> loaderTask = new AsyncTask<Void, Void, List<GpodnetPodcast>>() { + volatile Exception exception = null; + + @Override + protected List<GpodnetPodcast> doInBackground(Void... params) { + GpodnetService service = null; + try { + service = new GpodnetService(); + return loadPodcastData(service); + } catch (GpodnetServiceException e) { + exception = e; + e.printStackTrace(); + return null; + } finally { + if (service != null) { + service.shutdown(); + } + } + } + + @Override + protected void onPostExecute(List<GpodnetPodcast> gpodnetPodcasts) { + super.onPostExecute(gpodnetPodcasts); + final Context context = getActivity(); + if (context != null && gpodnetPodcasts != null && gpodnetPodcasts.size() > 0) { + PodcastListAdapter listAdapter = new PodcastListAdapter(context, 0, gpodnetPodcasts); + gridView.setAdapter(listAdapter); + listAdapter.notifyDataSetChanged(); + + progressBar.setVisibility(View.GONE); + gridView.setVisibility(View.VISIBLE); + txtvError.setVisibility(View.GONE); + butRetry.setVisibility(View.GONE); + } else if (context != null && gpodnetPodcasts != null) { + gridView.setVisibility(View.GONE); + progressBar.setVisibility(View.GONE); + txtvError.setText(getString(R.string.search_status_no_results)); + txtvError.setVisibility(View.VISIBLE); + butRetry.setVisibility(View.GONE); + } else if (context != null) { + gridView.setVisibility(View.GONE); + progressBar.setVisibility(View.GONE); + txtvError.setText(getString(R.string.error_msg_prefix) + exception.getMessage()); + txtvError.setVisibility(View.VISIBLE); + butRetry.setVisibility(View.VISIBLE); + } + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + gridView.setVisibility(View.GONE); + progressBar.setVisibility(View.VISIBLE); + txtvError.setVisibility(View.GONE); + butRetry.setVisibility(View.GONE); + } + }; + + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { + loaderTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + loaderTask.execute(); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastTopListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastTopListFragment.java new file mode 100644 index 000000000..33a35fa90 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastTopListFragment.java @@ -0,0 +1,20 @@ +package de.danoeh.antennapod.fragment.gpodnet; + +import de.danoeh.antennapod.core.gpoddernet.GpodnetService; +import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast; + +import java.util.List; + +/** + * + */ +public class PodcastTopListFragment extends PodcastListFragment { + private static final String TAG = "PodcastTopListFragment"; + private static final int PODCAST_COUNT = 50; + + @Override + protected List<GpodnetPodcast> loadPodcastData(GpodnetService service) throws GpodnetServiceException { + return service.getPodcastToplist(PODCAST_COUNT); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/SearchListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/SearchListFragment.java new file mode 100644 index 000000000..635842196 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/SearchListFragment.java @@ -0,0 +1,80 @@ +package de.danoeh.antennapod.fragment.gpodnet; + +import android.os.Bundle; +import android.support.v7.widget.SearchView; +import android.view.Menu; +import android.view.MenuInflater; + +import org.apache.commons.lang3.Validate; + +import java.util.List; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.gpoddernet.GpodnetService; +import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast; +import de.danoeh.antennapod.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.menuhandler.NavDrawerActivity; + +/** + * Performs a search on the gpodder.net directory and displays the results. + */ +public class SearchListFragment extends PodcastListFragment { + private static final String ARG_QUERY = "query"; + + private String query; + + public static SearchListFragment newInstance(String query) { + SearchListFragment fragment = new SearchListFragment(); + Bundle args = new Bundle(); + args.putString(ARG_QUERY, query); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (getArguments() != null && getArguments().containsKey(ARG_QUERY)) { + this.query = getArguments().getString(ARG_QUERY); + } else { + this.query = ""; + } + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + final SearchView sv = new SearchView(getActivity()); + if (!MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { + MenuItemUtils.addSearchItem(menu, sv); + sv.setQueryHint(getString(R.string.gpodnet_search_hint)); + sv.setQuery(query, false); + sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String s) { + sv.clearFocus(); + changeQuery(s); + return true; + } + + @Override + public boolean onQueryTextChange(String s) { + return false; + } + }); + } + } + + @Override + protected List<GpodnetPodcast> loadPodcastData(GpodnetService service) throws GpodnetServiceException { + return service.searchPodcasts(query, 0); + } + + public void changeQuery(String query) { + Validate.notNull(query); + + this.query = query; + loadData(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/SuggestionListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/SuggestionListFragment.java new file mode 100644 index 000000000..133bb0281 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/SuggestionListFragment.java @@ -0,0 +1,26 @@ +package de.danoeh.antennapod.fragment.gpodnet; + +import de.danoeh.antennapod.core.gpoddernet.GpodnetService; +import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast; +import de.danoeh.antennapod.core.preferences.GpodnetPreferences; + +import java.util.ArrayList; +import java.util.List; + +/** + * Displays suggestions from gpodder.net + */ +public class SuggestionListFragment extends PodcastListFragment { + private static final int SUGGESTIONS_COUNT = 50; + + @Override + protected List<GpodnetPodcast> loadPodcastData(GpodnetService service) throws GpodnetServiceException { + if (GpodnetPreferences.loggedIn()) { + service.authenticate(GpodnetPreferences.getUsername(), GpodnetPreferences.getPassword()); + return service.getSuggestions(SUGGESTIONS_COUNT); + } else { + return new ArrayList<GpodnetPodcast>(); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java new file mode 100644 index 000000000..7e02b647f --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java @@ -0,0 +1,50 @@ +package de.danoeh.antennapod.fragment.gpodnet; + +import android.os.Bundle; + +import org.apache.commons.lang3.Validate; + +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.core.gpoddernet.GpodnetService; +import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetTag; + +import java.util.List; + +/** + * Shows all podcasts from gpodder.net that belong to a specific tag. + * Use the newInstance method of this class to create a new TagFragment. + */ +public class TagFragment extends PodcastListFragment { + + private static final String TAG = "TagFragment"; + private static final int PODCAST_COUNT = 50; + + private GpodnetTag tag; + + public static TagFragment newInstance(String tagName) { + Validate.notNull(tagName); + TagFragment fragment = new TagFragment(); + Bundle args = new Bundle(); + args.putString("tag", tagName); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Bundle args = getArguments(); + Validate.isTrue(args != null && args.getString("tag") != null, "args invalid"); + + tag = new GpodnetTag(args.getString("tag")); + ((MainActivity) getActivity()).getMainActivtyActionBar().setTitle(tag.getName()); + } + + @Override + protected List<GpodnetPodcast> loadPodcastData(GpodnetService service) throws GpodnetServiceException { + return service.getPodcastsForTag(tag, PODCAST_COUNT); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java new file mode 100644 index 000000000..24e0e4caa --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java @@ -0,0 +1,146 @@ +package de.danoeh.antennapod.fragment.gpodnet; + +import android.app.Activity; +import android.content.Context; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.v4.app.ListFragment; +import android.support.v7.widget.SearchView; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.core.gpoddernet.GpodnetService; +import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetTag; +import de.danoeh.antennapod.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.menuhandler.NavDrawerActivity; + +public class TagListFragment extends ListFragment { + private static final String TAG = "TagListFragment"; + private static final int COUNT = 50; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + if (!MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { + final SearchView sv = new SearchView(getActivity()); + MenuItemUtils.addSearchItem(menu, sv); + sv.setQueryHint(getString(R.string.gpodnet_search_hint)); + sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String s) { + Activity activity = getActivity(); + if (activity != null) { + sv.clearFocus(); + ((MainActivity) activity).loadChildFragment(SearchListFragment.newInstance(s)); + } + return true; + } + + @Override + public boolean onQueryTextChange(String s) { + return false; + } + }); + } + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + String selectedTag = (String) getListAdapter().getItem(position); + MainActivity activity = (MainActivity) getActivity(); + activity.loadChildFragment(TagFragment.newInstance(selectedTag)); + } + }); + + startLoadTask(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + cancelLoadTask(); + } + + private AsyncTask<Void, Void, List<GpodnetTag>> loadTask; + + private void cancelLoadTask() { + if (loadTask != null && !loadTask.isCancelled()) { + loadTask.cancel(true); + } + } + + private void startLoadTask() { + cancelLoadTask(); + loadTask = new AsyncTask<Void, Void, List<GpodnetTag>>() { + private Exception exception; + + @Override + protected List<GpodnetTag> doInBackground(Void... params) { + GpodnetService service = new GpodnetService(); + try { + return service.getTopTags(COUNT); + } catch (GpodnetServiceException e) { + e.printStackTrace(); + exception = e; + return null; + } finally { + service.shutdown(); + } + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + setListShown(false); + } + + @Override + protected void onPostExecute(List<GpodnetTag> gpodnetTags) { + super.onPostExecute(gpodnetTags); + final Context context = getActivity(); + if (context != null) { + if (gpodnetTags != null) { + List<String> tagNames = new ArrayList<String>(); + for (GpodnetTag tag : gpodnetTags) { + tagNames.add(tag.getName()); + } + setListAdapter(new ArrayAdapter<String>(context, android.R.layout.simple_list_item_1, tagNames)); + } else if (exception != null) { + TextView txtvError = new TextView(getActivity()); + txtvError.setText(exception.getMessage()); + getListView().setEmptyView(txtvError); + } + setListShown(true); + + } + } + }; + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { + loadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + loadTask.execute(); + } + } +} + diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java new file mode 100644 index 000000000..8ccbdafc6 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java @@ -0,0 +1,191 @@ +package de.danoeh.antennapod.menuhandler; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.QueueAccess; +import de.danoeh.antennapod.core.util.ShareUtils; + +/** + * Handles interactions with the FeedItemMenu. + */ +public class FeedItemMenuHandler { + private static final String TAG = "FeedItemMenuHandler"; + + private FeedItemMenuHandler() { + + } + + /** + * Used by the MenuHandler to access different types of menus through one + * interface + */ + public interface MenuInterface { + /** + * Implementations of this method should call findItem(id) on their + * menu-object and call setVisibility(visibility) on the returned + * MenuItem object. + */ + abstract void setItemVisibility(int id, boolean visible); + } + + /** + * This method should be called in the prepare-methods of menus. It changes + * the visibility of the menu items depending on a FeedItem's attributes. + * + * @param mi An instance of MenuInterface that the method uses to change a + * MenuItem's visibility + * @param selectedItem The FeedItem for which the menu is supposed to be prepared + * @param showExtendedMenu True if MenuItems that let the user share information about + * the FeedItem and visit its website should be set visible. This + * parameter should be set to false if the menu space is limited. + * @param queueAccess Used for testing if the queue contains the selected item + * @return Returns true if selectedItem is not null. + */ + public static boolean onPrepareMenu(MenuInterface mi, + FeedItem selectedItem, boolean showExtendedMenu, QueueAccess queueAccess) { + if (selectedItem == null) { + return false; + } + DownloadRequester requester = DownloadRequester.getInstance(); + boolean hasMedia = selectedItem.getMedia() != null; + boolean downloaded = hasMedia && selectedItem.getMedia().isDownloaded(); + boolean downloading = hasMedia + && requester.isDownloadingFile(selectedItem.getMedia()); + boolean notLoadedAndNotLoading = hasMedia && (!downloaded) + && (!downloading); + boolean isPlaying = hasMedia + && selectedItem.getState() == FeedItem.State.PLAYING; + + FeedItem.State state = selectedItem.getState(); + + if (!isPlaying) { + mi.setItemVisibility(R.id.skip_episode_item, false); + } + if (!downloaded || isPlaying) { + mi.setItemVisibility(R.id.play_item, false); + mi.setItemVisibility(R.id.remove_item, false); + } + if (!notLoadedAndNotLoading) { + mi.setItemVisibility(R.id.download_item, false); + } + if (!(notLoadedAndNotLoading | downloading) | isPlaying) { + mi.setItemVisibility(R.id.stream_item, false); + } + if (!downloading) { + mi.setItemVisibility(R.id.cancel_download_item, false); + } + + boolean isInQueue = queueAccess.contains(selectedItem.getId()); + if (!isInQueue || isPlaying) { + mi.setItemVisibility(R.id.remove_from_queue_item, false); + } + if (!(!isInQueue && selectedItem.getMedia() != null)) { + mi.setItemVisibility(R.id.add_to_queue_item, false); + } + if (!showExtendedMenu || selectedItem.getLink() == null) { + mi.setItemVisibility(R.id.share_link_item, false); + } + + if (!BuildConfig.DEBUG + || !(state == FeedItem.State.IN_PROGRESS || state == FeedItem.State.READ)) { + mi.setItemVisibility(R.id.mark_unread_item, false); + } + if (!(state == FeedItem.State.NEW || state == FeedItem.State.IN_PROGRESS)) { + mi.setItemVisibility(R.id.mark_read_item, false); + } + + if (!showExtendedMenu || selectedItem.getLink() == null) { + mi.setItemVisibility(R.id.visit_website_item, false); + } + + if (selectedItem.getPaymentLink() == null || !selectedItem.getFlattrStatus().flattrable()) { + mi.setItemVisibility(R.id.support_item, false); + } + return true; + } + + /** + * The same method as onPrepareMenu(MenuInterface, FeedItem, boolean, QueueAccess), but lets the + * caller also specify a list of menu items that should not be shown. + * + * @param excludeIds Menu item that should be excluded + * @return true if selectedItem is not null. + */ + public static boolean onPrepareMenu(MenuInterface mi, + FeedItem selectedItem, boolean showExtendedMenu, QueueAccess queueAccess, int... excludeIds) { + boolean rc = onPrepareMenu(mi, selectedItem, showExtendedMenu, queueAccess); + if (rc && excludeIds != null) { + for (int id : excludeIds) { + mi.setItemVisibility(id, false); + } + } + + return rc; + } + + public static boolean onMenuItemClicked(Context context, int menuItemId, + FeedItem selectedItem) throws DownloadRequestException { + DownloadRequester requester = DownloadRequester.getInstance(); + switch (menuItemId) { + case R.id.skip_episode_item: + context.sendBroadcast(new Intent( + PlaybackService.ACTION_SKIP_CURRENT_EPISODE)); + break; + case R.id.download_item: + DBTasks.downloadFeedItems(context, selectedItem); + break; + case R.id.play_item: + DBTasks.playMedia(context, selectedItem.getMedia(), true, true, + false); + break; + case R.id.remove_item: + DBWriter.deleteFeedMediaOfItem(context, selectedItem.getMedia().getId()); + break; + case R.id.cancel_download_item: + requester.cancelDownload(context, selectedItem.getMedia()); + break; + case R.id.mark_read_item: + DBWriter.markItemRead(context, selectedItem, true, true); + break; + case R.id.mark_unread_item: + DBWriter.markItemRead(context, selectedItem, false, true); + break; + case R.id.add_to_queue_item: + DBWriter.addQueueItem(context, selectedItem.getId()); + break; + case R.id.remove_from_queue_item: + DBWriter.removeQueueItem(context, selectedItem.getId(), true); + break; + case R.id.stream_item: + DBTasks.playMedia(context, selectedItem.getMedia(), true, true, + true); + break; + case R.id.visit_website_item: + Uri uri = Uri.parse(selectedItem.getLink()); + context.startActivity(new Intent(Intent.ACTION_VIEW, uri)); + break; + case R.id.support_item: + DBTasks.flattrItemIfLoggedIn(context, selectedItem); + break; + case R.id.share_link_item: + ShareUtils.shareFeedItemLink(context, selectedItem); + break; + default: + return false; + } + // Refresh menu state + + return true; + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java new file mode 100644 index 000000000..62ae28820 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java @@ -0,0 +1,86 @@ +package de.danoeh.antennapod.menuhandler; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.service.download.DownloadService; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.ShareUtils; + +/** Handles interactions with the FeedItemMenu. */ +public class FeedMenuHandler { + private static final String TAG = "FeedMenuHandler"; + + public static boolean onCreateOptionsMenu(MenuInflater inflater, Menu menu) { + inflater.inflate(R.menu.feedlist, menu); + return true; + } + + public static boolean onPrepareOptionsMenu(Menu menu, Feed selectedFeed) { + if (selectedFeed == null) { + return true; + } + + if (BuildConfig.DEBUG) + Log.d(TAG, "Preparing options menu"); + menu.findItem(R.id.mark_all_read_item).setVisible( + selectedFeed.hasNewItems(true)); + if (selectedFeed.getPaymentLink() != null && selectedFeed.getFlattrStatus().flattrable()) + menu.findItem(R.id.support_item).setVisible(true); + else + menu.findItem(R.id.support_item).setVisible(false); + MenuItem refresh = menu.findItem(R.id.refresh_item); + if (DownloadService.isRunning + && DownloadRequester.getInstance().isDownloadingFile( + selectedFeed)) { + refresh.setVisible(false); + } else { + refresh.setVisible(true); + } + + return true; + } + + /** + * NOTE: This method does not handle clicks on the 'remove feed' - item. + * + * @throws DownloadRequestException + */ + public static boolean onOptionsItemClicked(Context context, MenuItem item, + Feed selectedFeed) throws DownloadRequestException { + switch (item.getItemId()) { + case R.id.refresh_item: + DBTasks.refreshFeed(context, selectedFeed); + break; + case R.id.mark_all_read_item: + DBWriter.markFeedRead(context, selectedFeed.getId()); + break; + case R.id.visit_website_item: + Uri uri = Uri.parse(selectedFeed.getLink()); + context.startActivity(new Intent(Intent.ACTION_VIEW, uri)); + break; + case R.id.support_item: + DBTasks.flattrFeedIfLoggedIn(context, selectedFeed); + break; + case R.id.share_link_item: + ShareUtils.shareFeedlink(context, selectedFeed); + break; + case R.id.share_source_item: + ShareUtils.shareFeedDownloadLink(context, selectedFeed); + break; + default: + return false; + } + return true; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java new file mode 100644 index 000000000..c4a96ac3f --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java @@ -0,0 +1,31 @@ +package de.danoeh.antennapod.menuhandler; + +import android.support.v4.view.MenuItemCompat; +import android.support.v7.widget.SearchView; +import android.view.Menu; +import android.view.MenuItem; + +import de.danoeh.antennapod.core.R; + +/** + * Utilities for menu items + */ +public class MenuItemUtils { + + public static MenuItem addSearchItem(Menu menu, SearchView searchView) { + MenuItem item = menu.add(Menu.NONE, R.id.search_item, Menu.NONE, R.string.search_label); + MenuItemCompat.setShowAsAction(item, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM); + MenuItemCompat.setActionView(item, searchView); + return item; + } + + /** + * Checks if the navigation drawer of the DrawerActivity is opened. This can be useful for Fragments + * that hide their menu if the navigation drawer is open. + * + * @return True if the drawer is open, false otherwise (also if the parameter is null) + */ + public static boolean isActivityDrawerOpen(NavDrawerActivity activity) { + return activity != null && activity.isDrawerOpen(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/NavDrawerActivity.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/NavDrawerActivity.java new file mode 100644 index 000000000..6ceaaada4 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/NavDrawerActivity.java @@ -0,0 +1,9 @@ +package de.danoeh.antennapod.menuhandler; + +/** + * Defines useful methods for activities that have a navigation drawer + */ +public interface NavDrawerActivity { + + public boolean isDrawerOpen(); +} diff --git a/app/src/main/java/de/danoeh/antennapod/receiver/ConnectivityActionReceiver.java b/app/src/main/java/de/danoeh/antennapod/receiver/ConnectivityActionReceiver.java new file mode 100644 index 000000000..f55a7603f --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/receiver/ConnectivityActionReceiver.java @@ -0,0 +1,46 @@ +package de.danoeh.antennapod.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.util.Log; + +import org.apache.commons.lang3.StringUtils; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.NetworkUtils; + +public class ConnectivityActionReceiver extends BroadcastReceiver { + private static final String TAG = "ConnectivityActionReceiver"; + + @Override + public void onReceive(final Context context, Intent intent) { + if (StringUtils.equals(intent.getAction(), ConnectivityManager.CONNECTIVITY_ACTION)) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Received intent"); + + if (NetworkUtils.autodownloadNetworkAvailable(context)) { + if (BuildConfig.DEBUG) + Log.d(TAG, + "auto-dl network available, starting auto-download"); + DBTasks.autodownloadUndownloadedItems(context); + } else { // if new network is Wi-Fi, finish ongoing downloads, + // otherwise cancel all downloads + ConnectivityManager cm = (ConnectivityManager) context + .getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo ni = cm.getActiveNetworkInfo(); + if (ni == null || ni.getType() != ConnectivityManager.TYPE_WIFI) { + if (BuildConfig.DEBUG) + Log.i(TAG, + "Device is no longer connected to Wi-Fi. Cancelling ongoing downloads"); + DownloadRequester.getInstance().cancelAllDownloads(context); + } + + } + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/receiver/PlayerWidget.java b/app/src/main/java/de/danoeh/antennapod/receiver/PlayerWidget.java new file mode 100644 index 000000000..7ab386edf --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/receiver/PlayerWidget.java @@ -0,0 +1,49 @@ +package de.danoeh.antennapod.receiver; + +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import org.apache.commons.lang3.StringUtils; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.service.PlayerWidgetService; + +public class PlayerWidget extends AppWidgetProvider { + private static final String TAG = "PlayerWidget"; + + @Override + public void onReceive(Context context, Intent intent) { + if (StringUtils.equals(intent.getAction(), PlaybackService.FORCE_WIDGET_UPDATE)) { + startUpdate(context); + } else if (StringUtils.equals(intent.getAction(), PlaybackService.STOP_WIDGET_UPDATE)) { + stopUpdate(context); + } + + } + + @Override + public void onEnabled(Context context) { + super.onEnabled(context); + if (BuildConfig.DEBUG) + Log.d(TAG, "Widget enabled"); + } + + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, + int[] appWidgetIds) { + startUpdate(context); + } + + private void startUpdate(Context context) { + context.startService(new Intent(context, PlayerWidgetService.class)); + } + + private void stopUpdate(Context context) { + context.stopService(new Intent(context, PlayerWidgetService.class)); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/receiver/SPAReceiver.java b/app/src/main/java/de/danoeh/antennapod/receiver/SPAReceiver.java new file mode 100644 index 000000000..359a546f6 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/receiver/SPAReceiver.java @@ -0,0 +1,55 @@ +package de.danoeh.antennapod.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import android.widget.Toast; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import org.apache.commons.lang3.StringUtils; + +import java.util.Arrays; +import java.util.Date; + +/** + * Receives intents from AntennaPod Single Purpose apps + */ +public class SPAReceiver extends BroadcastReceiver{ + private static final String TAG = "SPAReceiver"; + + public static final String ACTION_SP_APPS_QUERY_FEEDS = "de.danoeh.antennapdsp.intent.SP_APPS_QUERY_FEEDS"; + public static final String ACTION_SP_APPS_QUERY_FEEDS_REPSONSE = "de.danoeh.antennapdsp.intent.SP_APPS_QUERY_FEEDS_RESPONSE"; + public static final String ACTION_SP_APPS_QUERY_FEEDS_REPSONSE_FEEDS_EXTRA = "feeds"; + + @Override + public void onReceive(Context context, Intent intent) { + if (StringUtils.equals(intent.getAction(), ACTION_SP_APPS_QUERY_FEEDS_REPSONSE)) { + if (BuildConfig.DEBUG) Log.d(TAG, "Received SP_APPS_QUERY_RESPONSE"); + if (intent.hasExtra(ACTION_SP_APPS_QUERY_FEEDS_REPSONSE_FEEDS_EXTRA)) { + String[] feedUrls = intent.getStringArrayExtra(ACTION_SP_APPS_QUERY_FEEDS_REPSONSE_FEEDS_EXTRA); + if (feedUrls != null) { + if (BuildConfig.DEBUG) Log.d(TAG, "Received feeds list: " + Arrays.toString(feedUrls)); + for (String url : feedUrls) { + Feed f = new Feed(url, new Date()); + try { + DownloadRequester.getInstance().downloadFeed(context, f); + } catch (DownloadRequestException e) { + Log.e(TAG, "Error while trying to add feed " + url); + e.printStackTrace(); + } + } + Toast.makeText(context, R.string.sp_apps_importing_feeds_msg, Toast.LENGTH_LONG).show(); + + } else { + Log.e(TAG, "Received invalid SP_APPS_QUERY_REPSONSE: extra was null"); + } + } else { + Log.e(TAG, "Received invalid SP_APPS_QUERY_RESPONSE: Contains no extra"); + } + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/service/PlayerWidgetService.java b/app/src/main/java/de/danoeh/antennapod/service/PlayerWidgetService.java new file mode 100644 index 000000000..4622e2f79 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/service/PlayerWidgetService.java @@ -0,0 +1,203 @@ +package de.danoeh.antennapod.service; + +import android.app.PendingIntent; +import android.app.Service; +import android.appwidget.AppWidgetManager; +import android.content.ComponentName; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Build; +import android.os.IBinder; +import android.util.Log; +import android.view.KeyEvent; +import android.view.View; +import android.widget.RemoteViews; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; +import de.danoeh.antennapod.receiver.PlayerWidget; +import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.core.service.playback.PlayerStatus; +import de.danoeh.antennapod.core.util.Converter; +import de.danoeh.antennapod.core.util.playback.Playable; + +/** Updates the state of the player widget */ +public class PlayerWidgetService extends Service { + private static final String TAG = "PlayerWidgetService"; + + private PlaybackService playbackService; + + /** Controls write access to playbackservice reference */ + private Object psLock; + + /** True while service is updating the widget */ + private volatile boolean isUpdating; + + public PlayerWidgetService() { + } + + @Override + public void onCreate() { + super.onCreate(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Service created"); + isUpdating = false; + psLock = new Object(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Service is about to be destroyed"); + try { + unbindService(mConnection); + } catch (IllegalArgumentException e) { + Log.w(TAG, "IllegalArgumentException when trying to unbind service"); + } + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (!isUpdating) { + if (playbackService == null && PlaybackService.isRunning) { + bindService(new Intent(this, PlaybackService.class), + mConnection, 0); + } else { + startViewUpdaterIfNotRunning(); + } + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, + "Service was called while updating. Ignoring update request"); + } + return Service.START_NOT_STICKY; + } + + private void updateViews() { + if (playbackService == null) { + return; + } + isUpdating = true; + + ComponentName playerWidget = new ComponentName(this, PlayerWidget.class); + AppWidgetManager manager = AppWidgetManager.getInstance(this); + RemoteViews views = new RemoteViews(getPackageName(), + R.layout.player_widget); + PendingIntent startMediaplayer = PendingIntent.getActivity(this, 0, + PlaybackService.getPlayerActivityIntent(this), 0); + + views.setOnClickPendingIntent(R.id.layout_left, startMediaplayer); + final Playable media = playbackService.getPlayable(); + if (playbackService != null && media != null) { + PlayerStatus status = playbackService.getStatus(); + + views.setTextViewText(R.id.txtvTitle, media.getEpisodeTitle()); + + if (status == PlayerStatus.PLAYING) { + String progressString = getProgressString(playbackService); + if (progressString != null) { + views.setTextViewText(R.id.txtvProgress, progressString); + } + views.setImageViewResource(R.id.butPlay, R.drawable.av_pause_dark); + if (Build.VERSION.SDK_INT >= 15) { + views.setContentDescription(R.id.butPlay, getString(R.string.pause_label)); + } + } else { + views.setImageViewResource(R.id.butPlay, R.drawable.av_play_dark); + if (Build.VERSION.SDK_INT >= 15) { + views.setContentDescription(R.id.butPlay, getString(R.string.play_label)); + } + } + views.setOnClickPendingIntent(R.id.butPlay, + createMediaButtonIntent()); + } else { + views.setViewVisibility(R.id.txtvProgress, View.INVISIBLE); + views.setTextViewText(R.id.txtvTitle, + this.getString(R.string.no_media_playing_label)); + views.setImageViewResource(R.id.butPlay, R.drawable.av_play); + + } + + manager.updateAppWidget(playerWidget, views); + isUpdating = false; + } + + /** Creates an intent which fakes a mediabutton press */ + private PendingIntent createMediaButtonIntent() { + KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, + KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE); + Intent startingIntent = new Intent( + MediaButtonReceiver.NOTIFY_BUTTON_RECEIVER); + startingIntent.putExtra(Intent.EXTRA_KEY_EVENT, event); + + return PendingIntent.getBroadcast(this, 0, startingIntent, 0); + } + + private String getProgressString(PlaybackService ps) { + int position = ps.getCurrentPosition(); + int duration = ps.getDuration(); + if (position != PlaybackService.INVALID_TIME + && duration != PlaybackService.INVALID_TIME) { + return Converter.getDurationStringLong(position) + " / " + + Converter.getDurationStringLong(duration); + } else { + return null; + } + } + + private ServiceConnection mConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder service) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Connection to service established"); + synchronized (psLock) { + playbackService = ((PlaybackService.LocalBinder) service) + .getService(); + startViewUpdaterIfNotRunning(); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + synchronized (psLock) { + playbackService = null; + if (BuildConfig.DEBUG) + Log.d(TAG, "Disconnected from service"); + } + } + + }; + + private void startViewUpdaterIfNotRunning() { + if (!isUpdating) { + ViewUpdater updateThread = new ViewUpdater(this); + updateThread.start(); + } + } + + class ViewUpdater extends Thread { + private static final String THREAD_NAME = "ViewUpdater"; + private PlayerWidgetService service; + + public ViewUpdater(PlayerWidgetService service) { + super(); + setName(THREAD_NAME); + this.service = service; + + } + + @Override + public void run() { + synchronized (psLock) { + service.updateViews(); + } + } + + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/spa/SPAUtil.java b/app/src/main/java/de/danoeh/antennapod/spa/SPAUtil.java new file mode 100644 index 000000000..75cbd8b5a --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/spa/SPAUtil.java @@ -0,0 +1,69 @@ +package de.danoeh.antennapod.spa; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.util.Log; + +import org.apache.commons.lang3.Validate; + +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.receiver.SPAReceiver; + +/** + * Provides methods related to AntennaPodSP (https://github.com/danieloeh/AntennaPodSP) + */ +public class SPAUtil { + private static final String TAG = "SPAUtil"; + + private static final String PREF_HAS_QUERIED_SP_APPS = "prefSPAUtil.hasQueriedSPApps"; + + private SPAUtil() { + } + + + /** + * Sends an ACTION_SP_APPS_QUERY_FEEDS intent to all AntennaPod Single Purpose apps. + * The receiving single purpose apps will then send their feeds back to AntennaPod via an + * ACTION_SP_APPS_QUERY_FEEDS_RESPONSE intent. + * This intent will only be sent once. + * + * @return True if an intent was sent, false otherwise (for example if the intent has already been + * sent before. + */ + public static synchronized boolean sendSPAppsQueryFeedsIntent(Context context) { + if (context == null) throw new IllegalArgumentException("context = null"); + final Context appContext = context.getApplicationContext(); + if (appContext == null) { + Log.wtf(TAG, "Unable to get application context"); + return false; + } + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(appContext); + if (!prefs.getBoolean(PREF_HAS_QUERIED_SP_APPS, false)) { + appContext.sendBroadcast(new Intent(SPAReceiver.ACTION_SP_APPS_QUERY_FEEDS)); + if (BuildConfig.DEBUG) Log.d(TAG, "Sending SP_APPS_QUERY_FEEDS intent"); + + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(PREF_HAS_QUERIED_SP_APPS, true); + editor.commit(); + + return true; + } else { + return false; + } + } + + /** + * Resets all preferences created by this class. Should only be used for debug purposes. + */ + public static void resetSPAPreferences(Context c) { + if (BuildConfig.DEBUG) { + Validate.notNull(c); + SharedPreferences.Editor editor = PreferenceManager + .getDefaultSharedPreferences(c.getApplicationContext()).edit(); + editor.putBoolean(PREF_HAS_QUERIED_SP_APPS, false); + editor.commit(); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/view/AspectRatioVideoView.java b/app/src/main/java/de/danoeh/antennapod/view/AspectRatioVideoView.java new file mode 100644 index 000000000..f930c912a --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/view/AspectRatioVideoView.java @@ -0,0 +1,97 @@ +package de.danoeh.antennapod.view; + +/* + * Copyright (C) Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.VideoView; + +public class AspectRatioVideoView extends VideoView { + + + private int mVideoWidth; + private int mVideoHeight; + + public AspectRatioVideoView(Context context) { + this(context, null); + } + + public AspectRatioVideoView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public AspectRatioVideoView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + mVideoWidth = 0; + mVideoHeight = 0; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (mVideoWidth <= 0 || mVideoHeight <= 0) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + return; + } + + float heightRatio = (float) mVideoHeight / (float) getHeight(); + float widthRatio = (float) mVideoWidth / (float) getWidth(); + + int scaledHeight; + int scaledWidth; + + if (heightRatio > widthRatio) { + scaledHeight = (int) Math.ceil((float) mVideoHeight + / heightRatio); + scaledWidth = (int) Math.ceil((float) mVideoWidth + / heightRatio); + } else { + scaledHeight = (int) Math.ceil((float) mVideoHeight + / widthRatio); + scaledWidth = (int) Math.ceil((float) mVideoWidth + / widthRatio); + } + + setMeasuredDimension(scaledWidth, scaledHeight); + } + + /** + * Source code originally from: + * http://clseto.mysinablog.com/index.php?op=ViewArticle&articleId=2992625 + * + * @param videoWidth + * @param videoHeight + */ + public void setVideoSize(int videoWidth, int videoHeight) { + // Set the new video size + mVideoWidth = videoWidth; + mVideoHeight = videoHeight; + + /** + * If this isn't set the video is stretched across the + * SurfaceHolders display surface (i.e. the SurfaceHolder + * as the same size and the video is drawn to fit this + * display area). We want the size to be the video size + * and allow the aspectratio to handle how the surface is shown + */ + getHolder().setFixedSize(videoWidth, videoHeight); + + requestLayout(); + invalidate(); + } + +} |