Home | History | Annotate | Download | only in media
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.media;
     18 
     19 import android.net.NetworkUtils;
     20 import android.os.IBinder;
     21 import android.os.StrictMode;
     22 import android.util.Log;
     23 
     24 import java.io.BufferedInputStream;
     25 import java.io.InputStream;
     26 import java.io.IOException;
     27 import java.net.CookieHandler;
     28 import java.net.CookieManager;
     29 import java.net.Proxy;
     30 import java.net.URL;
     31 import java.net.HttpURLConnection;
     32 import java.net.MalformedURLException;
     33 import java.net.NoRouteToHostException;
     34 import java.net.ProtocolException;
     35 import java.net.UnknownServiceException;
     36 import java.util.HashMap;
     37 import java.util.Map;
     38 
     39 import static android.media.MediaPlayer.MEDIA_ERROR_UNSUPPORTED;
     40 
     41 /** @hide */
     42 public class MediaHTTPConnection extends IMediaHTTPConnection.Stub {
     43     private static final String TAG = "MediaHTTPConnection";
     44     private static final boolean VERBOSE = false;
     45 
     46     // connection timeout - 30 sec
     47     private static final int CONNECT_TIMEOUT_MS = 30 * 1000;
     48 
     49     private long mCurrentOffset = -1;
     50     private URL mURL = null;
     51     private Map<String, String> mHeaders = null;
     52     private HttpURLConnection mConnection = null;
     53     private long mTotalSize = -1;
     54     private InputStream mInputStream = null;
     55 
     56     private boolean mAllowCrossDomainRedirect = true;
     57     private boolean mAllowCrossProtocolRedirect = true;
     58 
     59     // from com.squareup.okhttp.internal.http
     60     private final static int HTTP_TEMP_REDIRECT = 307;
     61     private final static int MAX_REDIRECTS = 20;
     62 
     63     public MediaHTTPConnection() {
     64         if (CookieHandler.getDefault() == null) {
     65             CookieHandler.setDefault(new CookieManager());
     66         }
     67 
     68         native_setup();
     69     }
     70 
     71     @Override
     72     public IBinder connect(String uri, String headers) {
     73         if (VERBOSE) {
     74             Log.d(TAG, "connect: uri=" + uri + ", headers=" + headers);
     75         }
     76 
     77         try {
     78             disconnect();
     79             mAllowCrossDomainRedirect = true;
     80             mURL = new URL(uri);
     81             mHeaders = convertHeaderStringToMap(headers);
     82         } catch (MalformedURLException e) {
     83             return null;
     84         }
     85 
     86         return native_getIMemory();
     87     }
     88 
     89     private boolean parseBoolean(String val) {
     90         try {
     91             return Long.parseLong(val) != 0;
     92         } catch (NumberFormatException e) {
     93             return "true".equalsIgnoreCase(val) ||
     94                 "yes".equalsIgnoreCase(val);
     95         }
     96     }
     97 
     98     /* returns true iff header is internal */
     99     private boolean filterOutInternalHeaders(String key, String val) {
    100         if ("android-allow-cross-domain-redirect".equalsIgnoreCase(key)) {
    101             mAllowCrossDomainRedirect = parseBoolean(val);
    102             // cross-protocol redirects are also controlled by this flag
    103             mAllowCrossProtocolRedirect = mAllowCrossDomainRedirect;
    104         } else {
    105             return false;
    106         }
    107         return true;
    108     }
    109 
    110     private Map<String, String> convertHeaderStringToMap(String headers) {
    111         HashMap<String, String> map = new HashMap<String, String>();
    112 
    113         String[] pairs = headers.split("\r\n");
    114         for (String pair : pairs) {
    115             int colonPos = pair.indexOf(":");
    116             if (colonPos >= 0) {
    117                 String key = pair.substring(0, colonPos);
    118                 String val = pair.substring(colonPos + 1);
    119 
    120                 if (!filterOutInternalHeaders(key, val)) {
    121                     map.put(key, val);
    122                 }
    123             }
    124         }
    125 
    126         return map;
    127     }
    128 
    129     @Override
    130     public void disconnect() {
    131         teardownConnection();
    132         mHeaders = null;
    133         mURL = null;
    134     }
    135 
    136     private void teardownConnection() {
    137         if (mConnection != null) {
    138             mInputStream = null;
    139 
    140             mConnection.disconnect();
    141             mConnection = null;
    142 
    143             mCurrentOffset = -1;
    144         }
    145     }
    146 
    147     private static final boolean isLocalHost(URL url) {
    148         if (url == null) {
    149             return false;
    150         }
    151 
    152         String host = url.getHost();
    153 
    154         if (host == null) {
    155             return false;
    156         }
    157 
    158         try {
    159             if (host.equalsIgnoreCase("localhost")) {
    160                 return true;
    161             }
    162             if (NetworkUtils.numericToInetAddress(host).isLoopbackAddress()) {
    163                 return true;
    164             }
    165         } catch (IllegalArgumentException iex) {
    166         }
    167         return false;
    168     }
    169 
    170     private void seekTo(long offset) throws IOException {
    171         teardownConnection();
    172 
    173         try {
    174             int response;
    175             int redirectCount = 0;
    176 
    177             URL url = mURL;
    178 
    179             // do not use any proxy for localhost (127.0.0.1)
    180             boolean noProxy = isLocalHost(url);
    181 
    182             while (true) {
    183                 if (noProxy) {
    184                     mConnection = (HttpURLConnection)url.openConnection(Proxy.NO_PROXY);
    185                 } else {
    186                     mConnection = (HttpURLConnection)url.openConnection();
    187                 }
    188                 mConnection.setConnectTimeout(CONNECT_TIMEOUT_MS);
    189 
    190                 // handle redirects ourselves if we do not allow cross-domain redirect
    191                 mConnection.setInstanceFollowRedirects(mAllowCrossDomainRedirect);
    192 
    193                 if (mHeaders != null) {
    194                     for (Map.Entry<String, String> entry : mHeaders.entrySet()) {
    195                         mConnection.setRequestProperty(
    196                                 entry.getKey(), entry.getValue());
    197                     }
    198                 }
    199 
    200                 if (offset > 0) {
    201                     mConnection.setRequestProperty(
    202                             "Range", "bytes=" + offset + "-");
    203                 }
    204 
    205                 response = mConnection.getResponseCode();
    206                 if (response != HttpURLConnection.HTTP_MULT_CHOICE &&
    207                         response != HttpURLConnection.HTTP_MOVED_PERM &&
    208                         response != HttpURLConnection.HTTP_MOVED_TEMP &&
    209                         response != HttpURLConnection.HTTP_SEE_OTHER &&
    210                         response != HTTP_TEMP_REDIRECT) {
    211                     // not a redirect, or redirect handled by HttpURLConnection
    212                     break;
    213                 }
    214 
    215                 if (++redirectCount > MAX_REDIRECTS) {
    216                     throw new NoRouteToHostException("Too many redirects: " + redirectCount);
    217                 }
    218 
    219                 String method = mConnection.getRequestMethod();
    220                 if (response == HTTP_TEMP_REDIRECT &&
    221                         !method.equals("GET") && !method.equals("HEAD")) {
    222                     // "If the 307 status code is received in response to a
    223                     // request other than GET or HEAD, the user agent MUST NOT
    224                     // automatically redirect the request"
    225                     throw new NoRouteToHostException("Invalid redirect");
    226                 }
    227                 String location = mConnection.getHeaderField("Location");
    228                 if (location == null) {
    229                     throw new NoRouteToHostException("Invalid redirect");
    230                 }
    231                 url = new URL(mURL /* TRICKY: don't use url! */, location);
    232                 if (!url.getProtocol().equals("https") &&
    233                         !url.getProtocol().equals("http")) {
    234                     throw new NoRouteToHostException("Unsupported protocol redirect");
    235                 }
    236                 boolean sameProtocol = mURL.getProtocol().equals(url.getProtocol());
    237                 if (!mAllowCrossProtocolRedirect && !sameProtocol) {
    238                     throw new NoRouteToHostException("Cross-protocol redirects are disallowed");
    239                 }
    240                 boolean sameHost = mURL.getHost().equals(url.getHost());
    241                 if (!mAllowCrossDomainRedirect && !sameHost) {
    242                     throw new NoRouteToHostException("Cross-domain redirects are disallowed");
    243                 }
    244 
    245                 if (response != HTTP_TEMP_REDIRECT) {
    246                     // update effective URL, unless it is a Temporary Redirect
    247                     mURL = url;
    248                 }
    249             }
    250 
    251             if (mAllowCrossDomainRedirect) {
    252                 // remember the current, potentially redirected URL if redirects
    253                 // were handled by HttpURLConnection
    254                 mURL = mConnection.getURL();
    255             }
    256 
    257             if (response == HttpURLConnection.HTTP_PARTIAL) {
    258                 // Partial content, we cannot just use getContentLength
    259                 // because what we want is not just the length of the range
    260                 // returned but the size of the full content if available.
    261 
    262                 String contentRange =
    263                     mConnection.getHeaderField("Content-Range");
    264 
    265                 mTotalSize = -1;
    266                 if (contentRange != null) {
    267                     // format is "bytes xxx-yyy/zzz
    268                     // where "zzz" is the total number of bytes of the
    269                     // content or '*' if unknown.
    270 
    271                     int lastSlashPos = contentRange.lastIndexOf('/');
    272                     if (lastSlashPos >= 0) {
    273                         String total =
    274                             contentRange.substring(lastSlashPos + 1);
    275 
    276                         try {
    277                             mTotalSize = Long.parseLong(total);
    278                         } catch (NumberFormatException e) {
    279                         }
    280                     }
    281                 }
    282             } else if (response != HttpURLConnection.HTTP_OK) {
    283                 throw new IOException();
    284             } else {
    285                 mTotalSize = mConnection.getContentLength();
    286             }
    287 
    288             if (offset > 0 && response != HttpURLConnection.HTTP_PARTIAL) {
    289                 // Some servers simply ignore "Range" requests and serve
    290                 // data from the start of the content.
    291                 throw new ProtocolException();
    292             }
    293 
    294             mInputStream =
    295                 new BufferedInputStream(mConnection.getInputStream());
    296 
    297             mCurrentOffset = offset;
    298         } catch (IOException e) {
    299             mTotalSize = -1;
    300             mInputStream = null;
    301             mConnection = null;
    302             mCurrentOffset = -1;
    303 
    304             throw e;
    305         }
    306     }
    307 
    308     @Override
    309     public int readAt(long offset, int size) {
    310         return native_readAt(offset, size);
    311     }
    312 
    313     private int readAt(long offset, byte[] data, int size) {
    314         StrictMode.ThreadPolicy policy =
    315             new StrictMode.ThreadPolicy.Builder().permitAll().build();
    316 
    317         StrictMode.setThreadPolicy(policy);
    318 
    319         try {
    320             if (offset != mCurrentOffset) {
    321                 seekTo(offset);
    322             }
    323 
    324             int n = mInputStream.read(data, 0, size);
    325 
    326             if (n == -1) {
    327                 // InputStream signals EOS using a -1 result, our semantics
    328                 // are to return a 0-length read.
    329                 n = 0;
    330             }
    331 
    332             mCurrentOffset += n;
    333 
    334             if (VERBOSE) {
    335                 Log.d(TAG, "readAt " + offset + " / " + size + " => " + n);
    336             }
    337 
    338             return n;
    339         } catch (ProtocolException e) {
    340             Log.w(TAG, "readAt " + offset + " / " + size + " => " + e);
    341             return MEDIA_ERROR_UNSUPPORTED;
    342         } catch (NoRouteToHostException e) {
    343             Log.w(TAG, "readAt " + offset + " / " + size + " => " + e);
    344             return MEDIA_ERROR_UNSUPPORTED;
    345         } catch (UnknownServiceException e) {
    346             Log.w(TAG, "readAt " + offset + " / " + size + " => " + e);
    347             return MEDIA_ERROR_UNSUPPORTED;
    348         } catch (IOException e) {
    349             if (VERBOSE) {
    350                 Log.d(TAG, "readAt " + offset + " / " + size + " => -1");
    351             }
    352             return -1;
    353         } catch (Exception e) {
    354             if (VERBOSE) {
    355                 Log.d(TAG, "unknown exception " + e);
    356                 Log.d(TAG, "readAt " + offset + " / " + size + " => -1");
    357             }
    358             return -1;
    359         }
    360     }
    361 
    362     @Override
    363     public long getSize() {
    364         if (mConnection == null) {
    365             try {
    366                 seekTo(0);
    367             } catch (IOException e) {
    368                 return -1;
    369             }
    370         }
    371 
    372         return mTotalSize;
    373     }
    374 
    375     @Override
    376     public String getMIMEType() {
    377         if (mConnection == null) {
    378             try {
    379                 seekTo(0);
    380             } catch (IOException e) {
    381                 return "application/octet-stream";
    382             }
    383         }
    384 
    385         return mConnection.getContentType();
    386     }
    387 
    388     @Override
    389     public String getUri() {
    390         return mURL.toString();
    391     }
    392 
    393     @Override
    394     protected void finalize() {
    395         native_finalize();
    396     }
    397 
    398     private static native final void native_init();
    399     private native final void native_setup();
    400     private native final void native_finalize();
    401 
    402     private native final IBinder native_getIMemory();
    403     private native final int native_readAt(long offset, int size);
    404 
    405     static {
    406         System.loadLibrary("media_jni");
    407         native_init();
    408     }
    409 
    410     private long mNativeContext;
    411 
    412 }
    413