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