Merge pull request #728 from mfietz/issues/627+727

Add feed: remember credentials, Basic authentication: try UTF-8 encoding
This commit is contained in:
Tom Hennen 2015-04-27 17:54:28 -04:00
commit 6830549875
10 changed files with 219 additions and 98 deletions

View File

@ -14,9 +14,9 @@ dependencies {
compile 'commons-io:commons-io:2.4'
compile 'com.jayway.android.robotium:robotium-solo:5.2.1'
compile 'org.jsoup:jsoup:1.7.3'
compile 'com.squareup.picasso:picasso:2.4.0'
compile 'com.squareup.okhttp:okhttp:2.2.0'
compile 'com.squareup.okhttp:okhttp-urlconnection:2.2.0'
compile 'com.squareup.picasso:picasso:2.5.2'
compile 'com.squareup.okhttp:okhttp:2.3.0'
compile 'com.squareup.okhttp:okhttp-urlconnection:2.3.0'
compile 'com.squareup.okio:okio:1.2.0'
compile 'de.greenrobot:eventbus:2.4.0'
compile 'com.joanzapata.android:android-iconify:1.0.9'

View File

@ -26,7 +26,6 @@ import java.util.Map;
import javax.xml.parsers.ParserConfigurationException;
import de.danoeh.antennapod.BuildConfig;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedPreferences;
@ -91,12 +90,10 @@ public abstract class OnlineFeedViewActivity extends ActionBarActivity {
getSupportActionBar().setTitle(R.string.add_new_feed_label);
} else {
throw new IllegalArgumentException(
"Activity must be started with feedurl argument!");
throw new IllegalArgumentException("Activity must be started with feedurl argument!");
}
if (BuildConfig.DEBUG)
Log.d(TAG, "Activity was started with url " + feedUrl);
Log.d(TAG, "Activity was started with url " + feedUrl);
setLoadingLayout();
if (savedInstanceState == null) {
startFeedDownload(feedUrl, null, null);
@ -147,7 +144,7 @@ public abstract class OnlineFeedViewActivity extends ActionBarActivity {
@Override
public void run() {
if (BuildConfig.DEBUG) Log.d(TAG, "Download was completed");
Log.d(TAG, "Download was completed");
DownloadStatus status = downloader.getResult();
if (status != null) {
if (!status.isCancelled()) {
@ -164,15 +161,13 @@ public abstract class OnlineFeedViewActivity extends ActionBarActivity {
OnlineFeedViewActivity.this);
if (errorMsg != null
&& status.getReasonDetailed() != null) {
errorMsg += " ("
+ status.getReasonDetailed() + ")";
errorMsg += " (" + status.getReasonDetailed() + ")";
}
showErrorDialog(errorMsg);
}
}
} else {
Log.wtf(TAG,
"DownloadStatus returned by Downloader was null");
Log.wtf(TAG, "DownloadStatus returned by Downloader was null");
finish();
}
}
@ -181,21 +176,18 @@ public abstract class OnlineFeedViewActivity extends ActionBarActivity {
}
private void startFeedDownload(String url, String username, String password) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Starting feed download");
Log.d(TAG, "Starting feed download");
url = URLChecker.prepareURL(url);
feed = new Feed(url, new Date(0));
if (username != null && password != null) {
feed.setPreferences(new FeedPreferences(0, false, username, password));
}
String fileUrl = new File(getExternalCacheDir(),
FileNameGenerator.generateFileName(feed.getDownload_url()))
.toString();
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);
downloader = new HttpDownloader(request);
new Thread() {
@Override
public void run() {
@ -233,8 +225,7 @@ public abstract class OnlineFeedViewActivity extends ActionBarActivity {
"feed must be non-null and downloaded when parseFeed is called");
}
if (BuildConfig.DEBUG)
Log.d(TAG, "Parsing feed");
Log.d(TAG, "Parsing feed");
Thread thread = new Thread() {
@ -258,7 +249,7 @@ public abstract class OnlineFeedViewActivity extends ActionBarActivity {
e.printStackTrace();
reasonDetailed = e.getMessage();
} catch (UnsupportedFeedtypeException e) {
if (BuildConfig.DEBUG) Log.d(TAG, "Unsupported feed type detected");
Log.d(TAG, "Unsupported feed type detected");
if (StringUtils.equalsIgnoreCase("html", e.getRootElement())) {
if (showFeedDiscoveryDialog(new File(feed.getFile_url()), feed.getDownload_url())) {
return;
@ -269,8 +260,7 @@ public abstract class OnlineFeedViewActivity extends ActionBarActivity {
}
} finally {
boolean rc = new File(feed.getFile_url()).delete();
if (BuildConfig.DEBUG)
Log.d(TAG, "Deleted feed source file. Result: " + rc);
Log.d(TAG, "Deleted feed source file. Result: " + rc);
}
if (successful) {
@ -386,7 +376,12 @@ public abstract class OnlineFeedViewActivity extends ActionBarActivity {
String selectedUrl = urls.get(which);
dialog.dismiss();
resetIntent(selectedUrl, titles.get(which));
startFeedDownload(selectedUrl, null, null);
FeedPreferences prefs = feed.getPreferences();
if(prefs != null) {
startFeedDownload(selectedUrl, prefs.getUsername(), prefs.getPassword());
} else {
startFeedDownload(selectedUrl, null, null);
}
}
};

View File

@ -40,9 +40,9 @@ dependencies {
compile 'commons-io:commons-io:2.4'
compile 'com.jayway.android.robotium:robotium-solo:5.2.1'
compile 'org.jsoup:jsoup:1.7.3'
compile 'com.squareup.picasso:picasso:2.4.0'
compile 'com.squareup.okhttp:okhttp:2.2.0'
compile 'com.squareup.okhttp:okhttp-urlconnection:2.2.0'
compile 'com.squareup.picasso:picasso:2.5.2'
compile 'com.squareup.okhttp:okhttp:2.3.0'
compile 'com.squareup.okhttp:okhttp-urlconnection:2.3.0'
compile 'com.squareup.okio:okio:1.2.0'
compile 'com.nineoldandroids:library:2.4.0'
compile 'de.greenrobot:eventbus:2.4.0'

View File

@ -6,8 +6,12 @@ import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import com.squareup.okhttp.Interceptor;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Response;
import com.squareup.picasso.Cache;
import com.squareup.picasso.LruCache;
import com.squareup.picasso.OkHttpDownloader;
@ -22,13 +26,18 @@ import org.apache.commons.lang3.StringUtils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import de.danoeh.antennapod.core.service.download.HttpDownloader;
import de.danoeh.antennapod.core.storage.DBReader;
/**
* Provides access to Picasso instances.
*/
public class PicassoProvider {
private static final String TAG = "PicassoProvider";
private static final boolean DEBUG = false;
@ -56,10 +65,12 @@ public class PicassoProvider {
if (picassoSetup) {
return;
}
OkHttpClient client = new OkHttpClient();
client.interceptors().add(new BasicAuthenticationInterceptor(appContext));
Picasso picasso = new Picasso.Builder(appContext)
.indicatorsEnabled(DEBUG)
.loggingEnabled(DEBUG)
.downloader(new OkHttpDownloader(appContext))
.downloader(new OkHttpDownloader(client))
.addRequestHandler(new MediaRequestHandler(appContext))
.executor(getExecutorService())
.memoryCache(getMemoryCache(appContext))
@ -75,6 +86,48 @@ public class PicassoProvider {
picassoSetup = true;
}
private static class BasicAuthenticationInterceptor implements Interceptor {
private final Context context;
public BasicAuthenticationInterceptor(Context context) {
this.context = context;
}
@Override
public Response intercept(Chain chain) throws IOException {
com.squareup.okhttp.Request request = chain.request();
String url = request.urlString();
String authentication = DBReader.getImageAuthentication(context, url);
if(TextUtils.isEmpty(authentication)) {
Log.d(TAG, "no credentials for '" + url + "'");
return chain.proceed(request);
}
// add authentication
String[] auth = authentication.split(":");
String credentials = HttpDownloader.encodeCredentials(auth[0], auth[1], "ISO-8859-1");
com.squareup.okhttp.Request newRequest = request
.newBuilder()
.addHeader("Authorization", credentials)
.build();
Log.d(TAG, "Basic authentication with ISO-8859-1 encoding");
Response response = chain.proceed(newRequest);
if (!response.isSuccessful() && response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
credentials = HttpDownloader.encodeCredentials(auth[0], auth[1], "UTF-8");
newRequest = request
.newBuilder()
.addHeader("Authorization", credentials)
.build();
Log.d(TAG, "Basic authentication with UTF-8 encoding");
return chain.proceed(newRequest);
} else {
return response;
}
}
}
private static class MediaRequestHandler extends RequestHandler {
final Context context;
@ -90,7 +143,7 @@ public class PicassoProvider {
}
@Override
public Result load(Request data) throws IOException {
public Result load(Request data, int networkPolicy) throws IOException {
Bitmap bitmap = null;
MediaMetadataRetriever mmr = null;
try {
@ -109,13 +162,7 @@ public class PicassoProvider {
}
if (bitmap == null) {
// check for fallback Uri
String fallbackParam = data.uri.getQueryParameter(PicassoImageResource.PARAM_FALLBACK);
if (fallbackParam != null) {
Uri fallback = Uri.parse(fallbackParam);
bitmap = decodeStreamFromFile(data, fallback);
}
Log.wtf(TAG, "THIS SHOULD NEVER EVER HAPPEN!!");
}
return new Result(bitmap, Picasso.LoadedFrom.DISK);

View File

@ -315,10 +315,10 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr
@Override
public Uri getImageUri() {
if (hasItemImageDownloaded()) {
return image.getImageUri();
} else if (hasMedia()) {
if(media.hasEmbeddedPicture()) {
return media.getImageUri();
} else if (hasItemImageDownloaded()) {
return image.getImageUri();
} else if (feed != null) {
return feed.getImageUri();
} else {

View File

@ -2,9 +2,11 @@ package de.danoeh.antennapod.core.feed;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
import java.util.Date;
import java.util.List;
@ -34,6 +36,7 @@ public class FeedMedia extends FeedFile implements Playable {
private String mime_type;
private volatile FeedItem item;
private Date playbackCompletionDate;
private boolean hasEmbeddedPicture;
/* Used for loading item when restoring from parcel. */
private long itemID;
@ -50,6 +53,7 @@ public class FeedMedia extends FeedFile implements Playable {
long size, String mime_type, String file_url, String download_url,
boolean downloaded, Date playbackCompletionDate, int played_duration) {
super(file_url, download_url, downloaded);
checkEmbeddedPicture();
this.id = id;
this.item = item;
this.duration = duration;
@ -61,12 +65,6 @@ public class FeedMedia extends FeedFile implements Playable {
? null : (Date) playbackCompletionDate.clone();
}
public FeedMedia(long id, FeedItem item) {
super();
this.id = id;
this.item = item;
}
@Override
public String getHumanReadableIdentifier() {
if (item != null && item.getTitle() != null) {
@ -227,18 +225,16 @@ public class FeedMedia extends FeedFile implements Playable {
return (this.position > 0);
}
public FeedImage getImage() {
if (item != null) {
return (item.hasItemImageDownloaded()) ? item.getImage() : item.getFeed().getImage();
}
return null;
}
@Override
public int describeContents() {
return 0;
}
public boolean hasEmbeddedPicture() {
Log.d(TAG, "hasEmbeddedPicture() -> " + hasEmbeddedPicture);
return this.hasEmbeddedPicture;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(id);
@ -415,28 +411,46 @@ public class FeedMedia extends FeedFile implements Playable {
@Override
public Uri getImageUri() {
final Uri feedImgUri = getFeedImageUri();
if (localFileAvailable()) {
if (hasEmbeddedPicture) {
Uri.Builder builder = new Uri.Builder();
builder.scheme(SCHEME_MEDIA)
.encodedPath(getLocalMediaUrl());
if (feedImgUri != null) {
builder.appendQueryParameter(PARAM_FALLBACK, feedImgUri.toString());
}
builder.scheme(SCHEME_MEDIA).encodedPath(getLocalMediaUrl());
return builder.build();
} else if (item.hasItemImageDownloaded()) {
return item.getImage().getImageUri();
} else {
return feedImgUri;
return item.getImageUri();
}
}
private Uri getFeedImageUri() {
if (item != null && item.getFeed() != null) {
return item.getFeed().getImageUri();
} else {
return null;
@Override
public void setDownloaded(boolean downloaded) {
super.setDownloaded(downloaded);
checkEmbeddedPicture();
}
@Override
public void setFile_url(String file_url) {
super.setFile_url(file_url);
checkEmbeddedPicture();
}
private void checkEmbeddedPicture() {
Log.d(TAG, "checkEmbeddedPicture()");
if (!localFileAvailable()) {
hasEmbeddedPicture = false;
return;
}
MediaMetadataRetriever mmr = new MediaMetadataRetriever();
try {
mmr.setDataSource(getLocalMediaUrl());
byte[] image = mmr.getEmbeddedPicture();
if(image != null) {
hasEmbeddedPicture = true;
}
else {
hasEmbeddedPicture = false;
}
} catch (Exception e) {
e.printStackTrace();
hasEmbeddedPicture = false;
}
}
}

View File

@ -726,7 +726,12 @@ public class GpodnetService {
Validate.notNull(body);
ByteArrayOutputStream outputStream;
int contentLength = (int) body.contentLength();
int contentLength = 0;
try {
contentLength = (int) body.contentLength();
} catch (IOException ignore) {
// ignore
}
if (contentLength > 0) {
outputStream = new ByteArrayOutputStream(contentLength);
} else {

View File

@ -2,7 +2,6 @@ package de.danoeh.antennapod.core.service.download;
import android.util.Log;
import com.squareup.okhttp.Credentials;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
@ -18,19 +17,20 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.UnknownHostException;
import java.util.Date;
import de.danoeh.antennapod.core.BuildConfig;
import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.core.feed.FeedImage;
import de.danoeh.antennapod.core.util.DownloadError;
import de.danoeh.antennapod.core.util.StorageUtils;
import de.danoeh.antennapod.core.util.URIUtil;
import okio.ByteString;
public class HttpDownloader extends Downloader {
private static final String TAG = "HttpDownloader";
@ -81,11 +81,12 @@ public class HttpDownloader extends Downloader {
if (userInfo != null) {
String[] parts = userInfo.split(":");
if (parts.length == 2) {
String credentials = Credentials.basic(parts[0], parts[1]);
String credentials = encodeCredentials(parts[0], parts[1], "ISO-8859-1");
httpReq.header("Authorization", credentials);
}
} else if (!StringUtils.isEmpty(request.getUsername()) && request.getPassword() != null) {
String credentials = Credentials.basic(request.getUsername(), request.getPassword());
String credentials = encodeCredentials(request.getUsername(), request.getPassword(),
"ISO-8859-1");
httpReq.header("Authorization", credentials);
}
@ -99,13 +100,29 @@ public class HttpDownloader extends Downloader {
Response response = httpClient.newCall(httpReq.build()).execute();
responseBody = response.body();
String contentEncodingHeader = response.header("Content-Encoding");
boolean isGzip = StringUtils.equalsIgnoreCase(contentEncodingHeader, "gzip");
final boolean isGzip = StringUtils.equalsIgnoreCase(contentEncodingHeader, "gzip");
Log.d(TAG, "Response code is " + response.code());
if (BuildConfig.DEBUG)
Log.d(TAG, "Response code is " + response.code());
if(!response.isSuccessful() && response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
Log.d(TAG, "Authorization failed, re-trying with UTF-8 encoding");
if (userInfo != null) {
String[] parts = userInfo.split(":");
if (parts.length == 2) {
String credentials = encodeCredentials(parts[0], parts[1], "UTF-8");
httpReq.header("Authorization", credentials);
}
} else if (!StringUtils.isEmpty(request.getUsername()) && request.getPassword() != null) {
String credentials = encodeCredentials(request.getUsername(), request.getPassword(),
"UTF-8");
httpReq.header("Authorization", credentials);
}
response = httpClient.newCall(httpReq.build()).execute();
responseBody = response.body();
contentEncodingHeader = response.header("Content-Encoding");
isGzip = StringUtils.equalsIgnoreCase(contentEncodingHeader, "gzip");
}
if(!response.isSuccessful() && response.code() == HttpURLConnection.HTTP_NOT_MODIFIED) {
Log.d(TAG, "Feed '" + request.getSource() + "' not modified since last update, Download canceled");
@ -151,22 +168,18 @@ public class HttpDownloader extends Downloader {
out = new RandomAccessFile(destination, "rw");
}
byte[] buffer = new byte[BUFFER_SIZE];
int count = 0;
request.setStatusMsg(R.string.download_running);
if (BuildConfig.DEBUG)
Log.d(TAG, "Getting size of download");
Log.d(TAG, "Getting size of download");
request.setSize(responseBody.contentLength() + request.getSoFar());
if (BuildConfig.DEBUG)
Log.d(TAG, "Size is " + request.getSize());
Log.d(TAG, "Size is " + request.getSize());
if (request.getSize() < 0) {
request.setSize(DownloadStatus.SIZE_UNKNOWN);
}
long freeSpace = StorageUtils.getFreeSpaceAvailable();
if (BuildConfig.DEBUG)
Log.d(TAG, "Free space is " + freeSpace);
Log.d(TAG, "Free space is " + freeSpace);
if (request.getSize() != DownloadStatus.SIZE_UNKNOWN
&& request.getSize() > freeSpace) {
@ -174,8 +187,7 @@ public class HttpDownloader extends Downloader {
return;
}
if (BuildConfig.DEBUG)
Log.d(TAG, "Starting download");
Log.d(TAG, "Starting download");
while (!cancelled
&& (count = connection.read(buffer)) != -1) {
out.write(buffer, 0, count);
@ -226,15 +238,12 @@ public class HttpDownloader extends Downloader {
}
private void onSuccess() {
if (BuildConfig.DEBUG)
Log.d(TAG, "Download was successful");
Log.d(TAG, "Download was successful");
result.setSuccessful();
}
private void onFail(DownloadError reason, String reasonDetailed) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "Download failed");
}
Log.d(TAG, "Download failed");
result.setFailed(reason, reasonDetailed);
if (request.isDeleteOnFailure()) {
cleanup();
@ -242,8 +251,7 @@ public class HttpDownloader extends Downloader {
}
private void onCancelled() {
if (BuildConfig.DEBUG)
Log.d(TAG, "Download was cancelled");
Log.d(TAG, "Download was cancelled");
result.setCancelled();
cleanup();
}
@ -256,14 +264,23 @@ public class HttpDownloader extends Downloader {
File dest = new File(request.getDestination());
if (dest.exists()) {
boolean rc = dest.delete();
if (BuildConfig.DEBUG)
Log.d(TAG, "Deleted file " + dest.getName() + "; Result: "
Log.d(TAG, "Deleted file " + dest.getName() + "; Result: "
+ rc);
} else {
if (BuildConfig.DEBUG)
Log.d(TAG, "cleanup() didn't delete file: does not exist.");
Log.d(TAG, "cleanup() didn't delete file: does not exist.");
}
}
}
public static String encodeCredentials(String username, String password, String charset) {
try {
String credentials = username + ":" + password;
byte[] bytes = credentials.getBytes(charset);
String encoded = ByteString.of(bytes).base64();
return "Basic " + encoded;
} catch (UnsupportedEncodingException e) {
throw new AssertionError();
}
}
}

View File

@ -711,6 +711,35 @@ public final class DBReader {
return item;
}
/**
* Returns credentials based on image URL
*
* @param context A context that is used for opening a database connection.
* @param imageUrl The URL of the image
* @return Credentials in format "<Username>:<Password>", empty String if no authorization given
*/
public static String getImageAuthentication(final Context context, final String imageUrl) {
Log.d(TAG, "Loading credentials for image with URL " + imageUrl);
PodDBAdapter adapter = new PodDBAdapter(context);
adapter.open();
String credentials = getImageAuthentication(context, imageUrl, adapter);
adapter.close();
return credentials;
}
static String getImageAuthentication(final Context context, final String imageUrl, PodDBAdapter adapter) {
String credentials = null;
Cursor cursor = adapter.getImageAuthenticationCursor(imageUrl);
if (cursor.moveToFirst()) {
String username = cursor.getString(0);
String password = cursor.getString(1);
return username + ":" + password;
}
return "";
}
/**
* Loads a specific FeedItem from the database.
*

View File

@ -1146,6 +1146,20 @@ public class PodDBAdapter {
return db.rawQuery(query, null);
}
public Cursor getImageAuthenticationCursor(final String imageUrl) {
final String query = "SELECT " + KEY_USERNAME + "," + KEY_PASSWORD + " FROM "
+ TABLE_NAME_FEED_IMAGES + " INNER JOIN " + TABLE_NAME_FEEDS + " ON " +
TABLE_NAME_FEED_IMAGES + "." + KEY_ID + "=" + TABLE_NAME_FEEDS + "." + KEY_IMAGE + " WHERE "
+ TABLE_NAME_FEED_IMAGES + "." + KEY_DOWNLOAD_URL + "='" + imageUrl + "' UNION SELECT "
+ KEY_USERNAME + "," + KEY_PASSWORD + " FROM " + TABLE_NAME_FEED_IMAGES + " INNER JOIN "
+ TABLE_NAME_FEED_ITEMS + " ON " + TABLE_NAME_FEED_IMAGES + "." + KEY_ID + "=" +
TABLE_NAME_FEED_ITEMS + "." + KEY_IMAGE + " INNER JOIN " + TABLE_NAME_FEEDS + " ON "
+ TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + TABLE_NAME_FEEDS + "." + KEY_ID + " WHERE "
+ TABLE_NAME_FEED_IMAGES + "." + KEY_DOWNLOAD_URL + "='" + imageUrl + "'";
Log.d(TAG, "Query: " + query);
return db.rawQuery(query, null);
}
public int getQueueSize() {
final String query = String.format("SELECT COUNT(%s) FROM %s", KEY_ID, TABLE_NAME_QUEUE);
Cursor c = db.rawQuery(query, null);