From 1feb62ae853443168c27f088fcc30d5b8feb6507 Mon Sep 17 00:00:00 2001 From: Christopher Szucko Date: Fri, 14 Feb 2014 09:24:39 -0600 Subject: [PATCH 01/23] Support HTTP Basic Authentication for Feeds Enables the use of feed URLs with the format http://user:password@example.com --- .../antennapod/service/download/HttpDownloader.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/de/danoeh/antennapod/service/download/HttpDownloader.java b/src/de/danoeh/antennapod/service/download/HttpDownloader.java index fc2b3178b..1ffea19d5 100644 --- a/src/de/danoeh/antennapod/service/download/HttpDownloader.java +++ b/src/de/danoeh/antennapod/service/download/HttpDownloader.java @@ -11,8 +11,10 @@ import org.apache.commons.io.IOUtils; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; +import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.auth.BasicScheme; import java.io.*; import java.net.*; @@ -44,6 +46,15 @@ public class HttpDownloader extends Downloader { InputStream connection = null; try { HttpGet httpGet = new HttpGet(getURIFromRequestUrl(request.getSource())); + String userInfo = httpGet.getURI().getUserInfo(); + if (userInfo != null) { + String[] parts = userInfo.split(":"); + if (parts.length == 2) { + httpGet.addHeader(BasicScheme.authenticate( + new UsernamePasswordCredentials(parts[0], parts[1]), + "UTF-8", false)); + } + } HttpResponse response = httpClient.execute(httpGet); HttpEntity httpEntity = response.getEntity(); int responseCode = response.getStatusLine().getStatusCode(); From bed57245b3ce454e714b5d5b22c8167ad7b986c8 Mon Sep 17 00:00:00 2001 From: daniel oeh Date: Wed, 12 Mar 2014 17:11:46 +0100 Subject: [PATCH 02/23] Added single purpose app integration Antennapod will now import subscriptions from AntennaPodSP (https://github.com/danieloeh/AntennaPodSP) forks when launched --- AndroidManifest.xml | 5 ++ res/values/strings.xml | 4 ++ src/de/danoeh/antennapod/PodcastApp.java | 3 + .../antennapod/receiver/SPAReceiver.java | 55 ++++++++++++++++ src/de/danoeh/antennapod/spa/SPAUtil.java | 66 +++++++++++++++++++ 5 files changed, 133 insertions(+) create mode 100644 src/de/danoeh/antennapod/receiver/SPAReceiver.java create mode 100644 src/de/danoeh/antennapod/spa/SPAUtil.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index cfc8aeca3..772c168e1 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -465,6 +465,11 @@ android:scheme="package"/> + + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 43bedadee..4ea68d980 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -350,4 +350,8 @@ Episode is in the queue Number of new episodes Number of episodes you have started listening to + + + + Importing subscriptions from single-purpose apps… diff --git a/src/de/danoeh/antennapod/PodcastApp.java b/src/de/danoeh/antennapod/PodcastApp.java index 2141f71e8..4c4766327 100644 --- a/src/de/danoeh/antennapod/PodcastApp.java +++ b/src/de/danoeh/antennapod/PodcastApp.java @@ -7,6 +7,7 @@ import de.danoeh.antennapod.asynctask.ImageLoader; import de.danoeh.antennapod.feed.EventDistributor; import de.danoeh.antennapod.preferences.PlaybackPreferences; import de.danoeh.antennapod.preferences.UserPreferences; +import de.danoeh.antennapod.spa.SPAUtil; /** Main application class. */ public class PodcastApp extends Application { @@ -31,6 +32,8 @@ public class PodcastApp extends Application { UserPreferences.createInstance(this); PlaybackPreferences.createInstance(this); EventDistributor.getInstance(); + + SPAUtil.sendSPAppsQueryFeedsIntent(this); } @Override diff --git a/src/de/danoeh/antennapod/receiver/SPAReceiver.java b/src/de/danoeh/antennapod/receiver/SPAReceiver.java new file mode 100644 index 000000000..c378f74e1 --- /dev/null +++ b/src/de/danoeh/antennapod/receiver/SPAReceiver.java @@ -0,0 +1,55 @@ +package de.danoeh.antennapod.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import android.widget.Toast; +import de.danoeh.antennapod.AppConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.feed.Feed; +import de.danoeh.antennapod.storage.DownloadRequestException; +import de.danoeh.antennapod.storage.DownloadRequester; +import org.apache.commons.lang3.StringUtils; + +import java.util.Arrays; +import java.util.Date; + +/** + * Receives intents from AntennaPod Single Purpose apps + */ +public class SPAReceiver extends BroadcastReceiver{ + private static final String TAG = "SPAReceiver"; + + public static final String ACTION_SP_APPS_QUERY_FEEDS = "de.danoeh.antennapdsp.intent.SP_APPS_QUERY_FEEDS"; + public static final String ACTION_SP_APPS_QUERY_FEEDS_REPSONSE = "de.danoeh.antennapdsp.intent.SP_APPS_QUERY_FEEDS_RESPONSE"; + public static final String ACTION_SP_APPS_QUERY_FEEDS_REPSONSE_FEEDS_EXTRA = "feeds"; + + @Override + public void onReceive(Context context, Intent intent) { + if (StringUtils.equals(intent.getAction(), ACTION_SP_APPS_QUERY_FEEDS_REPSONSE)) { + if (AppConfig.DEBUG) Log.d(TAG, "Received SP_APPS_QUERY_RESPONSE"); + if (intent.hasExtra(ACTION_SP_APPS_QUERY_FEEDS_REPSONSE_FEEDS_EXTRA)) { + String[] feedUrls = intent.getStringArrayExtra(ACTION_SP_APPS_QUERY_FEEDS_REPSONSE_FEEDS_EXTRA); + if (feedUrls != null) { + if (AppConfig.DEBUG) Log.d(TAG, "Received feeds list: " + Arrays.toString(feedUrls)); + for (String url : feedUrls) { + Feed f = new Feed(url, new Date()); + try { + DownloadRequester.getInstance().downloadFeed(context, f); + } catch (DownloadRequestException e) { + Log.e(TAG, "Error while trying to add feed " + url); + e.printStackTrace(); + } + } + Toast.makeText(context, R.string.sp_apps_importing_feeds_msg, Toast.LENGTH_LONG).show(); + + } else { + Log.e(TAG, "Received invalid SP_APPS_QUERY_REPSONSE: extra was null"); + } + } else { + Log.e(TAG, "Received invalid SP_APPS_QUERY_RESPONSE: Contains no extra"); + } + } + } +} diff --git a/src/de/danoeh/antennapod/spa/SPAUtil.java b/src/de/danoeh/antennapod/spa/SPAUtil.java new file mode 100644 index 000000000..7720f7064 --- /dev/null +++ b/src/de/danoeh/antennapod/spa/SPAUtil.java @@ -0,0 +1,66 @@ +package de.danoeh.antennapod.spa; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.util.Log; +import de.danoeh.antennapod.AppConfig; +import de.danoeh.antennapod.receiver.SPAReceiver; + +/** + * Provides methods related to AntennaPodSP (https://github.com/danieloeh/AntennaPodSP) + */ +public class SPAUtil { + private static final String TAG = "SPAUtil"; + + private static final String PREF_HAS_QUERIED_SP_APPS = "prefSPAUtil.hasQueriedSPApps"; + + private SPAUtil() { + } + + + /** + * Sends an ACTION_SP_APPS_QUERY_FEEDS intent to all AntennaPod Single Purpose apps. + * The receiving single purpose apps will then send their feeds back to AntennaPod via an + * ACTION_SP_APPS_QUERY_FEEDS_RESPONSE intent. + * This intent will only be sent once. + * + * @return True if an intent was sent, false otherwise (for example if the intent has already been + * sent before. + */ + public static synchronized boolean sendSPAppsQueryFeedsIntent(Context context) { + if (context == null) throw new IllegalArgumentException("context = null"); + final Context appContext = context.getApplicationContext(); + if (appContext == null) { + Log.wtf(TAG, "Unable to get application context"); + return false; + } + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(appContext); + if (!prefs.getBoolean(PREF_HAS_QUERIED_SP_APPS, false)) { + appContext.sendBroadcast(new Intent(SPAReceiver.ACTION_SP_APPS_QUERY_FEEDS)); + if (AppConfig.DEBUG) Log.d(TAG, "Sending SP_APPS_QUERY_FEEDS intent"); + + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(PREF_HAS_QUERIED_SP_APPS, true); + editor.commit(); + + return true; + } else { + return false; + } + } + + /** + * Resets all preferences created by this class. Should only be used for debug purposes. + */ + public static void resetSPAPreferences(Context c) { + if (AppConfig.DEBUG) { + if (c == null) throw new IllegalArgumentException("c = null"); + SharedPreferences.Editor editor = PreferenceManager + .getDefaultSharedPreferences(c.getApplicationContext()).edit(); + editor.putBoolean(PREF_HAS_QUERIED_SP_APPS, false); + editor.commit(); + } + } +} From 6cb3a30307910b8f043af559607ae73a62e5a623 Mon Sep 17 00:00:00 2001 From: daniel oeh Date: Mon, 17 Mar 2014 01:26:08 +0100 Subject: [PATCH 03/23] Added username and password to preferences --- .../antennapod/feed/FeedPreferences.java | 23 ++++++++++++++-- .../danoeh/antennapod/storage/DBReader.java | 4 ++- .../antennapod/storage/PodDBAdapter.java | 27 ++++++++++++++++--- 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/src/de/danoeh/antennapod/feed/FeedPreferences.java b/src/de/danoeh/antennapod/feed/FeedPreferences.java index a63c1d52b..cfbc1cee2 100644 --- a/src/de/danoeh/antennapod/feed/FeedPreferences.java +++ b/src/de/danoeh/antennapod/feed/FeedPreferences.java @@ -1,7 +1,6 @@ package de.danoeh.antennapod.feed; import android.content.Context; - import de.danoeh.antennapod.storage.DBWriter; /** @@ -11,10 +10,14 @@ public class FeedPreferences { private long feedID; private boolean autoDownload; + private String username; + private String password; - public FeedPreferences(long feedID, boolean autoDownload) { + public FeedPreferences(long feedID, boolean autoDownload, String username, String password) { this.feedID = feedID; this.autoDownload = autoDownload; + this.username = username; + this.password = password; } public long getFeedID() { @@ -36,4 +39,20 @@ public class FeedPreferences { public void save(Context context) { DBWriter.setFeedPreferences(context, this); } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } } diff --git a/src/de/danoeh/antennapod/storage/DBReader.java b/src/de/danoeh/antennapod/storage/DBReader.java index ccbf6646f..c3e9ab9a2 100644 --- a/src/de/danoeh/antennapod/storage/DBReader.java +++ b/src/de/danoeh/antennapod/storage/DBReader.java @@ -344,7 +344,9 @@ public final class DBReader { } FeedPreferences preferences = new FeedPreferences(cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_ID), - cursor.getInt(PodDBAdapter.IDX_FEED_SEL_PREFERENCES_AUTO_DOWNLOAD) > 0); + cursor.getInt(PodDBAdapter.IDX_FEED_SEL_PREFERENCES_AUTO_DOWNLOAD) > 0, + cursor.getString(PodDBAdapter.IDX_FEED_SEL_PREFERENCES_USERNAME), + cursor.getString(PodDBAdapter.IDX_FEED_SEL_PREFERENCES_PASSWORD)); feed.setPreferences(preferences); return feed; diff --git a/src/de/danoeh/antennapod/storage/PodDBAdapter.java b/src/de/danoeh/antennapod/storage/PodDBAdapter.java index b44883744..46b9f1af3 100644 --- a/src/de/danoeh/antennapod/storage/PodDBAdapter.java +++ b/src/de/danoeh/antennapod/storage/PodDBAdapter.java @@ -25,7 +25,7 @@ import java.util.List; */ public class PodDBAdapter { private static final String TAG = "PodDBAdapter"; - private static final int DATABASE_VERSION = 11; + private static final int DATABASE_VERSION = 12; public static final String DATABASE_NAME = "Antennapod.db"; /** @@ -56,6 +56,8 @@ public class PodDBAdapter { public static final int KEY_TYPE_INDEX = 12; public static final int KEY_FEED_IDENTIFIER_INDEX = 13; public static final int KEY_FEED_FLATTR_STATUS_INDEX = 14; + public static final int KEY_FEED_USERNAME_INDEX = 15; + public static final int KEY_FEED_PASSWORD_INDEX = 16; // ----------- FeedItem indices public static final int KEY_CONTENT_ENCODED_INDEX = 2; public static final int KEY_PUBDATE_INDEX = 3; @@ -131,6 +133,8 @@ public class PodDBAdapter { public static final String KEY_PLAYBACK_COMPLETION_DATE = "playback_completion_date"; public static final String KEY_AUTO_DOWNLOAD = "auto_download"; public static final String KEY_PLAYED_DURATION = "played_duration"; + public static final String KEY_USERNAME = "username"; + public static final String KEY_PASSWORD = "password"; // Table names public static final String TABLE_NAME_FEEDS = "Feeds"; @@ -153,7 +157,9 @@ public class PodDBAdapter { + KEY_LASTUPDATE + " TEXT," + KEY_LANGUAGE + " TEXT," + KEY_AUTHOR + " TEXT," + KEY_IMAGE + " INTEGER," + KEY_TYPE + " TEXT," + KEY_FEED_IDENTIFIER + " TEXT," + KEY_AUTO_DOWNLOAD + " INTEGER DEFAULT 1," - + KEY_FLATTR_STATUS + " INTEGER)"; + + KEY_FLATTR_STATUS + " INTEGER," + + KEY_USERNAME + " TEXT," + + KEY_PASSWORD + " TEXT)"; private static final String CREATE_TABLE_FEED_ITEMS = "CREATE TABLE " + TABLE_NAME_FEED_ITEMS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE @@ -217,7 +223,9 @@ public class PodDBAdapter { TABLE_NAME_FEEDS + "." + KEY_TYPE, TABLE_NAME_FEEDS + "." + KEY_FEED_IDENTIFIER, TABLE_NAME_FEEDS + "." + KEY_AUTO_DOWNLOAD, - TABLE_NAME_FEEDS + "." + KEY_FLATTR_STATUS + TABLE_NAME_FEEDS + "." + KEY_FLATTR_STATUS, + TABLE_NAME_FEEDS + "." + KEY_USERNAME, + TABLE_NAME_FEEDS + "." + KEY_PASSWORD }; // column indices for FEED_SEL_STD @@ -237,6 +245,9 @@ public class PodDBAdapter { public static final int IDX_FEED_SEL_STD_FEED_IDENTIFIER = 13; public static final int IDX_FEED_SEL_PREFERENCES_AUTO_DOWNLOAD = 14; public static final int IDX_FEED_SEL_STD_FLATTR_STATUS = 15; + public static final int IDX_FEED_SEL_PREFERENCES_USERNAME = 16; + public static final int IDX_FEED_SEL_PREFERENCES_PASSWORD = 17; + /** @@ -383,6 +394,8 @@ public class PodDBAdapter { } ContentValues values = new ContentValues(); values.put(KEY_AUTO_DOWNLOAD, prefs.getAutoDownload()); + values.put(KEY_USERNAME, prefs.getUsername()); + values.put(KEY_PASSWORD, prefs.getPassword()); db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(prefs.getFeedID())}); } @@ -1308,6 +1321,14 @@ public class PodDBAdapter { + " ADD COLUMN " + KEY_PLAYED_DURATION + " INTEGER"); } + if (oldVersion <= 11) { + db.execSQL("ALTER TABLE " + TABLE_NAME_FEEDS + + " ADD COLUMN " + KEY_USERNAME + + " TEXT"); + db.execSQL("ALTER TABLE " + TABLE_NAME_FEEDS + + " ADD COLUMN " + KEY_PASSWORD + + " TEXT"); + } } } } From 3b5e83c74f17a318162f39a2004fe33207de1f10 Mon Sep 17 00:00:00 2001 From: daniel oeh Date: Mon, 17 Mar 2014 13:02:37 +0100 Subject: [PATCH 04/23] Added authentication support to DownloadRequester and HttpDownloader --- res/values/strings.xml | 1 + .../service/download/DownloadRequest.java | 26 ++++++- .../service/download/HttpDownloader.java | 19 ++++- .../antennapod/storage/DownloadRequester.java | 77 +++++++++++++------ .../danoeh/antennapod/util/DownloadError.java | 3 +- 5 files changed, 96 insertions(+), 30 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 4ea68d980..94d778b25 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -100,6 +100,7 @@ Unsupported Feed type Connection error Unknown host + Authentication error Cancel all downloads Download cancelled Downloads completed diff --git a/src/de/danoeh/antennapod/service/download/DownloadRequest.java b/src/de/danoeh/antennapod/service/download/DownloadRequest.java index 1f4e32e1b..320936cf1 100644 --- a/src/de/danoeh/antennapod/service/download/DownloadRequest.java +++ b/src/de/danoeh/antennapod/service/download/DownloadRequest.java @@ -8,6 +8,8 @@ public class DownloadRequest implements Parcelable { private final String destination; private final String source; private final String title; + private final String username; + private final String password; private final long feedfileId; private final int feedfileType; @@ -17,7 +19,7 @@ public class DownloadRequest implements Parcelable { protected int statusMsg; public DownloadRequest(String destination, String source, String title, - long feedfileId, int feedfileType) { + long feedfileId, int feedfileType, String username, String password) { if (destination == null) { throw new IllegalArgumentException("Destination must not be null"); } @@ -33,14 +35,28 @@ public class DownloadRequest implements Parcelable { this.title = title; this.feedfileId = feedfileId; this.feedfileType = feedfileType; + this.username = username; + this.password = password; } + public DownloadRequest(String destination, String source, String title, + long feedfileId, int feedfileType) { + this(destination, source, title, feedfileId, feedfileType, null, null); + } + private DownloadRequest(Parcel in) { destination = in.readString(); source = in.readString(); title = in.readString(); feedfileId = in.readLong(); feedfileType = in.readInt(); + if (in.dataAvail() > 0) { + username = in.readString(); + password = in.readString(); + } else { + username = null; + password = null; + } } @Override @@ -174,4 +190,12 @@ public class DownloadRequest implements Parcelable { public void setStatusMsg(int statusMsg) { this.statusMsg = statusMsg; } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } } diff --git a/src/de/danoeh/antennapod/service/download/HttpDownloader.java b/src/de/danoeh/antennapod/service/download/HttpDownloader.java index 1ffea19d5..4b6455343 100644 --- a/src/de/danoeh/antennapod/service/download/HttpDownloader.java +++ b/src/de/danoeh/antennapod/service/download/HttpDownloader.java @@ -8,6 +8,7 @@ import de.danoeh.antennapod.R; import de.danoeh.antennapod.util.DownloadError; import de.danoeh.antennapod.util.StorageUtils; import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; @@ -54,6 +55,9 @@ public class HttpDownloader extends Downloader { new UsernamePasswordCredentials(parts[0], parts[1]), "UTF-8", false)); } + } else if (StringUtils.isEmpty(request.getUsername()) && request.getPassword() != null) { + httpGet.addHeader(BasicScheme.authenticate(new UsernamePasswordCredentials(request.getUsername(), + request.getPassword()), "UTF-8", false)); } HttpResponse response = httpClient.execute(httpGet); HttpEntity httpEntity = response.getEntity(); @@ -67,8 +71,16 @@ public class HttpDownloader extends Downloader { Log.d(TAG, "Response code is " + responseCode); if (responseCode != HttpURLConnection.HTTP_OK || httpEntity == null) { - onFail(DownloadError.ERROR_HTTP_DATA_ERROR, - String.valueOf(responseCode)); + final DownloadError error; + final String details; + if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) { + error = DownloadError.ERROR_UNAUTHORIZED; + details = String.valueOf(responseCode); + } else { + error = DownloadError.ERROR_HTTP_DATA_ERROR; + details = String.valueOf(responseCode); + } + onFail(error, details); return; } @@ -132,7 +144,8 @@ public class HttpDownloader extends Downloader { "Download completed but size: " + request.getSoFar() + " does not equal expected size " + - request.getSize()); + request.getSize() + ); return; } onSuccess(); diff --git a/src/de/danoeh/antennapod/storage/DownloadRequester.java b/src/de/danoeh/antennapod/storage/DownloadRequester.java index 013162f0c..ba112b662 100644 --- a/src/de/danoeh/antennapod/storage/DownloadRequester.java +++ b/src/de/danoeh/antennapod/storage/DownloadRequester.java @@ -1,28 +1,28 @@ package de.danoeh.antennapod.storage; -import java.io.File; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import org.apache.commons.io.FilenameUtils; -import org.apache.commons.lang3.StringUtils; - import android.content.Context; import android.content.Intent; import android.util.Log; import android.webkit.URLUtil; import de.danoeh.antennapod.AppConfig; -import de.danoeh.antennapod.feed.EventDistributor; -import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.feed.FeedFile; -import de.danoeh.antennapod.feed.FeedImage; -import de.danoeh.antennapod.feed.FeedMedia; +import de.danoeh.antennapod.feed.*; import de.danoeh.antennapod.preferences.UserPreferences; import de.danoeh.antennapod.service.download.DownloadRequest; import de.danoeh.antennapod.service.download.DownloadService; import de.danoeh.antennapod.util.FileNameGenerator; import de.danoeh.antennapod.util.URLChecker; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.StringUtils; +import java.io.File; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + + +/** + * Sends download requests to the DownloadService. This class should always be used for starting downloads, + * otherwise they won't work correctly. + */ public class DownloadRequester { private static final String TAG = "DownloadRequester"; @@ -45,8 +45,34 @@ public class DownloadRequester { return downloader; } + /** + * Starts a new download with the given DownloadRequest. This method should only + * be used from outside classes if the DownloadRequest was created by the DownloadService to + * ensure that the data is valid. Use downloadFeed(), downloadImage() or downloadMedia() instead. + * + * @param context Context object for starting the DownloadService + * @param request The DownloadRequest. If another DownloadRequest with the same source URL is already stored, this method + * call will return false. + * @return True if the download request was accepted, false otherwise. + */ + public boolean download(Context context, DownloadRequest request) { + if (context == null) throw new IllegalArgumentException("context = null"); + if (request == null) throw new IllegalArgumentException("request = null"); + if (downloads.containsKey(request.getSource())) { + if (AppConfig.DEBUG) Log.i(TAG, "DownloadRequest is already stored."); + return false; + } + downloads.put(request.getSource(), request); + + Intent launchIntent = new Intent(context, DownloadService.class); + launchIntent.putExtra(DownloadService.EXTRA_REQUEST, request); + context.startService(launchIntent); + EventDistributor.getInstance().sendDownloadQueuedBroadcast(); + return true; + } + private void download(Context context, FeedFile item, File dest, - boolean overwriteIfExists) { + boolean overwriteIfExists, String username, String password) { if (!isDownloadingFile(item)) { if (!isFilenameAvailable(dest.toString()) || dest.exists()) { if (AppConfig.DEBUG) @@ -88,14 +114,9 @@ public class DownloadRequester { DownloadRequest request = new DownloadRequest(dest.toString(), item.getDownload_url(), item.getHumanReadableIdentifier(), - item.getId(), item.getTypeAsInt()); + item.getId(), item.getTypeAsInt(), username, password); - downloads.put(request.getSource(), request); - - Intent launchIntent = new Intent(context, DownloadService.class); - launchIntent.putExtra(DownloadService.EXTRA_REQUEST, request); - context.startService(launchIntent); - EventDistributor.getInstance().sendDownloadQueuedBroadcast(); + download(context, request); } else { Log.e(TAG, "URL " + item.getDownload_url() + " is already being downloaded"); @@ -124,8 +145,11 @@ public class DownloadRequester { public void downloadFeed(Context context, Feed feed) throws DownloadRequestException { if (feedFileValid(feed)) { + String username = (feed.getPreferences() != null) ? feed.getPreferences().getUsername() : null; + String password = (feed.getPreferences() != null) ? feed.getPreferences().getPassword() : null; + download(context, feed, new File(getFeedfilePath(context), - getFeedfileName(feed)), true); + getFeedfileName(feed)), true, username, password); } } @@ -133,7 +157,7 @@ public class DownloadRequester { throws DownloadRequestException { if (feedFileValid(image)) { download(context, image, new File(getImagefilePath(context), - getImagefileName(image)), true); + getImagefileName(image)), true, null, null); } } @@ -142,7 +166,8 @@ public class DownloadRequester { if (feedFileValid(feedmedia)) { download(context, feedmedia, new File(getMediafilePath(context, feedmedia), - getMediafilename(feedmedia)), false); + getMediafilename(feedmedia)), false, null, null + ); } } @@ -278,7 +303,8 @@ public class DownloadRequester { context, MEDIA_DOWNLOADPATH + FileNameGenerator.generateFileName(media.getItem() - .getFeed().getTitle()) + "/"); + .getFeed().getTitle()) + "/" + ); return externalStorage.toString(); } @@ -305,7 +331,8 @@ public class DownloadRequester { } String URLBaseFilename = URLUtil.guessFileName(media.getDownload_url(), - null, media.getMime_type());; + null, media.getMime_type()); + ; if (titleBaseFilename != "") { // Append extension diff --git a/src/de/danoeh/antennapod/util/DownloadError.java b/src/de/danoeh/antennapod/util/DownloadError.java index f7a5c23fe..1a64991a6 100644 --- a/src/de/danoeh/antennapod/util/DownloadError.java +++ b/src/de/danoeh/antennapod/util/DownloadError.java @@ -18,7 +18,8 @@ public enum DownloadError { ERROR_NOT_ENOUGH_SPACE(10, R.string.download_error_insufficient_space), ERROR_UNKNOWN_HOST(11, R.string.download_error_unknown_host), ERROR_REQUEST_ERROR(12, R.string.download_error_request_error), - ERROR_DB_ACCESS_ERROR(13, R.string.download_error_db_access); + ERROR_DB_ACCESS_ERROR(13, R.string.download_error_db_access), + ERROR_UNAUTHORIZED(14, R.string.download_error_unauthorized); private final int code; private final int resId; From d8a9d68bf84ac1ec7f8f176c70886b3ed9dfabc5 Mon Sep 17 00:00:00 2001 From: daniel oeh Date: Mon, 17 Mar 2014 13:31:33 +0100 Subject: [PATCH 05/23] Added authentication notification --- .../ic_stat_authentication.png | Bin 0 -> 467 bytes res/drawable-hdpi/ic_stat_authentication.png | Bin 0 -> 648 bytes .../ic_stat_authentication.png | Bin 0 -> 293 bytes res/drawable-mdpi/ic_stat_authentication.png | Bin 0 -> 460 bytes .../ic_stat_authentication.png | Bin 0 -> 529 bytes res/drawable-xhdpi/ic_stat_authentication.png | Bin 0 -> 882 bytes .../ic_stat_authentication.png | Bin 0 -> 1266 bytes res/values/strings.xml | 2 + .../service/download/DownloadService.java | 100 ++++++++++++------ 9 files changed, 68 insertions(+), 34 deletions(-) create mode 100755 res/drawable-hdpi-v11/ic_stat_authentication.png create mode 100755 res/drawable-hdpi/ic_stat_authentication.png create mode 100755 res/drawable-mdpi-v11/ic_stat_authentication.png create mode 100755 res/drawable-mdpi/ic_stat_authentication.png create mode 100755 res/drawable-xhdpi-v11/ic_stat_authentication.png create mode 100755 res/drawable-xhdpi/ic_stat_authentication.png create mode 100755 res/drawable-xxhdpi/ic_stat_authentication.png diff --git a/res/drawable-hdpi-v11/ic_stat_authentication.png b/res/drawable-hdpi-v11/ic_stat_authentication.png new file mode 100755 index 0000000000000000000000000000000000000000..ad148cc6b8e3fa55517dca32db0b0fa322053c7b GIT binary patch literal 467 zcmV;^0WAKBP)}i77qGt8Jc6%qwQz-?c%@+B8(16&hqYC(7F)X`2-?_~q!i>d zzbj^iyCs{Qy_qC(%Lj5ntNZ2!VsPsw|ur`iyje*e4A#QMj zZDH^AYEXIF*8sj=ytJ-T)BE?XK-E<$U;83ai}p1^eJ#fRhiKm)xRnpnz6j7<+P93o zVpO&7c4nXwZnXlG_J{dDfj$|3PR$2Ypjx;U?!@gZ5G=GvV`|wyz(QkT^I$8h#q8esxU;jfAG14~%0hyz0eL|a zYfC!|C7lXNKm#dKm}~R)vBbQWec^S!1BY4mGUvxVjOQ)1&_e&HD2nx_XiT$Qi|GdYwb_1_4Qui zMNzD$X-Y~dLd2{)L&U8%yz3-MsH&>EFJQa9XpHf@vnUg8ecxZY5Rj(n zmo^~J^R4bI&-0gvNRw=gxp^S~5r1_qoR($z!dm;NuIueIO;4u*%CcPR1NhorkW!K{ zX8cUse^WNN-3Ks~QqrszLYxFau-XT(F(qVto;Bxu=fZH$^Sr@eup)%`JS#vc^*#*4 zBSmO+gT>0>F?l zMqNuOXRE6RWOr2Gc#{!iwz?*1RM)dOj-La-2LK?(7zrUL&-3}z)rxa&0DxKxAt=kT zy}3c8l#c=6H2{1(19>|aK&vQZtMDLQ;HWv^(y+t6H6ru(a@}+nyEWy>R`U1%h(=5z< zv#{G}{C3dB5Ob_UEigiH?eKd%OXmgsYlgp{7=FkH2D!uA(dLinp^I4pc||7&7)H&# z=)AyZ0vYE3^Qf^KowrT^%N$@GHO|83Ej%QK!}d}@*gCcOA-f!4ac0=Dta5t00000NkvXXu0mjfCrW<3 literal 0 HcmV?d00001 diff --git a/res/drawable-mdpi/ic_stat_authentication.png b/res/drawable-mdpi/ic_stat_authentication.png new file mode 100755 index 0000000000000000000000000000000000000000..cadfb96438cbe1da952426cc5465fedaf52fea0e GIT binary patch literal 460 zcmV;-0WX1^@s6IQ*`u0004#Nkl(3qF-DY9PDTYOrMJwC z5W=I~Rn5#_nHfW3jGsqW2_c+0=MZBI0P39ck#lbAoWl@iM&I}M_X?bI8|NHmvl$kP z#aT+}x8Wfvr3Yhz57t^(Yr)L0)?N$^XKU?72r=C&@ZLWi1XpA&rJU>)0H}%Rg)s(W zan2#c0XstIyv?d}D5ttb{=hM+9J8G@KZgE+1qb5~VRmPadZQHKfw*6_0 zLErapZX_5`N_|mEfr!p;D|p#7&AC$Q`>h237{34kyh=Y=gzlpN0000{LV{?{1FcXNl;qRLMj&nMrTADAx!&~MLw z#Wv4DEGI&wTZ=#za0o6r}0H`KH z{F3ZvVhs9orFw{DFgXBlTXx6HG=Q>$m$e2&(ZFBq>I0(aAL6#p_*8bcCf)o63x?gx T0f}Xj00000NkvXXu0mjf8{6{w literal 0 HcmV?d00001 diff --git a/res/drawable-xhdpi/ic_stat_authentication.png b/res/drawable-xhdpi/ic_stat_authentication.png new file mode 100755 index 0000000000000000000000000000000000000000..4adfb636c6898e3d9da549e41b717d795fd3e232 GIT binary patch literal 882 zcmV-&1C9KNP)+5}>pzf-cMpPu9K=J89s=Wk;2&W zS~A8^mSvRZIeG7?X_^OhU4J|n3{F4K@OSIFe(anhW6U#-Al6#SvW&{Iq!7ZlS(d&0 zT*9lW+M5-8kezeXG|jEiXmsW{;TNi^daSi3V+<*!?klCh7;{A_b=?^A^e~9YWb)}k z;Y4)Gd;g=>nzYuWl)4W9ueNKHQXfUQ_-wDW)|BV@p7;LZBB#HUWf?^PV+?d_hQr~S zQtJ0uGtYHhf3XO_q}{|h_x`LpW6V#70a$Av4u`|FUI5OypIhUnqA0%X)|6#=UTghG zDMf9^ciQhe=U(pxptZi&ZklD;Q}6vJ@g!wgp0BEEyBj{wMNwSt1)!9AAf+TJB`Kvy zN*RAP_q_Lay!Vf+wflSV*(Dp2#vMk@~JAv!-PSg$P zx?7*7=@YH>E4=`;*6+;=NYixx*fDy6RW21t@5 zLBtPZlY@NN1e|lX5%FwqfWcsJT1xqK_e6cIl%lGtc6#4~=@iZ);`fNS-!QHb@jvT% z`-n(VN-BzCF^JJ) zdlqPW5)0jlsNrz9CWLqm0Nw_G%R-1Z0bo7ukH_PejsmgpB#t5gSOTJ z`Tqkk{bVoJB#w0_4%75bTmyj5r~YHTIq$=zqV+bcCzrh$p8QV07*qo IM6N<$f~20SUH||9 literal 0 HcmV?d00001 diff --git a/res/drawable-xxhdpi/ic_stat_authentication.png b/res/drawable-xxhdpi/ic_stat_authentication.png new file mode 100755 index 0000000000000000000000000000000000000000..b274bb60f9fea67e7606f89cc1af0d0238aeb706 GIT binary patch literal 1266 zcmV_)=ea!lB>`(HFvE7x5HPjF-%hOtP7oOlFe# z1GFgk#ut@ZDyZOrAcEkJ!d=^I<)p_6^6+|j&sld5^1#Cko0;VK%r}`$ zTCGNn8Z~Ovs8ORvjT%i`Ns=t5Y1+xM?2|0ZzSmmsXsvg()>~PYeXX@tS(dHEaeU9L zvkqj8Sx`znn`PM-S(f#rlqQa2!x%G^(t$bWMhIb~ltyd4r?nPw96vbI@KVYZt@Y

