Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(68)

Side by Side Diff: media/base/android/java/src/org/chromium/media/MediaDrmBridge.java

Issue 1174523002: Use Chromium's Logging instead of Android's Logging for media files (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Created 5 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright 2013 The Chromium Authors. All rights reserved. 1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 package org.chromium.media; 5 package org.chromium.media;
6 6
7 import android.annotation.TargetApi; 7 import android.annotation.TargetApi;
8 import android.media.MediaCrypto; 8 import android.media.MediaCrypto;
9 import android.media.MediaDrm; 9 import android.media.MediaDrm;
10 import android.os.AsyncTask; 10 import android.os.AsyncTask;
(...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after
49 // b) Finish createSession() if previous createSession() was interrupted 49 // b) Finish createSession() if previous createSession() was interrupted
50 // by a NotProvisionedException. 50 // by a NotProvisionedException.
51 // - Whenever an unexpected error occurred, we'll call release() to release 51 // - Whenever an unexpected error occurred, we'll call release() to release
52 // all resources and clear all states. In that case all calls to this 52 // all resources and clear all states. In that case all calls to this
53 // object will be no-op. All public APIs and callbacks should check 53 // object will be no-op. All public APIs and callbacks should check
54 // mMediaBridge to make sure release() hasn't been called. Also, we call 54 // mMediaBridge to make sure release() hasn't been called. Also, we call
55 // release() immediately after the error happens (e.g. after mMediaDrm) 55 // release() immediately after the error happens (e.g. after mMediaDrm)
56 // calls. Indirect calls should not call release() again to avoid 56 // calls. Indirect calls should not call release() again to avoid
57 // duplication (even though it doesn't hurt to call release() twice). 57 // duplication (even though it doesn't hurt to call release() twice).
58 58
59 private static final String TAG = "MediaDrmBridge"; 59 private static final String TAG = Log.makeTag("media.MediaDrmBridge");
60 private static final String SECURITY_LEVEL = "securityLevel"; 60 private static final String SECURITY_LEVEL = "securityLevel";
61 private static final String SERVER_CERTIFICATE = "serviceCertificate"; 61 private static final String SERVER_CERTIFICATE = "serviceCertificate";
62 private static final String PRIVACY_MODE = "privacyMode"; 62 private static final String PRIVACY_MODE = "privacyMode";
63 private static final String SESSION_SHARING = "sessionSharing"; 63 private static final String SESSION_SHARING = "sessionSharing";
64 private static final String ENABLE = "enable"; 64 private static final String ENABLE = "enable";
65 private static final int INVALID_SESSION_ID = 0; 65 private static final int INVALID_SESSION_ID = 0;
66 private static final char[] HEX_CHAR_LOOKUP = "0123456789ABCDEF".toCharArray (); 66 private static final char[] HEX_CHAR_LOOKUP = "0123456789ABCDEF".toCharArray ();
67 private static final long INVALID_NATIVE_MEDIA_DRM_BRIDGE = 0; 67 private static final long INVALID_NATIVE_MEDIA_DRM_BRIDGE = 0;
68 68
69 // KeyStatus for CDM key information. MUST keep this value in sync with 69 // KeyStatus for CDM key information. MUST keep this value in sync with
(...skipping 134 matching lines...) Expand 10 before | Expand all | Expand 10 after
204 assert !mProvisioningPending; 204 assert !mProvisioningPending;
205 assert mMediaCryptoSession == null; 205 assert mMediaCryptoSession == null;
206 assert mMediaCrypto == null; 206 assert mMediaCrypto == null;
207 207
208 // Open media crypto session. 208 // Open media crypto session.
209 mMediaCryptoSession = openSession(); 209 mMediaCryptoSession = openSession();
210 if (mMediaCryptoSession == null) { 210 if (mMediaCryptoSession == null) {
211 Log.e(TAG, "Cannot create MediaCrypto Session."); 211 Log.e(TAG, "Cannot create MediaCrypto Session.");
212 return false; 212 return false;
213 } 213 }
214 Log.d(TAG, "MediaCrypto Session created: " + bytesToHexString(mMediaCryp toSession)); 214 Log.d(TAG, "MediaCrypto Session created: %s", bytesToHexString(mMediaCry ptoSession));
215 215
216 // Create MediaCrypto object. 216 // Create MediaCrypto object.
217 try { 217 try {
218 // TODO: This requires KitKat. Is this class used on pre-KK devices? 218 // TODO: This requires KitKat. Is this class used on pre-KK devices?
219 if (MediaCrypto.isCryptoSchemeSupported(mSchemeUUID)) { 219 if (MediaCrypto.isCryptoSchemeSupported(mSchemeUUID)) {
220 mMediaCrypto = new MediaCrypto(mSchemeUUID, mMediaCryptoSession) ; 220 mMediaCrypto = new MediaCrypto(mSchemeUUID, mMediaCryptoSession) ;
221 Log.d(TAG, "MediaCrypto successfully created!"); 221 Log.d(TAG, "MediaCrypto successfully created!");
222 // Notify the native code that MediaCrypto is ready. 222 // Notify the native code that MediaCrypto is ready.
223 onMediaCryptoReady(); 223 onMediaCryptoReady();
224 return true; 224 return true;
(...skipping 90 matching lines...) Expand 10 before | Expand all | Expand 10 after
315 * @param securityLevel Security level to be set. 315 * @param securityLevel Security level to be set.
316 * @return whether the security level was successfully set. 316 * @return whether the security level was successfully set.
317 */ 317 */
318 @CalledByNative 318 @CalledByNative
319 private boolean setSecurityLevel(String securityLevel) { 319 private boolean setSecurityLevel(String securityLevel) {
320 if (mMediaDrm == null || mMediaCrypto != null) { 320 if (mMediaDrm == null || mMediaCrypto != null) {
321 return false; 321 return false;
322 } 322 }
323 323
324 String currentSecurityLevel = mMediaDrm.getPropertyString(SECURITY_LEVEL ); 324 String currentSecurityLevel = mMediaDrm.getPropertyString(SECURITY_LEVEL );
325 Log.e(TAG, "Security level: current " + currentSecurityLevel + ", new " + securityLevel); 325 Log.e(TAG, "Security level: current %s, new %s", currentSecurityLevel, s ecurityLevel);
326 if (securityLevel.equals(currentSecurityLevel)) { 326 if (securityLevel.equals(currentSecurityLevel)) {
327 // No need to set the same security level again. This is not just 327 // No need to set the same security level again. This is not just
328 // a shortcut! Setting the same security level actually causes an 328 // a shortcut! Setting the same security level actually causes an
329 // exception in MediaDrm! 329 // exception in MediaDrm!
330 return true; 330 return true;
331 } 331 }
332 332
333 try { 333 try {
334 mMediaDrm.setPropertyString(SECURITY_LEVEL, securityLevel); 334 mMediaDrm.setPropertyString(SECURITY_LEVEL, securityLevel);
335 return true; 335 return true;
336 } catch (java.lang.IllegalArgumentException e) { 336 } catch (java.lang.IllegalArgumentException e) {
337 Log.e(TAG, "Failed to set security level " + securityLevel, e); 337 Log.e(TAG, "Failed to set security level %s", securityLevel, e);
338 } catch (java.lang.IllegalStateException e) { 338 } catch (java.lang.IllegalStateException e) {
339 Log.e(TAG, "Failed to set security level " + securityLevel, e); 339 Log.e(TAG, "Failed to set security level %s", securityLevel, e);
340 } 340 }
341 341
342 Log.e(TAG, "Security level " + securityLevel + " not supported!"); 342 Log.e(TAG, "Security level %s not supported!", securityLevel);
343 return false; 343 return false;
344 } 344 }
345 345
346 /** 346 /**
347 * Set the server certificate. 347 * Set the server certificate.
348 * 348 *
349 * @param certificate Server certificate to be set. 349 * @param certificate Server certificate to be set.
350 * @return whether the server certificate was successfully set. 350 * @return whether the server certificate was successfully set.
351 */ 351 */
352 @CalledByNative 352 @CalledByNative
(...skipping 106 matching lines...) Expand 10 before | Expand all | Expand 10 after
459 sessionId, data, mime, MediaDrm.KEY_TYPE_STREAMING, optional Parameters); 459 sessionId, data, mime, MediaDrm.KEY_TYPE_STREAMING, optional Parameters);
460 } catch (IllegalStateException e) { 460 } catch (IllegalStateException e) {
461 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && e 461 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && e
462 instanceof android.media.MediaDrm.MediaDrmStateException) { 462 instanceof android.media.MediaDrm.MediaDrmStateException) {
463 // See b/21307186 for details. 463 // See b/21307186 for details.
464 Log.e(TAG, "MediaDrmStateException fired during getKeyRequest(). ", e); 464 Log.e(TAG, "MediaDrmStateException fired during getKeyRequest(). ", e);
465 } 465 }
466 } 466 }
467 467
468 String result = (request != null) ? "successed" : "failed"; 468 String result = (request != null) ? "successed" : "failed";
469 Log.d(TAG, "getKeyRequest " + result + "!"); 469 Log.d(TAG, "getKeyRequest %s!", result);
470 470
471 return request; 471 return request;
472 } 472 }
473 473
474 /** 474 /**
475 * Save data to |mPendingCreateSessionDataQueue| so that we can resume the 475 * Save data to |mPendingCreateSessionDataQueue| so that we can resume the
476 * createSession() call later. 476 * createSession() call later.
477 * 477 *
478 * @param initData Data needed to generate the key request. 478 * @param initData Data needed to generate the key request.
479 * @param mime Mime type. 479 * @param mime Mime type.
(...skipping 103 matching lines...) Expand 10 before | Expand all | Expand 10 after
583 583
584 MediaDrm.KeyRequest request = null; 584 MediaDrm.KeyRequest request = null;
585 request = getKeyRequest(sessionId, initData, mime, optionalParameter s); 585 request = getKeyRequest(sessionId, initData, mime, optionalParameter s);
586 if (request == null) { 586 if (request == null) {
587 mMediaDrm.closeSession(sessionId); 587 mMediaDrm.closeSession(sessionId);
588 onPromiseRejected(promiseId, "Generate request failed."); 588 onPromiseRejected(promiseId, "Generate request failed.");
589 return; 589 return;
590 } 590 }
591 591
592 // Success! 592 // Success!
593 Log.d(TAG, "createSession(): Session (" + bytesToHexString(sessionId ) + ") created."); 593 Log.d(TAG, "createSession(): Session (%s) created.", bytesToHexStrin g(sessionId));
594 onPromiseResolvedWithSession(promiseId, sessionId); 594 onPromiseResolvedWithSession(promiseId, sessionId);
595 onSessionMessage(sessionId, request); 595 onSessionMessage(sessionId, request);
596 mSessionIds.put(ByteBuffer.wrap(sessionId), mime); 596 mSessionIds.put(ByteBuffer.wrap(sessionId), mime);
597 } catch (android.media.NotProvisionedException e) { 597 } catch (android.media.NotProvisionedException e) {
598 Log.e(TAG, "Device not provisioned", e); 598 Log.e(TAG, "Device not provisioned", e);
599 if (newSessionOpened) { 599 if (newSessionOpened) {
600 mMediaDrm.closeSession(sessionId); 600 mMediaDrm.closeSession(sessionId);
601 } 601 }
602 savePendingCreateSessionData(initData, mime, optionalParameters, pro miseId); 602 savePendingCreateSessionData(initData, mime, optionalParameters, pro miseId);
603 startProvisioning(); 603 startProvisioning();
(...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after
644 try { 644 try {
645 // Some implementations don't have removeKeys, crbug/475632 645 // Some implementations don't have removeKeys, crbug/475632
646 mMediaDrm.removeKeys(sessionId); 646 mMediaDrm.removeKeys(sessionId);
647 } catch (Exception e) { 647 } catch (Exception e) {
648 Log.e(TAG, "removeKeys failed: ", e); 648 Log.e(TAG, "removeKeys failed: ", e);
649 } 649 }
650 mMediaDrm.closeSession(sessionId); 650 mMediaDrm.closeSession(sessionId);
651 mSessionIds.remove(ByteBuffer.wrap(sessionId)); 651 mSessionIds.remove(ByteBuffer.wrap(sessionId));
652 onPromiseResolved(promiseId); 652 onPromiseResolved(promiseId);
653 onSessionClosed(sessionId); 653 onSessionClosed(sessionId);
654 Log.d(TAG, "Session " + bytesToHexString(sessionId) + " closed."); 654 Log.d(TAG, "Session %s closed", bytesToHexString(sessionId));
655 } 655 }
656 656
657 /** 657 /**
658 * Update a session with response. 658 * Update a session with response.
659 * 659 *
660 * @param sessionId Reference ID of session to be updated. 660 * @param sessionId Reference ID of session to be updated.
661 * @param response Response data from the server. 661 * @param response Response data from the server.
662 * @param promiseId Promise ID of this call. 662 * @param promiseId Promise ID of this call.
663 */ 663 */
664 @CalledByNative 664 @CalledByNative
(...skipping 13 matching lines...) Expand all
678 678
679 try { 679 try {
680 try { 680 try {
681 mMediaDrm.provideKeyResponse(sessionId, response); 681 mMediaDrm.provideKeyResponse(sessionId, response);
682 } catch (java.lang.IllegalStateException e) { 682 } catch (java.lang.IllegalStateException e) {
683 // This is not really an exception. Some error code are incorrec tly 683 // This is not really an exception. Some error code are incorrec tly
684 // reported as an exception. 684 // reported as an exception.
685 // TODO(qinmin): remove this exception catch when b/10495563 is fixed. 685 // TODO(qinmin): remove this exception catch when b/10495563 is fixed.
686 Log.e(TAG, "Exception intentionally caught when calling provideK eyResponse()", e); 686 Log.e(TAG, "Exception intentionally caught when calling provideK eyResponse()", e);
687 } 687 }
688 Log.d(TAG, "Key successfully added for session " + bytesToHexString( sessionId)); 688 Log.d(TAG, "Key successfully added for session %s", bytesToHexString (sessionId));
689 onPromiseResolved(promiseId); 689 onPromiseResolved(promiseId);
690 onSessionKeysChange(sessionId, true, KEY_STATUS_USABLE); 690 onSessionKeysChange(sessionId, true, KEY_STATUS_USABLE);
691 return; 691 return;
692 } catch (android.media.NotProvisionedException e) { 692 } catch (android.media.NotProvisionedException e) {
693 // TODO(xhwang): Should we handle this? 693 // TODO(xhwang): Should we handle this?
694 Log.e(TAG, "failed to provide key response", e); 694 Log.e(TAG, "failed to provide key response", e);
695 } catch (android.media.DeniedByServerException e) { 695 } catch (android.media.DeniedByServerException e) {
696 Log.e(TAG, "failed to provide key response", e); 696 Log.e(TAG, "failed to provide key response", e);
697 } 697 }
698 onPromiseRejected(promiseId, "Update session failed."); 698 onPromiseRejected(promiseId, "Update session failed.");
(...skipping 100 matching lines...) Expand 10 before | Expand all | Expand 10 after
799 @Override 799 @Override
800 public void run() { 800 public void run() {
801 if (isNativeMediaDrmBridgeValid()) { 801 if (isNativeMediaDrmBridgeValid()) {
802 nativeOnPromiseResolvedWithSession(mNativeMediaDrmBridge, pr omiseId, sessionId); 802 nativeOnPromiseResolvedWithSession(mNativeMediaDrmBridge, pr omiseId, sessionId);
803 } 803 }
804 } 804 }
805 }); 805 });
806 } 806 }
807 807
808 private void onPromiseRejected(final long promiseId, final String errorMessa ge) { 808 private void onPromiseRejected(final long promiseId, final String errorMessa ge) {
809 Log.e(TAG, "onPromiseRejected: " + errorMessage); 809 Log.e(TAG, "onPromiseRejected: %s", errorMessage);
810 mHandler.post(new Runnable() { 810 mHandler.post(new Runnable() {
811 @Override 811 @Override
812 public void run() { 812 public void run() {
813 if (isNativeMediaDrmBridgeValid()) { 813 if (isNativeMediaDrmBridgeValid()) {
814 nativeOnPromiseRejected(mNativeMediaDrmBridge, promiseId, er rorMessage); 814 nativeOnPromiseRejected(mNativeMediaDrmBridge, promiseId, er rorMessage);
815 } 815 }
816 } 816 }
817 }); 817 });
818 } 818 }
819 819
(...skipping 27 matching lines...) Expand all
847 public void run() { 847 public void run() {
848 if (isNativeMediaDrmBridgeValid()) { 848 if (isNativeMediaDrmBridgeValid()) {
849 nativeOnSessionKeysChange( 849 nativeOnSessionKeysChange(
850 mNativeMediaDrmBridge, sessionId, hasAdditionalUsabl eKey, keyStatus); 850 mNativeMediaDrmBridge, sessionId, hasAdditionalUsabl eKey, keyStatus);
851 } 851 }
852 } 852 }
853 }); 853 });
854 } 854 }
855 855
856 private void onLegacySessionError(final byte[] sessionId, final String error Message) { 856 private void onLegacySessionError(final byte[] sessionId, final String error Message) {
857 Log.e(TAG, "onLegacySessionError: " + errorMessage); 857 Log.e(TAG, "onLegacySessionError: %s", errorMessage);
858 mHandler.post(new Runnable() { 858 mHandler.post(new Runnable() {
859 @Override 859 @Override
860 public void run() { 860 public void run() {
861 if (isNativeMediaDrmBridgeValid()) { 861 if (isNativeMediaDrmBridgeValid()) {
862 nativeOnLegacySessionError(mNativeMediaDrmBridge, sessionId, errorMessage); 862 nativeOnLegacySessionError(mNativeMediaDrmBridge, sessionId, errorMessage);
863 } 863 }
864 } 864 }
865 }); 865 });
866 } 866 }
867 867
(...skipping 10 matching lines...) Expand all
878 878
879 private class MediaDrmListener implements MediaDrm.OnEventListener { 879 private class MediaDrmListener implements MediaDrm.OnEventListener {
880 @Override 880 @Override
881 public void onEvent( 881 public void onEvent(
882 MediaDrm mediaDrm, byte[] sessionId, int event, int extra, byte[ ] data) { 882 MediaDrm mediaDrm, byte[] sessionId, int event, int extra, byte[ ] data) {
883 if (sessionId == null) { 883 if (sessionId == null) {
884 Log.e(TAG, "MediaDrmListener: Null session."); 884 Log.e(TAG, "MediaDrmListener: Null session.");
885 return; 885 return;
886 } 886 }
887 if (!sessionExists(sessionId)) { 887 if (!sessionExists(sessionId)) {
888 Log.e(TAG, "MediaDrmListener: Invalid session " + bytesToHexStri ng(sessionId)); 888 Log.e(TAG, "MediaDrmListener: Invalid session %s", bytesToHexStr ing(sessionId));
889 return; 889 return;
890 } 890 }
891 switch(event) { 891 switch(event) {
892 case MediaDrm.EVENT_PROVISION_REQUIRED: 892 case MediaDrm.EVENT_PROVISION_REQUIRED:
893 Log.d(TAG, "MediaDrm.EVENT_PROVISION_REQUIRED"); 893 Log.d(TAG, "MediaDrm.EVENT_PROVISION_REQUIRED");
894 break; 894 break;
895 case MediaDrm.EVENT_KEY_REQUIRED: 895 case MediaDrm.EVENT_KEY_REQUIRED:
896 Log.d(TAG, "MediaDrm.EVENT_KEY_REQUIRED"); 896 Log.d(TAG, "MediaDrm.EVENT_KEY_REQUIRED");
897 if (mProvisioningPending) { 897 if (mProvisioningPending) {
898 return; 898 return;
(...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after
937 private byte[] mResponseBody; 937 private byte[] mResponseBody;
938 938
939 public PostRequestTask(byte[] drmRequest) { 939 public PostRequestTask(byte[] drmRequest) {
940 mDrmRequest = drmRequest; 940 mDrmRequest = drmRequest;
941 } 941 }
942 942
943 @Override 943 @Override
944 protected Void doInBackground(String... urls) { 944 protected Void doInBackground(String... urls) {
945 mResponseBody = postRequest(urls[0], mDrmRequest); 945 mResponseBody = postRequest(urls[0], mDrmRequest);
946 if (mResponseBody != null) { 946 if (mResponseBody != null) {
947 Log.d(TAG, "response length=" + mResponseBody.length); 947 Log.d(TAG, "response length=%d", mResponseBody.length);
948 } 948 }
949 return null; 949 return null;
950 } 950 }
951 951
952 private byte[] postRequest(String url, byte[] drmRequest) { 952 private byte[] postRequest(String url, byte[] drmRequest) {
953 HttpURLConnection urlConnection = null; 953 HttpURLConnection urlConnection = null;
954 try { 954 try {
955 URL request = new URL(url + "&signedRequest=" + new String(drmRe quest)); 955 URL request = new URL(url + "&signedRequest=" + new String(drmRe quest));
956 urlConnection = (HttpURLConnection) request.openConnection(); 956 urlConnection = (HttpURLConnection) request.openConnection();
957 urlConnection.setDoOutput(true); 957 urlConnection.setDoOutput(true);
(...skipping 15 matching lines...) Expand all
973 while (true) { 973 while (true) {
974 read = bis.read(buffer); 974 read = bis.read(buffer);
975 if (read == -1) break; 975 if (read == -1) break;
976 bos.write(buffer, 0, read); 976 bos.write(buffer, 0, read);
977 } 977 }
978 } finally { 978 } finally {
979 bis.close(); 979 bis.close();
980 } 980 }
981 return bos.toByteArray(); 981 return bos.toByteArray();
982 } else { 982 } else {
983 Log.d(TAG, "Server returned HTTP error code " + responseCode ); 983 Log.d(TAG, "Server returned HTTP error code %d", responseCod e);
984 return null; 984 return null;
985 } 985 }
986 } catch (MalformedURLException e) { 986 } catch (MalformedURLException e) {
987 e.printStackTrace(); 987 e.printStackTrace();
988 } catch (IOException e) { 988 } catch (IOException e) {
989 e.printStackTrace(); 989 e.printStackTrace();
990 } catch (IllegalStateException e) { 990 } catch (IllegalStateException e) {
991 e.printStackTrace(); 991 e.printStackTrace();
992 } finally { 992 } finally {
993 if (urlConnection != null) urlConnection.disconnect(); 993 if (urlConnection != null) urlConnection.disconnect();
(...skipping 19 matching lines...) Expand all
1013 long nativeMediaDrmBridge, byte[] sessionId, byte[] message, String destinationUrl); 1013 long nativeMediaDrmBridge, byte[] sessionId, byte[] message, String destinationUrl);
1014 private native void nativeOnSessionClosed(long nativeMediaDrmBridge, byte[] sessionId); 1014 private native void nativeOnSessionClosed(long nativeMediaDrmBridge, byte[] sessionId);
1015 private native void nativeOnSessionKeysChange(long nativeMediaDrmBridge, byt e[] sessionId, 1015 private native void nativeOnSessionKeysChange(long nativeMediaDrmBridge, byt e[] sessionId,
1016 boolean hasAdditionalUsableKey, int keyStatus); 1016 boolean hasAdditionalUsableKey, int keyStatus);
1017 private native void nativeOnLegacySessionError( 1017 private native void nativeOnLegacySessionError(
1018 long nativeMediaDrmBridge, byte[] sessionId, String errorMessage); 1018 long nativeMediaDrmBridge, byte[] sessionId, String errorMessage);
1019 1019
1020 private native void nativeOnResetDeviceCredentialsCompleted( 1020 private native void nativeOnResetDeviceCredentialsCompleted(
1021 long nativeMediaDrmBridge, boolean success); 1021 long nativeMediaDrmBridge, boolean success);
1022 } 1022 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698