diff options
Diffstat (limited to 'app/src/main/java/de/danoeh/antennapod/activity')
20 files changed, 4686 insertions, 0 deletions
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..eb7a844db --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java @@ -0,0 +1,749 @@ +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.Fragment; +import android.support.v4.app.FragmentTransaction; +import android.support.v4.app.ListFragment; +import android.support.v4.widget.DrawerLayout; +import android.support.v7.app.ActionBarDrawerToggle; +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 com.squareup.picasso.Picasso; + +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.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.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.util.playback.ExternalMedia; +import de.danoeh.antennapod.core.util.playback.Playable; +import de.danoeh.antennapod.core.util.playback.PlaybackController; +import de.danoeh.antennapod.dialog.VariableSpeedDialog; +import de.danoeh.antennapod.fragment.CoverFragment; +import de.danoeh.antennapod.fragment.ItemDescriptionFragment; +import de.danoeh.antennapod.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.menuhandler.NavDrawerActivity; + +/** + * 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) { + if (resId == R.string.player_preparing_msg + || resId == R.string.player_seeking_msg + || resId == R.string.player_buffering_msg) { + // TODO Show progress bar here + } + } + + @Override + protected void clearStatusMsg() { + // TODO Hide progress bar here + + } + + /** + * 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() { + Picasso.with(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() { + Picasso.with(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); + + drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, R.string.drawer_open, R.string.drawer_close) { + CharSequence currentTitle = getSupportActionBar().getTitle(); + + @Override + public void onDrawerOpened(View drawerView) { + super.onDrawerOpened(drawerView); + currentTitle = getSupportActionBar().getTitle(); + getSupportActionBar().setTitle(R.string.app_name); + supportInvalidateOptionsMenu(); + } + + @Override + public void onDrawerClosed(View drawerView) { + super.onDrawerClosed(drawerView); + getSupportActionBar().setTitle(currentTitle); + supportInvalidateOptionsMenu(); + } + }; + + 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..d265c05b1 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java @@ -0,0 +1,249 @@ +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 com.squareup.picasso.Picasso; + +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.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())) { + Picasso.with(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..93c71a868 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/activity/FeedInfoActivity.java @@ -0,0 +1,198 @@ +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.CheckBox; +import android.widget.CompoundButton; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.TextView; + +import com.squareup.picasso.Picasso; + +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.R; +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() { + Picasso.with(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..3b10ba4c3 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/activity/FlattrAuthActivity.java @@ -0,0 +1,127 @@ +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 de.danoeh.antennapod.preferences.PreferenceController; + +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, PreferenceController.getPreferenceActivity()); + 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..df6ff1046 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java @@ -0,0 +1,438 @@ +package de.danoeh.antennapod.activity; + +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.media.AudioManager; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Handler; +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.support.v7.app.ActionBarDrawerToggle; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ListView; + +import org.apache.commons.lang3.Validate; + +import java.util.List; + +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.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.util.StorageUtils; +import de.danoeh.antennapod.fragment.AddFeedFragment; +import de.danoeh.antennapod.fragment.DownloadsFragment; +import de.danoeh.antennapod.fragment.ExternalPlayerFragment; +import de.danoeh.antennapod.fragment.ItemlistFragment; +import de.danoeh.antennapod.fragment.NewEpisodesFragment; +import de.danoeh.antennapod.fragment.PlaybackHistoryFragment; +import de.danoeh.antennapod.fragment.QueueFragment; +import de.danoeh.antennapod.menuhandler.NavDrawerActivity; +import de.danoeh.antennapod.preferences.PreferenceController; + +/** + * 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); + 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); + + drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, 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(); + + } + }; + + 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, PreferenceController.getPreferenceActivity())); + 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..561188291 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java @@ -0,0 +1,500 @@ +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.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 de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.R; +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; +import de.danoeh.antennapod.dialog.TimeDialog; + +/** + * 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); + + 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()) { + 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..9f028000e --- /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, false, 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, null); + 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..c40489374 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java @@ -0,0 +1,120 @@ +package de.danoeh.antennapod.activity; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.PreferenceFragment; +import android.support.v7.app.ActionBar; +import android.support.v7.app.ActionBarActivity; +import android.view.Menu; +import android.view.MenuItem; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.preferences.PreferenceController; + +/** + * PreferenceActivity for API 11+. In order to change the behavior of the preference UI, see + * PreferenceController. + */ +public class PreferenceActivity extends ActionBarActivity { + + private PreferenceController preferenceController; + private MainFragment prefFragment; + private static PreferenceActivity instance; + + + private final PreferenceController.PreferenceUI preferenceUI = new PreferenceController.PreferenceUI() { + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + @Override + public Preference findPreference(CharSequence key) { + return prefFragment.findPreference(key); + } + + @Override + public Activity getActivity() { + return PreferenceActivity.this; + } + }; + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + @Override + protected void onCreate(Bundle savedInstanceState) { + setTheme(UserPreferences.getTheme()); + super.onCreate(savedInstanceState); + instance = this; + + ActionBar ab = getSupportActionBar(); + if (ab != null) { + ab.setDisplayHomeAsUpEnabled(true); + } + + // set up layout + FrameLayout root = new FrameLayout(this); + root.setId(R.id.content); + root.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + setContentView(root); + prefFragment = new MainFragment(); + getFragmentManager().beginTransaction().replace(R.id.content, prefFragment).commit(); + + preferenceController = new PreferenceController(preferenceUI); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + preferenceController.onActivityResult(requestCode, resultCode, data); + } + + @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(); + } + + @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; + } + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public static class MainFragment extends PreferenceFragment { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.preferences); + instance.preferenceController.onCreate(); + } + + @Override + public void onResume() { + super.onResume(); + instance.preferenceController.onResume(); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivityGingerbread.java b/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivityGingerbread.java new file mode 100644 index 000000000..c58593f77 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivityGingerbread.java @@ -0,0 +1,96 @@ +package de.danoeh.antennapod.activity; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Intent; +import android.content.res.Resources.Theme; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.PreferenceScreen; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.preferences.PreferenceController; + +/** + * PreferenceActivity for API 10. In order to change the behavior of the preference UI, see + * PreferenceController. + */ +public class PreferenceActivityGingerbread extends android.preference.PreferenceActivity { + private static final String TAG = "PreferenceActivity"; + + private PreferenceController preferenceController; + + private final PreferenceController.PreferenceUI preferenceUI = new PreferenceController.PreferenceUI() { + + @SuppressWarnings("deprecation") + @Override + public Preference findPreference(CharSequence key) { + return PreferenceActivityGingerbread.this.findPreference(key); + } + + @Override + public Activity getActivity() { + return PreferenceActivityGingerbread.this; + } + }; + + @SuppressLint("NewApi") + @SuppressWarnings("deprecation") + @Override + public void onCreate(Bundle savedInstanceState) { + setTheme(UserPreferences.getTheme()); + super.onCreate(savedInstanceState); + + addPreferencesFromResource(R.xml.preferences); + preferenceController = new PreferenceController(preferenceUI); + preferenceController.onCreate(); + } + + + @Override + protected void onResume() { + super.onResume(); + preferenceController.onResume(); + } + + @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); + preferenceController.onActivityResult(requestCode, resultCode, data); + } + + @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..d625f21da --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java @@ -0,0 +1,360 @@ +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.support.v4.view.WindowCompat; +import android.util.Log; +import android.util.Pair; +import android.view.MotionEvent; +import android.view.SurfaceHolder; +import android.view.View; +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_VideoPlayer); + } + + @SuppressLint("AppCompatMethod") + @Override + protected void onCreate(Bundle savedInstanceState) { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + supportRequestWindowFeature(WindowCompat.FEATURE_ACTION_BAR_OVERLAY); // has to be called before setting layout content + 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); + getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); + + } + 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(); + } + } +} |