diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 767f7a9b9..fa003cf3a 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -35,8 +35,13 @@ android:name="de.danoeh.antennapod.PodcastApp" android:icon="@drawable/ic_launcher" android:label="@string/app_name" + android:backupAgent=".backup.OpmlBackupAgent" + android:restoreAnyVersion="true" android:logo="@drawable/ic_launcher" android:theme="@style/Theme.AntennaPod.Light"> + Number of new episodes Number of episodes you have started listening to + + "Restored feed subscriptions from backup" + Importing subscriptions from single-purpose apps… diff --git a/src/de/danoeh/antennapod/activity/MainActivity.java b/src/de/danoeh/antennapod/activity/MainActivity.java index 29e36abc8..1a49e63d0 100644 --- a/src/de/danoeh/antennapod/activity/MainActivity.java +++ b/src/de/danoeh/antennapod/activity/MainActivity.java @@ -35,152 +35,153 @@ import de.danoeh.antennapod.util.StorageUtils; import java.util.ArrayList; -/** The activity that is shown when the user launches the app. */ +/** + * The activity that is shown when the user launches the app. + */ public class MainActivity extends ActionBarActivity { - private static final String TAG = "MainActivity"; + private static final String TAG = "MainActivity"; - private static final int EVENTS = EventDistributor.DOWNLOAD_HANDLED - | EventDistributor.DOWNLOAD_QUEUED; + private static final int EVENTS = EventDistributor.DOWNLOAD_HANDLED + | EventDistributor.DOWNLOAD_QUEUED; - private ViewPager viewpager; - private TabsAdapter pagerAdapter; - private ExternalPlayerFragment externalPlayerFragment; + private ViewPager viewpager; + private TabsAdapter pagerAdapter; + private ExternalPlayerFragment externalPlayerFragment; - private static boolean appLaunched = false; + private static boolean appLaunched = false; - @Override - public void onCreate(Bundle savedInstanceState) { - setTheme(UserPreferences.getTheme()); - super.onCreate(savedInstanceState); - StorageUtils.checkStorageAvailability(this); - requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); - setContentView(R.layout.main); + @Override + public void onCreate(Bundle savedInstanceState) { + setTheme(UserPreferences.getTheme()); + super.onCreate(savedInstanceState); + StorageUtils.checkStorageAvailability(this); + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + setContentView(R.layout.main); setVolumeControlStream(AudioManager.STREAM_MUSIC); getSupportActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); - viewpager = (ViewPager) findViewById(R.id.viewpager); - pagerAdapter = new TabsAdapter(this, viewpager); + viewpager = (ViewPager) findViewById(R.id.viewpager); + pagerAdapter = new TabsAdapter(this, viewpager); - viewpager.setAdapter(pagerAdapter); + viewpager.setAdapter(pagerAdapter); - ActionBar.Tab feedsTab = getSupportActionBar().newTab(); - feedsTab.setText(R.string.podcasts_label); - ActionBar.Tab episodesTab = getSupportActionBar().newTab(); - episodesTab.setText(R.string.episodes_label); + ActionBar.Tab feedsTab = getSupportActionBar().newTab(); + feedsTab.setText(R.string.podcasts_label); + ActionBar.Tab episodesTab = getSupportActionBar().newTab(); + episodesTab.setText(R.string.episodes_label); - pagerAdapter.addTab(feedsTab, FeedlistFragment.class, null); - pagerAdapter.addTab(episodesTab, EpisodesFragment.class, null); + pagerAdapter.addTab(feedsTab, FeedlistFragment.class, null); + pagerAdapter.addTab(episodesTab, EpisodesFragment.class, null); - FragmentTransaction transaction = getSupportFragmentManager() - .beginTransaction(); - externalPlayerFragment = new ExternalPlayerFragment(); - transaction.replace(R.id.playerFragment, externalPlayerFragment); - transaction.commit(); + FragmentTransaction transaction = getSupportFragmentManager() + .beginTransaction(); + externalPlayerFragment = new ExternalPlayerFragment(); + transaction.replace(R.id.playerFragment, externalPlayerFragment); + transaction.commit(); - // executed on application start - if (!appLaunched && getIntent().getAction() != null - && getIntent().getAction().equals(Intent.ACTION_MAIN)) { - appLaunched = true; - if (DBReader.getNumberOfUnreadItems(this) > 0) { - // select 'episodes' tab - getSupportActionBar().setSelectedNavigationItem(1); - } - } - if (savedInstanceState != null) { - getSupportActionBar().setSelectedNavigationItem( - savedInstanceState.getInt("tab", 0)); - } - } + // executed on application start + if (!appLaunched && getIntent().getAction() != null + && getIntent().getAction().equals(Intent.ACTION_MAIN)) { + appLaunched = true; + if (DBReader.getNumberOfUnreadItems(this) > 0) { + // select 'episodes' tab + getSupportActionBar().setSelectedNavigationItem(1); + } + } + if (savedInstanceState != null) { + getSupportActionBar().setSelectedNavigationItem( + savedInstanceState.getInt("tab", 0)); + } + } - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putInt("tab", getSupportActionBar() - .getSelectedNavigationIndex()); - } + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt("tab", getSupportActionBar() + .getSelectedNavigationIndex()); + } - @Override - protected void onPause() { - super.onPause(); - EventDistributor.getInstance().unregister(contentUpdate); - } + @Override + protected void onPause() { + super.onPause(); + EventDistributor.getInstance().unregister(contentUpdate); + } - @Override - protected void onResume() { - super.onResume(); - StorageUtils.checkStorageAvailability(this); - updateProgressBarVisibility(); - EventDistributor.getInstance().register(contentUpdate); + @Override + protected void onResume() { + super.onResume(); + StorageUtils.checkStorageAvailability(this); + updateProgressBarVisibility(); + EventDistributor.getInstance().register(contentUpdate); + } - } + private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() { - 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."); + updateProgressBarVisibility(); + } + } + }; - @Override - public void update(EventDistributor eventDistributor, Integer arg) { - if ((EVENTS & arg) != 0) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received contentUpdate Intent."); - updateProgressBarVisibility(); - } - } - }; + private void updateProgressBarVisibility() { + if (DownloadService.isRunning + && DownloadRequester.getInstance().isDownloadingFeeds()) { + setSupportProgressBarIndeterminateVisibility(true); + } else { + setSupportProgressBarIndeterminateVisibility(false); + } + supportInvalidateOptionsMenu(); + } - private void updateProgressBarVisibility() { - if (DownloadService.isRunning - && DownloadRequester.getInstance().isDownloadingFeeds()) { - setSupportProgressBarIndeterminateVisibility(true); - } else { - setSupportProgressBarIndeterminateVisibility(false); - } - supportInvalidateOptionsMenu(); - } + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.add_feed: + startActivity(new Intent(this, AddFeedActivity.class)); + return true; + case R.id.all_feed_refresh: + DBTasks.refreshAllFeeds(this, null); + return true; + case R.id.show_downloads: + startActivity(new Intent(this, DownloadActivity.class)); + return true; + case R.id.show_preferences: + startActivity(new Intent(this, PreferenceActivity.class)); + return true; + case R.id.show_player: + startActivity(PlaybackService.getPlayerActivityIntent(this)); + return true; + case R.id.show_playback_history: + startActivity(new Intent(this, PlaybackHistoryActivity.class)); + return true; + default: + return super.onOptionsItemSelected(item); + } + } - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.add_feed: - startActivity(new Intent(this, AddFeedActivity.class)); - return true; - case R.id.all_feed_refresh: - DBTasks.refreshAllFeeds(this, null); - return true; - case R.id.show_downloads: - startActivity(new Intent(this, DownloadActivity.class)); - return true; - case R.id.show_preferences: - startActivity(new Intent(this, PreferenceActivity.class)); - return true; - case R.id.show_player: - startActivity(PlaybackService.getPlayerActivityIntent(this)); - return true; - case R.id.show_playback_history: - startActivity(new Intent(this, PlaybackHistoryActivity.class)); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { + @Override + public boolean onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); - MenuItem refreshAll = menu.findItem(R.id.all_feed_refresh); - if (DownloadService.isRunning - && DownloadRequester.getInstance().isDownloadingFeeds()) { - refreshAll.setVisible(false); - } else { - refreshAll.setVisible(true); - } - return true; - } + MenuItem refreshAll = menu.findItem(R.id.all_feed_refresh); + if (DownloadService.isRunning + && DownloadRequester.getInstance().isDownloadingFeeds()) { + refreshAll.setVisible(false); + } else { + refreshAll.setVisible(true); + } + return true; + } - @Override - public boolean onCreateOptionsMenu(Menu menu) { + @Override + public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.main, menu); + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.main, menu); SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); @@ -198,87 +199,87 @@ public class MainActivity extends ActionBarActivity { return true; - } + } - public static class TabsAdapter extends FragmentPagerAdapter implements - ActionBar.TabListener, ViewPager.OnPageChangeListener { - private final Context mContext; - private final ActionBar mActionBar; - private final ViewPager mViewPager; - private final ArrayList mTabs = new ArrayList(); + public static class TabsAdapter extends FragmentPagerAdapter implements + ActionBar.TabListener, ViewPager.OnPageChangeListener { + private final Context mContext; + private final ActionBar mActionBar; + private final ViewPager mViewPager; + private final ArrayList mTabs = new ArrayList(); - static final class TabInfo { - private final Class clss; - private final Bundle args; + static final class TabInfo { + private final Class clss; + private final Bundle args; - TabInfo(Class _class, Bundle _args) { - clss = _class; - args = _args; - } - } + TabInfo(Class _class, Bundle _args) { + clss = _class; + args = _args; + } + } - public TabsAdapter(MainActivity activity, ViewPager pager) { - super(activity.getSupportFragmentManager()); - mContext = activity; - mActionBar = activity.getSupportActionBar(); - mViewPager = pager; - mViewPager.setAdapter(this); - mViewPager.setOnPageChangeListener(this); - } + public TabsAdapter(MainActivity activity, ViewPager pager) { + super(activity.getSupportFragmentManager()); + mContext = activity; + mActionBar = activity.getSupportActionBar(); + mViewPager = pager; + mViewPager.setAdapter(this); + mViewPager.setOnPageChangeListener(this); + } - public void addTab(ActionBar.Tab tab, Class clss, Bundle args) { - TabInfo info = new TabInfo(clss, args); - tab.setTag(info); - tab.setTabListener(this); - mTabs.add(info); - mActionBar.addTab(tab); - notifyDataSetChanged(); - } + public void addTab(ActionBar.Tab tab, Class clss, Bundle args) { + TabInfo info = new TabInfo(clss, args); + tab.setTag(info); + tab.setTabListener(this); + mTabs.add(info); + mActionBar.addTab(tab); + notifyDataSetChanged(); + } - @Override - public int getCount() { - return mTabs.size(); - } + @Override + public int getCount() { + return mTabs.size(); + } - @Override - public Fragment getItem(int position) { - TabInfo info = mTabs.get(position); - return Fragment.instantiate(mContext, info.clss.getName(), - info.args); - } + @Override + public Fragment getItem(int position) { + TabInfo info = mTabs.get(position); + return Fragment.instantiate(mContext, info.clss.getName(), + info.args); + } - @Override - public void onPageScrolled(int position, float positionOffset, - int positionOffsetPixels) { - } + @Override + public void onPageScrolled(int position, float positionOffset, + int positionOffsetPixels) { + } - @Override - public void onPageSelected(int position) { - mActionBar.setSelectedNavigationItem(position); - } + @Override + public void onPageSelected(int position) { + mActionBar.setSelectedNavigationItem(position); + } - @Override - public void onPageScrollStateChanged(int state) { - } + @Override + public void onPageScrollStateChanged(int state) { + } - @Override - public void onTabSelected(ActionBar.Tab tab, FragmentTransaction ft) { - Object tag = tab.getTag(); - for (int i = 0; i < mTabs.size(); i++) { - if (mTabs.get(i) == tag) { - mViewPager.setCurrentItem(i); - } - } - } + @Override + public void onTabSelected(ActionBar.Tab tab, FragmentTransaction ft) { + Object tag = tab.getTag(); + for (int i = 0; i < mTabs.size(); i++) { + if (mTabs.get(i) == tag) { + mViewPager.setCurrentItem(i); + } + } + } - @Override - public void onTabUnselected(ActionBar.Tab tab, FragmentTransaction ft) { + @Override + public void onTabUnselected(ActionBar.Tab tab, FragmentTransaction ft) { - } + } - @Override - public void onTabReselected(ActionBar.Tab tab, FragmentTransaction ft) { - } - } + @Override + public void onTabReselected(ActionBar.Tab tab, FragmentTransaction ft) { + } + } } diff --git a/src/de/danoeh/antennapod/backup/OpmlBackupAgent.java b/src/de/danoeh/antennapod/backup/OpmlBackupAgent.java new file mode 100644 index 000000000..56d1ca092 --- /dev/null +++ b/src/de/danoeh/antennapod/backup/OpmlBackupAgent.java @@ -0,0 +1,212 @@ +package de.danoeh.antennapod.backup; + +import android.app.backup.BackupAgentHelper; +import android.app.backup.BackupDataInputStream; +import android.app.backup.BackupDataOutput; +import android.app.backup.BackupHelper; +import android.content.Context; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import de.danoeh.antennapod.BuildConfig; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; +import java.math.BigInteger; +import java.security.DigestInputStream; +import java.security.DigestOutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; + +import de.danoeh.antennapod.AppConfig; +import de.danoeh.antennapod.feed.Feed; +import de.danoeh.antennapod.opml.OpmlElement; +import de.danoeh.antennapod.opml.OpmlReader; +import de.danoeh.antennapod.opml.OpmlWriter; +import de.danoeh.antennapod.storage.DBReader; +import de.danoeh.antennapod.storage.DownloadRequestException; +import de.danoeh.antennapod.storage.DownloadRequester; +import de.danoeh.antennapod.util.LangUtils; + +public class OpmlBackupAgent extends BackupAgentHelper { + private static final String OPML_BACKUP_KEY = "opml"; + + @Override + public void onCreate() { + addHelper(OPML_BACKUP_KEY, new OpmlBackupHelper(this)); + } + + private static final void LOGD(String tag, String msg) { + if (BuildConfig.DEBUG && Log.isLoggable(tag, Log.DEBUG)) { + Log.d(tag, msg); + } + } + + private static final void LOGD(String tag, String msg, Throwable tr) { + if (BuildConfig.DEBUG && Log.isLoggable(tag, Log.DEBUG)) { + Log.d(tag, msg, tr); + } + } + + /** Class for backing up and restoring the OPML file. */ + private static class OpmlBackupHelper implements BackupHelper { + private static final String TAG = "OpmlBackupHelper"; + + private static final String OPML_ENTITY_KEY = "antennapod-feeds.opml"; + + private final Context mContext; + + /** Checksum of restored OPML file */ + private byte[] mChecksum; + + public OpmlBackupHelper(Context context) { + mContext = context; + } + + @Override + public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) { + Log.d(TAG, "Performing backup"); + ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); + MessageDigest digester = null; + Writer writer; + + try { + digester = MessageDigest.getInstance("MD5"); + writer = new OutputStreamWriter(new DigestOutputStream(byteStream, digester), + LangUtils.UTF_8); + } catch (NoSuchAlgorithmException e) { + writer = new OutputStreamWriter(byteStream, LangUtils.UTF_8); + } + + try { + // Write OPML + new OpmlWriter().writeDocument(DBReader.getFeedList(mContext), writer); + + // Compare checksum of new and old file to see if we need to perform a backup at all + if (digester != null) { + byte[] newChecksum = digester.digest(); + LOGD(TAG, "New checksum: " + new BigInteger(1, newChecksum).toString(16)); + + // Get the old checksum + if (oldState != null) { + FileInputStream inState = new FileInputStream(oldState.getFileDescriptor()); + int len = inState.read(); + + if (len != -1) { + byte[] oldChecksum = new byte[len]; + inState.read(oldChecksum); + LOGD(TAG, "Old checksum: " + new BigInteger(1, oldChecksum).toString(16)); + + if (Arrays.equals(oldChecksum, newChecksum)) { + LOGD(TAG, "Checksums are the same; won't backup"); + return; + } + } + } + + writeNewStateDescription(newState, newChecksum); + } + + LOGD(TAG, "Backing up OPML"); + byte[] bytes = byteStream.toByteArray(); + data.writeEntityHeader(OPML_ENTITY_KEY, bytes.length); + data.writeEntityData(bytes, bytes.length); + } catch (IOException e) { + Log.e(TAG, "Error during backup", e); + } finally { + if (writer != null) { + try { + writer.close(); + } catch (IOException e) { + } + } + } + } + + @Override + public void restoreEntity(BackupDataInputStream data) { + LOGD(TAG, "Backup restore"); + + if (!OPML_ENTITY_KEY.equals(data.getKey())) { + LOGD(TAG, "Unknown entity key: " + data.getKey()); + return; + } + + MessageDigest digester = null; + Reader reader; + + try { + digester = MessageDigest.getInstance("MD5"); + reader = new InputStreamReader(new DigestInputStream(data, digester), + LangUtils.UTF_8); + } catch (NoSuchAlgorithmException e) { + reader = new InputStreamReader(data, LangUtils.UTF_8); + } + + try { + ArrayList opmlElements = new OpmlReader().readDocument(reader); + mChecksum = digester == null ? null : digester.digest(); + DownloadRequester downloader = DownloadRequester.getInstance(); + Date lastUpdated = new Date(); + + for (OpmlElement opmlElem : opmlElements) { + Feed feed = new Feed(opmlElem.getXmlUrl(), lastUpdated, opmlElem.getText()); + + try { + downloader.downloadFeed(mContext, feed); + } catch (DownloadRequestException e) { + LOGD(TAG, "Error while restoring/downloading feed", e); + } + } + } catch (XmlPullParserException e) { + Log.e(TAG, "Error while parsing the OPML file", e); + } catch (IOException e) { + Log.e(TAG, "Failed to restore OPML backup", e); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + } + } + } + } + + @Override + public void writeNewStateDescription(ParcelFileDescriptor newState) { + writeNewStateDescription(newState, mChecksum); + } + + /** + * Writes the new state description, which is the checksum of the OPML file. + * + * @param newState + * @param checksum + */ + private void writeNewStateDescription(ParcelFileDescriptor newState, byte[] checksum) { + if (checksum == null) { + return; + } + + try { + FileOutputStream outState = new FileOutputStream(newState.getFileDescriptor()); + outState.write(checksum.length); + outState.write(checksum); + outState.flush(); + outState.close(); + } catch (IOException e) { + Log.e(TAG, "Failed to write new state description", e); + } + } + } +} diff --git a/src/de/danoeh/antennapod/preferences/UserPreferences.java b/src/de/danoeh/antennapod/preferences/UserPreferences.java index 3662b646e..31250bcd9 100644 --- a/src/de/danoeh/antennapod/preferences/UserPreferences.java +++ b/src/de/danoeh/antennapod/preferences/UserPreferences.java @@ -74,6 +74,7 @@ public class UserPreferences implements private String playbackSpeed; private String[] playbackSpeedArray; private boolean pauseForFocusLoss; + private boolean isFreshInstall; private UserPreferences(Context context) { this.context = context; @@ -282,6 +283,11 @@ public class UserPreferences implements return instance.pauseForFocusLoss; } + public static boolean isFreshInstall() { + instanceAvailable(); + return instance.isFreshInstall; + } + @Override public void onSharedPreferenceChanged(SharedPreferences sp, String key) { if (BuildConfig.DEBUG) diff --git a/src/de/danoeh/antennapod/storage/DBWriter.java b/src/de/danoeh/antennapod/storage/DBWriter.java index c1ce9da36..f2586cdcb 100644 --- a/src/de/danoeh/antennapod/storage/DBWriter.java +++ b/src/de/danoeh/antennapod/storage/DBWriter.java @@ -1,5 +1,6 @@ package de.danoeh.antennapod.storage; +import android.app.backup.BackupManager; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -197,6 +198,9 @@ public class DBWriter { GpodnetPreferences.addRemovedFeed(feed.getDownload_url()); EventDistributor.getInstance().sendFeedUpdateBroadcast(); + + BackupManager backupManager = new BackupManager(context); + backupManager.dataChanged(); } } }); @@ -695,6 +699,9 @@ public class DBWriter { GpodnetPreferences.addAddedFeed(feed.getDownload_url()); EventDistributor.getInstance().sendFeedUpdateBroadcast(); + + BackupManager backupManager = new BackupManager(context); + backupManager.dataChanged(); } }); } diff --git a/src/de/danoeh/antennapod/storage/PodDBAdapter.java b/src/de/danoeh/antennapod/storage/PodDBAdapter.java index 825b5ac30..8e2d10711 100644 --- a/src/de/danoeh/antennapod/storage/PodDBAdapter.java +++ b/src/de/danoeh/antennapod/storage/PodDBAdapter.java @@ -1,5 +1,6 @@ package de.danoeh.antennapod.storage; +import android.app.backup.BackupManager; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; @@ -386,6 +387,7 @@ public class PodDBAdapter { Log.d(this.toString(), "Updating existing Feed in db"); db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(feed.getId())}); + } return feed.getId(); } @@ -844,6 +846,7 @@ public class PodDBAdapter { removeFeedItem(item); } } + db.delete(TABLE_NAME_FEEDS, KEY_ID + "=?", new String[]{String.valueOf(feed.getId())}); db.setTransactionSuccessful();