content: /*
   * Copyright (C) 2013 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.media;
  
  import static android.media.MediaPlayer.MEDIA_ERROR_UNSUPPORTED;
  
  import android.annotation.UnsupportedAppUsage;
  import android.net.NetworkUtils;
  import android.os.IBinder;
  import android.os.StrictMode;
  import android.util.Log;
  
  import com.android.internal.annotations.GuardedBy;
  import java.io.BufferedInputStream;
  import java.io.IOException;
  import java.io.InputStream;
  import java.net.CookieHandler;
  import java.net.HttpURLConnection;
  import java.net.MalformedURLException;
  import java.net.NoRouteToHostException;
  import java.net.ProtocolException;
  import java.net.Proxy;
  import java.net.URL;
  import java.net.UnknownServiceException;
  import java.util.HashMap;
  import java.util.Map;
  import java.util.concurrent.atomic.AtomicInteger;
  
  /** @hide */
  public class MediaHTTPConnection extends IMediaHTTPConnection.Stub {
      private static final String TAG = "MediaHTTPConnection";
      private static final boolean VERBOSE = false;
  
      // connection timeout - 30 sec
      private static final int CONNECT_TIMEOUT_MS = 30 * 1000;
  
      @GuardedBy("this")
      @UnsupportedAppUsage
      private long mCurrentOffset = -1;
  
      @GuardedBy("this")
      @UnsupportedAppUsage
      private URL mURL = null;
  
      @GuardedBy("this")
      @UnsupportedAppUsage
      private Map<String, String> mHeaders = null;
  
      // volatile so that disconnect() can be called without acquiring a lock.
      // All other access is @GuardedBy("this").
      @UnsupportedAppUsage
      private volatile HttpURLConnection mConnection = null;
  
      @GuardedBy("this")
      @UnsupportedAppUsage
      private long mTotalSize = -1;
  
      @GuardedBy("this")
      private InputStream mInputStream = null;
  
      @GuardedBy("this")
      @UnsupportedAppUsage
      private boolean mAllowCrossDomainRedirect = true;
  
      @GuardedBy("this")
      @UnsupportedAppUsage
      private boolean mAllowCrossProtocolRedirect = true;
  
      // from com.squareup.okhttp.internal.http
      private final static int HTTP_TEMP_REDIRECT = 307;
      private final static int MAX_REDIRECTS = 20;
  
      // The number of threads that are currently running disconnect() (possibly
      // not yet holding the synchronized lock).
      private final AtomicInteger mNumDisconnectingThreads = new AtomicInteger(0);
  
      @UnsupportedAppUsage
      public MediaHTTPConnection() {
          CookieHandler cookieHandler = CookieHandler.getDefault();
          if (cookieHandler == null) {
              Log.w(TAG, "MediaHTTPConnection: Unexpected. No CookieHandler found.");
          }
  
          native_setup();
      }
  
      @Override
      @UnsupportedAppUsage
      public synchronized IBinder connect(String uri, String headers) {
          if (VERBOSE) {
              Log.d(TAG, "connect: uri=" + uri + ", headers=" + headers);
          }
  
          try {
              disconnect();
              mAllowCrossDomainRedirect = true;
              mURL = new URL(uri);
              mHeaders = convertHeaderStringToMap(headers);
          } catch (MalformedURLException e) {
              return null;
          }
  
          return native_getIMemory();
      }
  
      private static boolean parseBoolean(String val) {
          try {
              return Long.parseLong(val) != 0;
          } catch (NumberFormatException e) {
              return "true".equalsIgnoreCase(val) ||
                  "yes".equalsIgnoreCase(val);
          }
      }
  
      /* returns true iff header is internal */
      private synchronized boolean filterOutInternalHeaders(String key, String val) {
          if ("android-allow-cross-domain-redirect".equalsIgnoreCase(key)) {
              mAllowCrossDomainRedirect = parseBoolean(val);
              // cross-protocol redirects are also controlled by this flag
              mAllowCrossProtocolRedirect = mAllowCrossDomainRedirect;
          } else {
              return false;
          }
          return true;
      }
  
      private synchronized Map<String, String> convertHeaderStringToMap(String headers) {
          HashMap<String, String> map = new HashMap<String, String>();
  
          String[] pairs = headers.split("\r\n");
          for (String pair : pairs) {
              int colonPos = pair.indexOf(":");
              if (colonPos >= 0) {
                  String key = pair.substring(0, colonPos);
                  String val = pair.substring(colonPos + 1);
  
                  if (!filterOutInternalHeaders(key, val)) {
                      map.put(key, val);
                  }
              }
          }
  
          return map;
      }
  
      @Override
      @UnsupportedAppUsage
      public void disconnect() {
          mNumDisconnectingThreads.incrementAndGet();
          try {
              HttpURLConnection connectionToDisconnect = mConnection;
              // Call disconnect() before blocking for the lock in order to ensure that any
              // other thread that is blocked in readAt() will return quickly.
              if (connectionToDisconnect != null) {
                  connectionToDisconnect.disconnect();
              }
              synchronized (this) {
                  // It's possible that while we were waiting to acquire the lock, another thread
                  // concurrently started a new connection; if so, we're disconnecting that one
                  // here, too.
                  teardownConnection();
                  mHeaders = null;
                  mURL = null;
              }
          } finally {
              mNumDisconnectingThreads.decrementAndGet();
          }
      }
  
      private synchronized void teardownConnection() {
          if (mConnection != null) {
              if (mInputStream != null) {
                  try {
                      mInputStream.close();
                  } catch (IOException e) {
                  }
                  mInputStream = null;
              }
  
              mConnection.disconnect();
              mConnection = null;
  
              mCurrentOffset = -1;
          }
      }
  
      private static final boolean isLocalHost(URL url) {
          if (url == null) {
              return false;
          }
  
          String host = url.getHost();
  
          if (host == null) {
              return false;
          }
  
          try {
              if (host.equalsIgnoreCase("localhost")) {
                  return true;
              }
              if (NetworkUtils.numericToInetAddress(host).isLoopbackAddress()) {
                  return true;
              }
          } catch (IllegalArgumentException iex) {
          }
          return false;
      }
  
      private synchronized void seekTo(long offset) throws IOException {
          teardownConnection();
  
          try {
              int response;
              int redirectCount = 0;
  
              URL url = mURL;
  
              // do not use any proxy for localhost (127.0.0.1)
              boolean noProxy = isLocalHost(url);
  
              while (true) {
                  // If another thread is concurrently disconnect()ing, there's a race
                  // between them and us. Therefore, we check mNumDisconnectingThreads shortly
                  // (not atomically) before & after writing mConnection. This guarantees that
                  // we won't "lose" a disconnect by creating a new connection that might
                  // miss the disconnect.
                  //
                  // Note that throwing an instanceof IOException is also what this thread
                  // would have done if another thread disconnect()ed the connection while
                  // this thread was blocked reading from that connection further down in this
                  // loop.
                  if (mNumDisconnectingThreads.get() > 0) {
                      throw new IOException("concurrently disconnecting");
                  }
                  if (noProxy) {
                      mConnection = (HttpURLConnection)url.openConnection(Proxy.NO_PROXY);
                  } else {
                      mConnection = (HttpURLConnection)url.openConnection();
                  }
                  // If another thread is concurrently disconnecting, throwing IOException will
                  // cause us to release the lock, giving the other thread a chance to acquire
                  // it. It also ensures that the catch block will run, which will tear down
                  // the connection even if the other thread happens to already be on its way
                  // out of disconnect().
                  if (mNumDisconnectingThreads.get() > 0) {
                      throw new IOException("concurrently disconnecting");
                  }
                  // If we get here without having thrown, we know that other threads
                  // will see our write to mConnection. Any disconnect() on that mConnection
                  // instance will cause our read from/write to that connection instance below
                  // to encounter an instanceof IOException.
                  mConnection.setConnectTimeout(CONNECT_TIMEOUT_MS);
  
                  // handle redirects ourselves if we do not allow cross-domain redirect
                  mConnection.setInstanceFollowRedirects(mAllowCrossDomainRedirect);
  
                  if (mHeaders != null) {
                      for (Map.Entry<String, String> entry : mHeaders.entrySet()) {
                          mConnection.setRequestProperty(
                                  entry.getKey(), entry.getValue());
                      }
                  }
  
                  if (offset > 0) {
                      mConnection.setRequestProperty(
                              "Range", "bytes=" + offset + "-");
                  }
  
                  response = mConnection.getResponseCode();
                  if (response != HttpURLConnection.HTTP_MULT_CHOICE &&
                          response != HttpURLConnection.HTTP_MOVED_PERM &&
                          response != HttpURLConnection.HTTP_MOVED_TEMP &&
                          response != HttpURLConnection.HTTP_SEE_OTHER &&
                          response != HTTP_TEMP_REDIRECT) {
                      // not a redirect, or redirect handled by HttpURLConnection
                      break;
                  }
  
                  if (++redirectCount > MAX_REDIRECTS) {
                      throw new NoRouteToHostException("Too many redirects: " + redirectCount);
                  }
  
                  String method = mConnection.getRequestMethod();
                  if (response == HTTP_TEMP_REDIRECT &&
                          !method.equals("GET") && !method.equals("HEAD")) {
                      // "If the 307 status code is received in response to a
                      // request other than GET or HEAD, the user agent MUST NOT
                      // automatically redirect the request"
                      throw new NoRouteToHostException("Invalid redirect");
                  }
                  String location = mConnection.getHeaderField("Location");
                  if (location == null) {
                      throw new NoRouteToHostException("Invalid redirect");
                  }
                  url = new URL(mURL /* TRICKY: don't use url! */, location);
                  if (!url.getProtocol().equals("https") &&
                          !url.getProtocol().equals("http")) {
                      throw new NoRouteToHostException("Unsupported protocol redirect");
                  }
                  boolean sameProtocol = mURL.getProtocol().equals(url.getProtocol());
                  if (!mAllowCrossProtocolRedirect && !sameProtocol) {
                      throw new NoRouteToHostException("Cross-protocol redirects are disallowed");
                  }
                  boolean sameHost = mURL.getHost().equals(url.getHost());
                  if (!mAllowCrossDomainRedirect && !sameHost) {
                      throw new NoRouteToHostException("Cross-domain redirects are disallowed");
                  }
  
                  if (response != HTTP_TEMP_REDIRECT) {
                      // update effective URL, unless it is a Temporary Redirect
                      mURL = url;
                  }
              }
  
              if (mAllowCrossDomainRedirect) {
                  // remember the current, potentially redirected URL if redirects
                  // were handled by HttpURLConnection
                  mURL = mConnection.getURL();
              }
  
              if (response == HttpURLConnection.HTTP_PARTIAL) {
                  // Partial content, we cannot just use getContentLength
                  // because what we want is not just the length of the range
                  // returned but the size of the full content if available.
  
                  String contentRange =
                      mConnection.getHeaderField("Content-Range");
  
                  mTotalSize = -1;
                  if (contentRange != null) {
                      // format is "bytes xxx-yyy/zzz
                      // where "zzz" is the total number of bytes of the
                      // content or '*' if unknown.
  
                      int lastSlashPos = contentRange.lastIndexOf('/');
                      if (lastSlashPos >= 0) {
                          String total =
                              contentRange.substring(lastSlashPos + 1);
  
                          try {
                              mTotalSize = Long.parseLong(total);
                          } catch (NumberFormatException e) {
                          }
                      }
                  }
              } else if (response != HttpURLConnection.HTTP_OK) {
                  throw new IOException();
              } else {
                  mTotalSize = mConnection.getContentLength();
              }
  
              if (offset > 0 && response != HttpURLConnection.HTTP_PARTIAL) {
                  // Some servers simply ignore "Range" requests and serve
                  // data from the start of the content.
                  throw new ProtocolException();
              }
  
              mInputStream =
                  new BufferedInputStream(mConnection.getInputStream());
  
              mCurrentOffset = offset;
          } catch (IOException e) {
              mTotalSize = -1;
              teardownConnection();
              mCurrentOffset = -1;
  
              throw e;
          }
      }
  
      @Override
      @UnsupportedAppUsage
      public synchronized int readAt(long offset, int size) {
          return native_readAt(offset, size);
      }
  
      private synchronized int readAt(long offset, byte[] data, int size) {
          StrictMode.ThreadPolicy policy =
              new StrictMode.ThreadPolicy.Builder().permitAll().build();
  
          StrictMode.setThreadPolicy(policy);
  
          try {
              if (offset != mCurrentOffset) {
                  seekTo(offset);
              }
  
              int n = mInputStream.read(data, 0, size);
  
              if (n == -1) {
                  // InputStream signals EOS using a -1 result, our semantics
                  // are to return a 0-length read.
                  n = 0;
              }
  
              mCurrentOffset += n;
  
              if (VERBOSE) {
                  Log.d(TAG, "readAt " + offset + " / " + size + " => " + n);
              }
  
              return n;
          } catch (ProtocolException e) {
              Log.w(TAG, "readAt " + offset + " / " + size + " => " + e);
              return MEDIA_ERROR_UNSUPPORTED;
          } catch (NoRouteToHostException e) {
              Log.w(TAG, "readAt " + offset + " / " + size + " => " + e);
              return MEDIA_ERROR_UNSUPPORTED;
          } catch (UnknownServiceException e) {
              Log.w(TAG, "readAt " + offset + " / " + size + " => " + e);
              return MEDIA_ERROR_UNSUPPORTED;
          } catch (IOException e) {
              if (VERBOSE) {
                  Log.d(TAG, "readAt " + offset + " / " + size + " => -1");
              }
              return -1;
          } catch (Exception e) {
              if (VERBOSE) {
                  Log.d(TAG, "unknown exception " + e);
                  Log.d(TAG, "readAt " + offset + " / " + size + " => -1");
              }
              return -1;
          }
      }
  
      @Override
      public synchronized long getSize() {
          if (mConnection == null) {
              try {
                  seekTo(0);
              } catch (IOException e) {
                  return -1;
              }
          }
  
          return mTotalSize;
      }
  
      @Override
      @UnsupportedAppUsage
      public synchronized String getMIMEType() {
          if (mConnection == null) {
              try {
                  seekTo(0);
              } catch (IOException e) {
                  return "application/octet-stream";
              }
          }
  
          return mConnection.getContentType();
      }
  
      @Override
      @UnsupportedAppUsage
      public synchronized String getUri() {
          return mURL.toString();
      }
  
      @Override
      protected void finalize() {
          native_finalize();
      }
  
      private static native final void native_init();
      private native final void native_setup();
      private native final void native_finalize();
  
      private native final IBinder native_getIMemory();
      private native final int native_readAt(long offset, int size);
  
      static {
          System.loadLibrary("media_jni");
          native_init();
      }
  
      private long mNativeContext;
  
  }
  