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         CookieHandler cookieHandler = CookieHandler.getDefault();
     65         if (cookieHandler == null) {
     66             Log.w(TAG, "MediaHTTPConnection: Unexpected. No CookieHandler found.");
     67         }
     68 
     69         native_setup();
     70     }
     71 
     72     @Override
     73     public IBinder connect(String uri, String headers) {
     74         if (VERBOSE) {
     75             Log.d(TAG, "connect: uri=" + uri + ", headers=" + headers);
     76         }
     77 
     78         try {
     79             disconnect();
     80             mAllowCrossDomainRedirect = true;
     81             mURL = new URL(uri);
     82             mHeaders = convertHeaderStringToMap(headers);
     83         } catch (MalformedURLException e) {
     84             return null;
     85         }
     86 
     87         return native_getIMemory();
     88     }
     89 
     90     private boolean parseBoolean(String val) {
     91         try {
     92             return Long.parseLong(val) != 0;
     93         } catch (NumberFormatException e) {
     94             return "true".equalsIgnoreCase(val) ||
     95                 "yes".equalsIgnoreCase(val);
     96         }
     97     }
     98 
     99     /* returns true iff header is internal */
    100     private boolean filterOutInternalHeaders(String key, String val) {
    101         if ("android-allow-cross-domain-redirect".equalsIgnoreCase(key)) {
    102             mAllowCrossDomainRedirect = parseBoolean(val);
    103             // cross-protocol redirects are also controlled by this flag
    104             mAllowCrossProtocolRedirect = mAllowCrossDomainRedirect;
    105         } else {
    106             return false;
    107         }
    108         return true;
    109     }
    110 
    111     private Map<String, String> convertHeaderStringToMap(String headers) {
    112         HashMap<String, String> map = new HashMap<String, String>();
    113 
    114         String[] pairs = headers.split("\r\n");
    115         for (String pair : pairs) {
    116             int colonPos = pair.indexOf(":");
    117             if (colonPos >= 0) {
    118                 String key = pair.substring(0, colonPos);
    119                 String val = pair.substring(colonPos + 1);
    120 
    121                 if (!filterOutInternalHeaders(key, val)) {
    122                     map.put(key, val);
    123                 }
    124             }
    125         }
    126 
    127         return map;
    128     }
    129 
    130     @Override
    131     public void disconnect() {
    132         teardownConnection();
    133         mHeaders = null;
    134         mURL = null;
    135     }
    136 
    137     private void teardownConnection() {
    138         if (mConnection != null) {
    139             if (mInputStream != null) {
    140                 try {
    141                     mInputStream.close();
    142                 } catch (IOException e) {
    143                 }
    144                 mInputStream = null;
    145             }
    146 
    147             mConnection.disconnect();
    148             mConnection = null;
    149 
    150             mCurrentOffset = -1;
    151         }
    152     }
    153 
    154     private static final boolean isLocalHost(URL url) {
    155         if (url == null) {
    156             return false;
    157         }
    158 
    159         String host = url.getHost();
    160 
    161         if (host == null) {
    162             return false;
    163         }
    164 
    165         try {
    166             if (host.equalsIgnoreCase("localhost")) {
    167                 return true;
    168             }
    169             if (NetworkUtils.numericToInetAddress(host).isLoopbackAddress()) {
    170                 return true;
    171             }
    172         } catch (IllegalArgumentException iex) {
    173         }
    174         return false;
    175     }
    176 
    177     private void seekTo(long offset) throws IOException {
    178         teardownConnection();
    179 
    180         try {
    181             int response;
    182             int redirectCount = 0;
    183 
    184             URL url = mURL;
    185 
    186             // do not use any proxy for localhost (127.0.0.1)
    187             boolean noProxy = isLocalHost(url);
    188 
    189             while (true) {
    190                 if (noProxy) {
    191                     mConnection = (HttpURLConnection)url.openConnection(Proxy.NO_PROXY);
    192                 } else {
    193                     mConnection = (HttpURLConnection)url.openConnection();
    194                 }
    195                 mConnection.setConnectTimeout(CONNECT_TIMEOUT_MS);
    196 
    197                 // handle redirects ourselves if we do not allow cross-domain redirect
    198                 mConnection.setInstanceFollowRedirects(mAllowCrossDomainRedirect);
    199 
    200                 if (mHeaders != null) {
    201                     for (Map.Entry<String, String> entry : mHeaders.entrySet()) {
    202                         mConnection.setRequestProperty(
    203                                 entry.getKey(), entry.getValue());
    204                     }
    205                 }
    206 
    207                 if (offset > 0) {
    208                     mConnection.setRequestProperty(
    209                             "Range", "bytes=" + offset + "-");
    210                 }
    211 
    212                 response = mConnection.getResponseCode();
    213                 if (response != HttpURLConnection.HTTP_MULT_CHOICE &&
    214                         response != HttpURLConnection.HTTP_MOVED_PERM &&
    215                         response != HttpURLConnection.HTTP_MOVED_TEMP &&
    216                         response != HttpURLConnection.HTTP_SEE_OTHER &&
    217                         response != HTTP_TEMP_REDIRECT) {
    218                     // not a redirect, or redirect handled by HttpURLConnection
    219                     break;
    220                 }
    221 
    222                 if (++redirectCount > MAX_REDIRECTS) {
    223                     throw new NoRouteToHostException("Too many redirects: " + redirectCount);
    224                 }
    225 
    226                 String method = mConnection.getRequestMethod();
    227                 if (response == HTTP_TEMP_REDIRECT &&
    228                         !method.equals("GET") && !method.equals("HEAD")) {
    229                     // "If the 307 status code is received in response to a
    230                     // request other than GET or HEAD, the user agent MUST NOT
    231                     // automatically redirect the request"
    232                     throw new NoRouteToHostException("Invalid redirect");
    233                 }
    234                 String location = mConnection.getHeaderField("Location");
    235                 if (location == null) {
    236                     throw new NoRouteToHostException("Invalid redirect");
    237                 }
    238                 url = new URL(mURL /* TRICKY: don't use url! */, location);
    239                 if (!url.getProtocol().equals("https") &&
    240                         !url.getProtocol().equals("http")) {
    241                     throw new NoRouteToHostException("Unsupported protocol redirect");
    242                 }
    243                 boolean sameProtocol = mURL.getProtocol().equals(url.getProtocol());
    244                 if (!mAllowCrossProtocolRedirect && !sameProtocol) {
    245                     throw new NoRouteToHostException("Cross-protocol redirects are disallowed");
    246                 }
    247                 boolean sameHost = mURL.getHost().equals(url.getHost());
    248                 if (!mAllowCrossDomainRedirect && !sameHost) {
    249                     throw new NoRouteToHostException("Cross-domain redirects are disallowed");
    250                 }
    251 
    252                 if (response != HTTP_TEMP_REDIRECT) {
    253                     // update effective URL, unless it is a Temporary Redirect
    254                     mURL = url;
    255                 }
    256             }
    257 
    258             if (mAllowCrossDomainRedirect) {
    259                 // remember the current, potentially redirected URL if redirects
    260                 // were handled by HttpURLConnection
    261                 mURL = mConnection.getURL();
    262             }
    263 
    264             if (response == HttpURLConnection.HTTP_PARTIAL) {
    265                 // Partial content, we cannot just use getContentLength
    266                 // because what we want is not just the length of the range
    267                 // returned but the size of the full content if available.
    268 
    269                 String contentRange =
    270                     mConnection.getHeaderField("Content-Range");
    271 
    272                 mTotalSize = -1;
    273                 if (contentRange != null) {
    274                     // format is "bytes xxx-yyy/zzz
    275                     // where "zzz" is the total number of bytes of the
    276                     // content or '*' if unknown.
    277 
    278                     int lastSlashPos = contentRange.lastIndexOf('/');
    279                     if (lastSlashPos >= 0) {
    280                         String total =
    281                             contentRange.substring(lastSlashPos + 1);
    282 
    283                         try {
    284                             mTotalSize = Long.parseLong(total);
    285                         } catch (NumberFormatException e) {
    286                         }
    287                     }
    288                 }
    289             } else if (response != HttpURLConnection.HTTP_OK) {
    290                 throw new IOException();
    291             } else {
    292                 mTotalSize = mConnection.getContentLength();
    293             }
    294 
    295             if (offset > 0 && response != HttpURLConnection.HTTP_PARTIAL) {
    296                 // Some servers simply ignore "Range" requests and serve
    297                 // data from the start of the content.
    298                 throw new ProtocolException();
    299             }
    300 
    301             mInputStream =
    302                 new BufferedInputStream(mConnection.getInputStream());
    303 
    304             mCurrentOffset = offset;
    305         } catch (IOException e) {
    306             mTotalSize = -1;
    307             teardownConnection();
    308             mCurrentOffset = -1;
    309 
    310             throw e;
    311         }
    312     }
    313 
    314     @Override
    315     public int readAt(long offset, int size) {
    316         return native_readAt(offset, size);
    317     }
    318 
    319     private int readAt(long offset, byte[] data, int size) {
    320         StrictMode.ThreadPolicy policy =
    321             new StrictMode.ThreadPolicy.Builder().permitAll().build();
    322 
    323         StrictMode.setThreadPolicy(policy);
    324 
    325         try {
    326             synchronized(this) {
    327                 if (offset != mCurrentOffset) {
    328                     seekTo(offset);
    329                 }
    330             }
    331 
    332             int n = mInputStream.read(data, 0, size);
    333 
    334             if (n == -1) {
    335                 // InputStream signals EOS using a -1 result, our semantics
    336                 // are to return a 0-length read.
    337                 n = 0;
    338             }
    339 
    340             mCurrentOffset += n;
    341 
    342             if (VERBOSE) {
    343                 Log.d(TAG, "readAt " + offset + " / " + size + " => " + n);
    344             }
    345 
    346             return n;
    347         } catch (ProtocolException e) {
    348             Log.w(TAG, "readAt " + offset + " / " + size + " => " + e);
    349             return MEDIA_ERROR_UNSUPPORTED;
    350         } catch (NoRouteToHostException e) {
    351             Log.w(TAG, "readAt " + offset + " / " + size + " => " + e);
    352             return MEDIA_ERROR_UNSUPPORTED;
    353         } catch (UnknownServiceException e) {
    354             Log.w(TAG, "readAt " + offset + " / " + size + " => " + e);
    355             return MEDIA_ERROR_UNSUPPORTED;
    356         } catch (IOException e) {
    357             if (VERBOSE) {
    358                 Log.d(TAG, "readAt " + offset + " / " + size + " => -1");
    359             }
    360             return -1;
    361         } catch (Exception e) {
    362             if (VERBOSE) {
    363                 Log.d(TAG, "unknown exception " + e);
    364                 Log.d(TAG, "readAt " + offset + " / " + size + " => -1");
    365             }
    366             return -1;
    367         }
    368     }
    369 
    370     @Override
    371     public synchronized long getSize() {
    372         if (mConnection == null) {
    373             try {
    374                 seekTo(0);
    375             } catch (IOException e) {
    376                 return -1;
    377             }
    378         }
    379 
    380         return mTotalSize;
    381     }
    382 
    383     @Override
    384     public synchronized String getMIMEType() {
    385         if (mConnection == null) {
    386             try {
    387                 seekTo(0);
    388             } catch (IOException e) {
    389                 return "application/octet-stream";
    390             }
    391         }
    392 
    393         return mConnection.getContentType();
    394     }
    395 
    396     @Override
    397     public String getUri() {
    398         return mURL.toString();
    399     }
    400 
    401     @Override
    402     protected void finalize() {
    403         native_finalize();
    404     }
    405 
    406     private static native final void native_init();
    407     private native final void native_setup();
    408     private native final void native_finalize();
    409 
    410     private native final IBinder native_getIMemory();
    411     private native final int native_readAt(long offset, int size);
    412 
    413     static {
    414         System.loadLibrary("media_jni");
    415         native_init();
    416     }
    417 
    418     private long mNativeContext;
    419 
    420 }
    421