r~(F=?7^N+}=u z-@0?t5FQr`LoL7bq!*I|R`{N*@C^AZ^i)otPKh5wf zNs`>C2)Nztc0UZm@Jtwn*XjZlf9pehW{g>|9=^ZZoGn@Ln_(EfUVIQcoz96M2;K_A zus14j8BqUrdcit?l=2x$>8>`xXkH^iW zl%6{b0Hw5N?Uh3aaV7`?69hqi-bz3wf>P=a#@L~Q7tZV10=QUp5kknIpT80mf6M!X zklXrojz>}Swk?1QHUdp*#@LHi0D>Ukd7xpB3pflkiK8fb+zJ4t^tr%`o6#C zIL;kb0363zB!pa_7DR=$mGHTI-|zaqZwMhKZ_8X9Q)hg|IS!&IdcrC|5CluU@86g- z;JEQ+$1%qHoOAnMXTI;hKQ4eR3n8vXQDpZ!x!rCb!x(>KJ8YQ=Aw~#sRS02cC(7lz z?h?j$yKWGS@t`gC>x}PL;u58F#VQ1Z@DYsh&v~e#Cka?Ke)u>c#KduIAHs2*`!L3z zjtejjuny4FAX=@~0s!E7jPWm3fvb!VIzR~R*9G)_-^6iz$=bhjQI6vrhY-GmG5*r` z{obe*MhNX8gfsx)l+5$Tgq}nQy^1k@7ee>}Lg-C|(Avhv z#-ppNt9KU1_cw(dgpff9<&bqC>{sHj>Q`c`;#M29MX+f-6NeUra2Z0ll?Sb=NU_g3 zpHGP5ijVSKLyQVN{ESR(cRH+G*Ij`SUa3=s9ELGA-Q8(Yxvske0Jwq>G7!Sb@Wo@4 z()rq*9LHG%06dV}b={|q+%r)WA>d$93Y2)h z0Wikrk2HYmx(|Dvx8-@B@jTA}00!-m0BE)Td3SPM_ean3c83iB_+i%h8Z~Ovs8ORv cjb^F80TV(GaGV0X)Bpeg07*qoM6N<$f_%nd_y7O^ literal 0 HcmV?d00001 diff --git a/res/values/strings.xml b/res/values/strings.xml index 94d778b25..376320ad2 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -116,6 +116,8 @@ Media file Image An error occurred when trying to download the file:\u0020 + Authentication required + The resource you requested requires a username and a password Error! diff --git a/src/de/danoeh/antennapod/service/download/DownloadService.java b/src/de/danoeh/antennapod/service/download/DownloadService.java index c27b4d4fe..962fce747 100644 --- a/src/de/danoeh/antennapod/service/download/DownloadService.java +++ b/src/de/danoeh/antennapod/service/download/DownloadService.java @@ -1,20 +1,5 @@ package de.danoeh.antennapod.service.download; -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.List; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicInteger; - -import javax.xml.parsers.ParserConfigurationException; - -import android.media.MediaMetadataRetriever; -import de.danoeh.antennapod.storage.*; -import org.xml.sax.SAXException; - import android.annotation.SuppressLint; import android.app.Notification; import android.app.NotificationManager; @@ -26,7 +11,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.graphics.Bitmap; import android.graphics.BitmapFactory; -import android.os.AsyncTask; +import android.media.MediaMetadataRetriever; import android.os.Binder; import android.os.Handler; import android.os.IBinder; @@ -37,16 +22,25 @@ import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.DownloadActivity; import de.danoeh.antennapod.activity.DownloadLogActivity; -import de.danoeh.antennapod.feed.EventDistributor; -import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.feed.FeedImage; -import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.feed.FeedMedia; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.feed.*; +import de.danoeh.antennapod.storage.*; import de.danoeh.antennapod.syndication.handler.FeedHandler; import de.danoeh.antennapod.syndication.handler.UnsupportedFeedtypeException; import de.danoeh.antennapod.util.ChapterUtils; import de.danoeh.antennapod.util.DownloadError; import de.danoeh.antennapod.util.InvalidFeedException; +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.Collections; +import java.util.Date; +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; /** * Manages the download of feedfiles in the app. Downloads can be enqueued viathe startService intent. @@ -162,9 +156,13 @@ public class DownloadService extends Service { } } else { numberOfDownloads.decrementAndGet(); - if (!successful && !status.isCancelled()) { - Log.e(TAG, "Download failed"); - saveDownloadStatus(status); + if (!status.isCancelled()) { + if (status.getReason() == DownloadError.ERROR_UNAUTHORIZED) { + postAuthenticationNotification(downloader.getDownloadRequest()); + } else { + Log.e(TAG, "Download failed"); + saveDownloadStatus(status); + } } sendDownloadHandledIntent(); queryDownloadsAsync(); @@ -224,7 +222,9 @@ public class DownloadService extends Service { t.setPriority(Thread.MIN_PRIORITY); return t; } - })); + } + ) + ); schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE, new ThreadFactory() { @@ -274,8 +274,9 @@ public class DownloadService extends Service { @SuppressLint("NewApi") private void setupNotificationBuilders() { PendingIntent pIntent = PendingIntent.getActivity(this, 0, new Intent( - this, DownloadActivity.class), - PendingIntent.FLAG_UPDATE_CURRENT); + this, DownloadActivity.class), + PendingIntent.FLAG_UPDATE_CURRENT + ); Bitmap icon = BitmapFactory.decodeResource(getResources(), R.drawable.stat_notify_sync); @@ -284,7 +285,8 @@ public class DownloadService extends Service { notificationBuilder = new Notification.BigTextStyle( new Notification.Builder(this).setOngoing(true) .setContentIntent(pIntent).setLargeIcon(icon) - .setSmallIcon(R.drawable.stat_notify_sync)); + .setSmallIcon(R.drawable.stat_notify_sync) + ); } else { notificationCompatBuilder = new NotificationCompat.Builder(this) .setOngoing(true).setContentIntent(pIntent) @@ -413,12 +415,13 @@ public class DownloadService extends Service { private Downloader getDownloader(DownloadRequest request) { if (URLUtil.isHttpUrl(request.getSource()) - || URLUtil.isHttpsUrl(request.getSource())) { + || URLUtil.isHttpsUrl(request.getSource())) { return new HttpDownloader(request); } Log.e(TAG, "Could not find appropriate downloader for " - + request.getSource()); + + request.getSource() + ); return null; } @@ -495,14 +498,17 @@ public class DownloadService extends Service { .setContentText( String.format( getString(R.string.download_report_content), - successfulDownloads, failedDownloads)) + successfulDownloads, failedDownloads) + ) .setSmallIcon(R.drawable.stat_notify_sync) .setLargeIcon( BitmapFactory.decodeResource(getResources(), - R.drawable.stat_notify_sync)) + R.drawable.stat_notify_sync) + ) .setContentIntent( PendingIntent.getActivity(this, 0, new Intent(this, - DownloadLogActivity.class), 0)) + DownloadLogActivity.class), 0) + ) .setAutoCancel(true).getNotification(); NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); nm.notify(REPORT_ID, notification); @@ -544,6 +550,30 @@ public class DownloadService extends Service { } } + private void postAuthenticationNotification(final DownloadRequest downloadRequest) { + handler.post(new Runnable() { + @Override + public void run() { + final String resourceTitle = (downloadRequest.getTitle() != null) + ? downloadRequest.getTitle() : downloadRequest.getSource(); + + NotificationCompat.Builder builder = new NotificationCompat.Builder(DownloadService.this); + builder.setTicker(getText(R.string.authentication_notification_title)) + .setContentTitle(getText(R.string.authentication_notification_title)) + .setContentText(getText(R.string.authentication_notification_msg)) + .setStyle(new NotificationCompat.BigTextStyle().bigText(getText(R.string.authentication_notification_msg) + + ": " + resourceTitle)) + .setSmallIcon(R.drawable.ic_stat_authentication) + .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.ic_stat_authentication)) + .setAutoCancel(true) + .setContentIntent(PendingIntent.getActivity(getApplicationContext(), 0, new Intent(getApplicationContext(), MainActivity.class), 0)); + Notification n = builder.build(); + NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(downloadRequest.getSource().hashCode(), n); + } + }); + } + /** * Is called whenever a Feed is downloaded */ @@ -633,7 +663,9 @@ public class DownloadService extends Service { .getImage() .getHumanReadableIdentifier(), DownloadError.ERROR_REQUEST_ERROR, - false, e.getMessage())); + false, e.getMessage() + ) + ); } } From 0c2d78157c94a7c92cea5d7548332f55f5d976b2 Mon Sep 17 00:00:00 2001 From: daniel oeh Date: Mon, 17 Mar 2014 17:00:13 +0100 Subject: [PATCH 06/23] Implemented DownloadAuthenticationActivity --- AndroidManifest.xml | 80 +++-- .../download_authentication_activity.xml | 92 +++++ .../download_authentication_activity.xml | 69 ++++ .../DownloadAuthenticationActivity.java | 106 ++++++ src/de/danoeh/antennapod/feed/Feed.java | 17 + .../antennapod/feed/FeedPreferences.java | 31 ++ .../service/download/DownloadRequest.java | 315 +++++++++--------- .../service/download/DownloadService.java | 9 +- .../service/download/HttpDownloader.java | 2 +- src/de/danoeh/antennapod/storage/DBTasks.java | 16 +- .../antennapod/storage/PodDBAdapter.java | 73 ++-- 11 files changed, 589 insertions(+), 221 deletions(-) create mode 100644 res/layout-v14/download_authentication_activity.xml create mode 100644 res/layout/download_authentication_activity.xml create mode 100644 src/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 772c168e1..71bb1a5bd 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,42 +1,42 @@ + package="de.danoeh.antennapod" + android:versionCode="35" + android:versionName="0.9.8.2"> - - - + + + + android:targetSdkVersion="19"/> - + + android:xlargeScreens="true"/> + android:required="false"/> + android:required="false"/> - - + + + android:theme="@style/Theme.AntennaPod.Light"> + android:value="de.danoeh.antennapod.activity.MainActivity"/> + @@ -114,7 +115,7 @@ android:configChanges="orientation|screenSize"> + android:value="de.danoeh.antennapod.activity.MainActivity"/> @@ -131,14 +132,14 @@ android:exported="true"> - + android:value="de.danoeh.antennapod.activity.MainActivity"/> + + android:value="de.danoeh.antennapod.activity.MainActivity"/> @@ -150,6 +151,10 @@ + + @@ -170,14 +175,14 @@ android:label="@string/settings_label"> + android:value="de.danoeh.antennapod.activity.MainActivity"/> + android:value="de.danoeh.antennapod.activity.DownloadActivity"/> + android:value="de.danoeh.antennapod.activity.AddFeedActivity"/> @@ -328,7 +333,7 @@ android:configChanges="keyboardHidden|orientation"> + android:value="de.danoeh.antennapod.activity.MiroGuideMainActivity"/> + android:value="de.danoeh.antennapod.activity.MiroGuideCategoryActivity"/> + android:value="de.danoeh.antennapod.activity.MainActivity"/> @@ -360,28 +365,30 @@ android:label="@string/playback_history_label"> - + android:value="de.danoeh.antennapod.activity.MainActivity"/> + + + android:value="de.danoeh.antennapod.activity.MainActiviy"/> - + android:value="de.danoeh.antennapod.activity.PreferenceActivity"/> + + android:value="de.danoeh.antennapod.activity.PreferenceActivity"/> + + android:value="de.danoeh.antennapod.activity.MainActivity"/> + android:value="de.danoeh.antennapod.activity.AddFeedActivity"/> + android:value="de.danoeh.antennapod.activity.gpoddernet.GpodnetMainActivity"/> + + android:value="de.danoeh.antennapod.activity.gpoddernet.GpodnetMainActivity"/> + android:value="de.danoeh.antennapod.activity.PreferenceActivity"/> diff --git a/res/layout-v14/download_authentication_activity.xml b/res/layout-v14/download_authentication_activity.xml new file mode 100644 index 000000000..c1fe55ceb --- /dev/null +++ b/res/layout-v14/download_authentication_activity.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + +