diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:31:44 -0800 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:31:44 -0800 |
commit | 9066cfe9886ac131c34d59ed0e2d287b0e3c0087 (patch) | |
tree | d88beb88001f2482911e3d28e43833b50e4b4e97 /media/java | |
parent | d83a98f4ce9cfa908f5c54bbd70f03eec07e7553 (diff) | |
download | frameworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.zip frameworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.tar.gz frameworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.tar.bz2 |
auto import from //depot/cupcake/@135843
Diffstat (limited to 'media/java')
32 files changed, 11837 insertions, 0 deletions
diff --git a/media/java/android/drm/mobile1/DrmConstraintInfo.java b/media/java/android/drm/mobile1/DrmConstraintInfo.java new file mode 100644 index 0000000..50ae8bd --- /dev/null +++ b/media/java/android/drm/mobile1/DrmConstraintInfo.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2007 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.drm.mobile1; + +import java.util.Date; + +/** + * This class provides interfaces to access the DRM constraint. + */ +public class DrmConstraintInfo { + /** + * The constraint of count. + */ + private int count; + + /** + * The constraint of start date. + */ + private long startDate; + + /** + * The constraint of end date. + */ + private long endDate; + + /** + * The constraint of interval. + */ + private long interval; + + /** + * Construct the DrmConstraint. + */ + DrmConstraintInfo() { + count = -1; + startDate = -1; + endDate = -1; + interval = -1; + } + + /** + * Get the count constraint. + * + * @return the count or -1 if no limit. + */ + public int getCount() { + return count; + } + + /** + * Get the start date constraint. + * + * @return the start date or null if no limit. + */ + public Date getStartDate() { + if (startDate == -1) + return null; + + return new Date(startDate); + } + + /** + * Get the end date constraint. + * + * @return the end date or null if no limit. + */ + public Date getEndDate() { + if (endDate == -1) + return null; + + return new Date(endDate); + } + + /** + * Get the Interval constraint. + * + * @return the interval or -1 if no limit. + */ + public long getInterval() { + return interval; + } +} diff --git a/media/java/android/drm/mobile1/DrmException.java b/media/java/android/drm/mobile1/DrmException.java new file mode 100644 index 0000000..7b06c92 --- /dev/null +++ b/media/java/android/drm/mobile1/DrmException.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2007 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.drm.mobile1; + +import java.io.IOException; + +/** + * A DrmException is thrown to report errors specific to handle DRM content and rights. + */ +public class DrmException extends Exception +{ + // TODO: add more specific DRM error codes. + + private DrmException() { + } + + public DrmException(String message) { + super(message); + } +} diff --git a/media/java/android/drm/mobile1/DrmRawContent.java b/media/java/android/drm/mobile1/DrmRawContent.java new file mode 100644 index 0000000..046b84a --- /dev/null +++ b/media/java/android/drm/mobile1/DrmRawContent.java @@ -0,0 +1,464 @@ +/* + * Copyright (C) 2007 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.drm.mobile1; + +import java.io.*; + +/** + * This class provides interfaces to access the DRM raw content. + */ +public class DrmRawContent { + /** + * The "application/vnd.oma.drm.message" mime type. + */ + public static final String DRM_MIMETYPE_MESSAGE_STRING = "application/vnd.oma.drm.message"; + + /** + * The "application/vnd.oma.drm.content" mime type. + */ + public static final String DRM_MIMETYPE_CONTENT_STRING = "application/vnd.oma.drm.content"; + + /** + * The DRM delivery type: Forward-Lock + */ + public static final int DRM_FORWARD_LOCK = 1; + + /** + * The DRM delivery type: Combined Delivery + */ + public static final int DRM_COMBINED_DELIVERY = 2; + + /** + * The DRM delivery type: Separate Delivery + */ + public static final int DRM_SEPARATE_DELIVERY = 3; + + /** + * The DRM delivery type: Separate Delivery in DRM message + */ + public static final int DRM_SEPARATE_DELIVERY_DM = 4; + + /** + * The DRM media content length is unknown currently + */ + public static final int DRM_UNKNOWN_DATA_LEN = -1; + + + /** + * The id of "application/vnd.oma.drm.message" mime type. + */ + private static final int DRM_MIMETYPE_MESSAGE = 1; + + /** + * The id of "application/vnd.oma.drm.content" mime type. + */ + private static final int DRM_MIMETYPE_CONTENT = 2; + + /** + * Successful operation. + */ + private static final int JNI_DRM_SUCCESS = 0; + + /** + * General failure. + */ + private static final int JNI_DRM_FAILURE = -1; + + /** + * Indicates the end of the DRM content is reached. + */ + private static final int JNI_DRM_EOF = -2; + + /** + * The media content length is unknown from native method + */ + private static final int JNI_DRM_UNKNOWN_DATA_LEN = -3; + + /** + * The member to save the original InputStream data. + */ + private BufferedInputStream inData; + + /** + * The member to save the original InputStream data length. + */ + private int inDataLen; + + /** + * The unique id to this DRM content. It will be initialized + * in constructor by native method. And it will not be changed + * after initialization. + */ + private int id; + + /** + * The rights issuer address of this DRM object. + */ + private String rightsIssuer; + + /** + * The media content type of this DRM object. + */ + private String mediaType; + + /** + * The delivery method type of this DRM object. + */ + private int rawType; + + + /** + * Construct a DrmRawContent object. + * + * @param inRawdata object of DRM raw data stream. + * @param len the length of raw data can be read. + * @param mimeTypeStr the mime type of the DRM content. + */ + public DrmRawContent(InputStream inRawdata, int len, String mimeTypeStr) throws DrmException, IOException { + int mimeType; + + id = -1; + inData = new BufferedInputStream(inRawdata, 1024); + inDataLen = len; + + if (DRM_MIMETYPE_MESSAGE_STRING.equals(mimeTypeStr)) + mimeType = DRM_MIMETYPE_MESSAGE; + else if (DRM_MIMETYPE_CONTENT_STRING.equals(mimeTypeStr)) + mimeType = DRM_MIMETYPE_CONTENT; + else + throw new IllegalArgumentException("mimeType must be DRM_MIMETYPE_MESSAGE or DRM_MIMETYPE_CONTENT"); + + if (len <= 0) + throw new IllegalArgumentException("len must be > 0"); + + /* call native method to initialize this DRM content */ + id = nativeConstructDrmContent(inData, inDataLen, mimeType); + + if (JNI_DRM_FAILURE == id) + throw new DrmException("nativeConstructDrmContent() returned JNI_DRM_FAILURE"); + + /* init the rights issuer field. */ + rightsIssuer = nativeGetRightsAddress(); + + /* init the raw content type. */ + rawType = nativeGetDeliveryMethod(); + if (JNI_DRM_FAILURE == rawType) + throw new DrmException("nativeGetDeliveryMethod() returned JNI_DRM_FAILURE"); + + /* init the media content type. */ + mediaType = nativeGetContentType(); + if (null == mediaType) + throw new DrmException("nativeGetContentType() returned null"); + } + + /** + * Get rights address from raw Seperate Delivery content. + * + * @return the string of the rights issuer address, + * or null if no rights issuer. + */ + public String getRightsAddress() { + return rightsIssuer; + } + + /** + * Get the type of the raw DRM content. + * + * @return one of the following delivery type of this DRM content: + * #DRM_FORWARD_LOCK + * #DRM_COMBINED_DELIVERY + * #DRM_SEPARATE_DELIVERY + * #DRM_SEPARATE_DELIVERY_DM + */ + public int getRawType() { + return rawType; + } + + /** + * Get one InputStream object to read decrypted content. + * + * @param rights the rights object contain decrypted key. + * + * @return the InputStream object of decrypted media content. + */ + public InputStream getContentInputStream(DrmRights rights) { + if (null == rights) + throw new NullPointerException(); + + return new DrmInputStream(rights); + } + + /** + * Get the type of the decrypted media content. + * + * @return the decrypted media content type of this DRM content. + */ + public String getContentType() { + return mediaType; + } + + /** + * Get the length of the decrypted media content. + * + * @param rights the rights object contain decrypted key. + * + * @return the length of the decrypted media content. + * #DRM_UNKNOWN_DATA_LEN if the length is unknown currently. + */ + public int getContentLength(DrmRights rights) throws DrmException { + /** + * Because currently the media object associate with rights object + * has been handled in native logic, so here it is not need to deal + * the rights. But for the apps, it is mandatory for user to get + * the rights object before get the media content length. + */ + if (null == rights) + throw new NullPointerException(); + + int mediaLen = nativeGetContentLength(); + + if (JNI_DRM_FAILURE == mediaLen) + throw new DrmException("nativeGetContentLength() returned JNI_DRM_FAILURE"); + + if (JNI_DRM_UNKNOWN_DATA_LEN == mediaLen) + return DRM_UNKNOWN_DATA_LEN; + + return mediaLen; + } + + /** + * This class provide a InputStream to the DRM media content. + */ + class DrmInputStream extends InputStream + { + /** + * The flag to indicate whether this stream is closed or not. + */ + private boolean isClosed; + + /** + * The offset of this DRM content to be reset. + */ + private int offset; + + /** + * A byte of data to be readed. + */ + private byte[] b; + + /** + * Construct a DrmInputStream instance. + */ + public DrmInputStream(DrmRights rights) { + /** + * Because currently the media object associate with rights object + * has been handled in native logic, so here it is not need to deal + * the rights. But for the apps, it is mandatory for user to get + * the rights object before get the media content data. + */ + + isClosed = false; + offset = 0; + b = new byte[1]; + } + + /* Non-javadoc + * @see java.io.InputStream#available() + */ + public int available() throws IOException { + /* call native method to get this DRM decrypted media content length */ + int len = nativeGetContentLength(); + + if (JNI_DRM_FAILURE == len) + throw new IOException(); + + /* if the length is unknown, just return 0 for available value */ + if (JNI_DRM_UNKNOWN_DATA_LEN == len) + return 0; + + int availableLen = len - offset; + if (availableLen < 0) + throw new IOException(); + + return availableLen; + } + + /* Non-javadoc + * @see java.io.InputStream#read() + */ + public int read() throws IOException { + int res; + + res = read(b, 0, 1); + + if (-1 == res) + return -1; + + return b[0] & 0xff; + } + + /* Non-javadoc + * @see java.io.InputStream#read(byte) + */ + public int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } + + /* Non-javadoc + * @see java.io.InputStream#read(byte, int, int) + */ + public int read(byte[] b, int off, int len) throws IOException { + if (null == b) + throw new NullPointerException(); + if (off < 0 || len < 0 || off + len > b.length) + throw new IndexOutOfBoundsException(); + if (true == isClosed) + throw new IOException(); + + if (0 == len) + return 0; + + len = nativeReadContent(b, off, len, offset); + + if (JNI_DRM_FAILURE == len) + throw new IOException(); + else if (JNI_DRM_EOF == len) + return -1; + + offset += len; + + return len; + } + + /* Non-javadoc + * @see java.io.InputStream#markSupported() + */ + public boolean markSupported() { + return false; + } + + /* Non-javadoc + * @see java.io.InputStream#mark(int) + */ + public void mark(int readlimit) { + } + + /* Non-javadoc + * @see java.io.InputStream#reset() + */ + public void reset() throws IOException { + throw new IOException(); + } + + /* Non-javadoc + * @see java.io.InputStream#skip() + */ + public long skip(long n) throws IOException { + return 0; + } + + /* Non-javadoc + * @see java.io.InputStream#close() + */ + public void close() { + isClosed = true; + } + } + + /** + * native method: construct a DRM content according the mime type. + * + * @param data input DRM content data to be parsed. + * @param len the length of the data. + * @param mimeType the mime type of this DRM content. the value of this field includes: + * #DRM_MIMETYPE_MESSAGE + * #DRM_MIMETYPE_CONTENT + * + * @return #the id of the DRM content if succeed. + * #JNI_DRM_FAILURE if fail. + */ + private native int nativeConstructDrmContent(InputStream data, int len, int mimeType); + + /** + * native method: get this DRM content rights issuer. + * + * @return the address of rights issuer if in case of separate delivery. + * null if not separete delivery, or otherwise. + */ + private native String nativeGetRightsAddress(); + + /** + * native method: get this DRM content delivery type. + * + * @return the delivery method, the value may be one of the following: + * #DRM_FORWARD_LOCK + * #DRM_COMBINED_DELIVERY + * #DRM_SEPARATE_DELIVERY + * #DRM_SEPARATE_DELIVERY_DM + * #JNI_DRM_FAILURE if fail. + */ + private native int nativeGetDeliveryMethod(); + + /** + * native method: get a piece of media content data. + * + * @param buf the buffer to save DRM media content data. + * @param bufOff the offset of the buffer to start to save data. + * @param len the number of byte to read. + * @param mediaOff the offset of the media content data to start to read. + * + * @return the length of the media content data has been read. + * #JNI_DRM_EOF if reach to end of the media content. + * #JNI_DRM_FAILURE if fail. + */ + private native int nativeReadContent(byte[] buf, int bufOff, int len, int mediaOff); + + /** + * native method: get this DRM content type. + * + * @return the decrypted media content type. + * null if fail. + */ + private native String nativeGetContentType(); + + /** + * native method: get this DRM decrypted media content length. + * + * @return the length of decrypted media content. + * #JNI_DRM_FAILURE if fail. + * #JNI_DRM_UNKNOWN_DATA_LEN if the length is unknown currently. + */ + private native int nativeGetContentLength(); + + /** + * The finalizer of the DRMRawContent. Do some cleanup. + */ + protected native void finalize(); + + + /** + * Load the shared library to link the native methods. + */ + static { + try { + System.loadLibrary("drm1_jni"); + } + catch (UnsatisfiedLinkError ule) { + System.err.println("WARNING: Could not load libdrm1_jni.so"); + } + } +} diff --git a/media/java/android/drm/mobile1/DrmRights.java b/media/java/android/drm/mobile1/DrmRights.java new file mode 100644 index 0000000..bcccb6a --- /dev/null +++ b/media/java/android/drm/mobile1/DrmRights.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2007 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.drm.mobile1; + +/** + * This class provides interfaces to access the DRM rights. + */ +public class DrmRights { + /** + * The DRM permission of play. + */ + public static final int DRM_PERMISSION_PLAY = 1; + + /** + * The DRM permission of display. + */ + public static final int DRM_PERMISSION_DISPLAY = 2; + + /** + * The DRM permission of execute. + */ + public static final int DRM_PERMISSION_EXECUTE = 3; + + /** + * The DRM permission of print. + */ + public static final int DRM_PERMISSION_PRINT = 4; + + /** + * Successful operation. + */ + private static final int JNI_DRM_SUCCESS = 0; + + /** + * General failure. + */ + private static final int JNI_DRM_FAILURE = -1; + + /** + * The uid of this rights object. + */ + private String roId = ""; + + + /** + * Construct the DrmRights. + */ + public DrmRights() { + } + + /** + * Get the constraint of the given permission on this rights object. + * + * @param permission the given permission. + * + * @return a DrmConstraint instance. + */ + public DrmConstraintInfo getConstraint(int permission) { + DrmConstraintInfo c = new DrmConstraintInfo(); + + /* call native method to get latest constraint information */ + int res = nativeGetConstraintInfo(permission, c); + + if (JNI_DRM_FAILURE == res) + return null; + + return c; + } + + /** + * Consume the rights of the given permission. + * + * @param permission the given permission. + * + * @return true if consume success. + * false if consume failure. + */ + public boolean consumeRights(int permission) { + /* call native method to consume and update rights */ + int res = nativeConsumeRights(permission); + + if (JNI_DRM_FAILURE == res) + return false; + + return true; + } + + + /** + * native method: get the constraint information of the given permission. + * + * @param permission the given permission. + * @param constraint the instance of constraint. + * + * @return #JNI_DRM_SUCCESS if succeed. + * #JNI_DRM_FAILURE if fail. + */ + private native int nativeGetConstraintInfo(int permission, DrmConstraintInfo constraint); + + /** + * native method: consume the rights of the given permission. + * + * @param permission the given permission. + * + * @return #JNI_DRM_SUCCESS if succeed. + * #JNI_DRM_FAILURE if fail. + */ + private native int nativeConsumeRights(int permission); + + + /** + * Load the shared library to link the native methods. + */ + static { + try { + System.loadLibrary("drm1_jni"); + } + catch (UnsatisfiedLinkError ule) { + System.err.println("WARNING: Could not load libdrm1_jni.so"); + } + } +} diff --git a/media/java/android/drm/mobile1/DrmRightsManager.java b/media/java/android/drm/mobile1/DrmRightsManager.java new file mode 100644 index 0000000..1bc36ec --- /dev/null +++ b/media/java/android/drm/mobile1/DrmRightsManager.java @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2007 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.drm.mobile1; + +import java.io.*; +import java.util.*; + +/** + * This class provides interfaces to access the DRM right manager. + */ +public class DrmRightsManager { + /** + * The "application/vnd.oma.drm.rights+xml" mime type. + */ + public static final String DRM_MIMETYPE_RIGHTS_XML_STRING = "application/vnd.oma.drm.rights+xml"; + + /** + * The "application/vnd.oma.drm.rights+wbxml" mime type. + */ + public static final String DRM_MIMETYPE_RIGHTS_WBXML_STRING = "application/vnd.oma.drm.rights+wbxml"; + + /** + * The id of "application/vnd.oma.drm.rights+xml" mime type. + */ + private static final int DRM_MIMETYPE_RIGHTS_XML = 3; + + /** + * The id of "application/vnd.oma.drm.rights+wbxml" mime type. + */ + private static final int DRM_MIMETYPE_RIGHTS_WBXML = 4; + + /** + * The id of "application/vnd.oma.drm.message" mime type. + */ + private static final int DRM_MIMETYPE_MESSAGE = 1; + + /** + * Successful operation. + */ + private static final int JNI_DRM_SUCCESS = 0; + + /** + * General failure. + */ + private static final int JNI_DRM_FAILURE = -1; + + /** + * The instance of the rights manager. + */ + private static DrmRightsManager singleton = null; + + + /** + * Construct a DrmRightsManager + */ + protected DrmRightsManager() { + } + + /** + * Get the DrmRightsManager instance. + * + * @return the instance of DrmRightsManager. + */ + public static synchronized DrmRightsManager getInstance() { + if (singleton == null) { + singleton = new DrmRightsManager(); + } + + return singleton; + } + + /** + * Install one DRM rights and return one instance of DrmRights. + * + * @param rightsData raw rights data. + * @param mimeTypeStr the mime type of the rights object. + * + * @return the instance of the installed DrmRights. + */ + public synchronized DrmRights installRights(InputStream rightsData, int len, String mimeTypeStr) throws DrmException, IOException { + int mimeType = 0; + + if (DRM_MIMETYPE_RIGHTS_XML_STRING.equals(mimeTypeStr)) + mimeType = DRM_MIMETYPE_RIGHTS_XML; + else if (DRM_MIMETYPE_RIGHTS_WBXML_STRING.equals(mimeTypeStr)) + mimeType = DRM_MIMETYPE_RIGHTS_WBXML; + else if (DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equals(mimeTypeStr)) + mimeType = DRM_MIMETYPE_MESSAGE; + else + throw new IllegalArgumentException("mimeType must be DRM_MIMETYPE_RIGHTS_XML or DRM_MIMETYPE_RIGHTS_WBXML or DRM_MIMETYPE_MESSAGE"); + + if (len <= 0) + return null; + + DrmRights rights = new DrmRights(); + + /* call native method to install this rights object. */ + int res = nativeInstallDrmRights(rightsData, len, mimeType, rights); + + if (JNI_DRM_FAILURE == res) + throw new DrmException("nativeInstallDrmRights() returned JNI_DRM_FAILURE"); + + return rights; + } + + /** + * Query DRM rights of specified DRM raw content. + * + * @param content raw content object. + * + * @return the instance of DrmRights, or null if there is no rights. + */ + public synchronized DrmRights queryRights(DrmRawContent content) { + DrmRights rights = new DrmRights(); + + /* call native method to query the rights */ + int res = nativeQueryRights(content, rights); + + if (JNI_DRM_FAILURE == res) + return null; + + return rights; + } + + /** + * Get the list of all DRM rights saved in local client. + * + * @return the list of all the rights object. + */ + public synchronized List getRightsList() { + List rightsList = new ArrayList(); + + /* call native method to get how many rights object in current agent */ + int num = nativeGetNumOfRights(); + + if (JNI_DRM_FAILURE == num) + return null; + + if (num > 0) { + DrmRights[] rightsArray = new DrmRights[num]; + int i; + + for (i = 0; i < num; i++) + rightsArray[i] = new DrmRights(); + + /* call native method to get all the rights information */ + num = nativeGetRightsList(rightsArray, num); + + if (JNI_DRM_FAILURE == num) + return null; + + /* add all rights informations to ArrayList */ + for (i = 0; i < num; i++) + rightsList.add(rightsArray[i]); + } + + return rightsList; + } + + /** + * Delete the specified DRM rights object. + * + * @param rights the specified rights object to be deleted. + */ + public synchronized void deleteRights(DrmRights rights) { + /* call native method to delete the specified rights object */ + int res = nativeDeleteRights(rights); + + if (JNI_DRM_FAILURE == res) + return; + } + + + /** + * native method: install rights object to local client. + * + * @param data input DRM rights object data to be installed. + * @param len the length of the data. + * @param mimeType the mime type of this DRM rights object. the value of this field includes: + * #DRM_MIMETYPE_RIGHTS_XML + * #DRM_MIMETYPE_RIGHTS_WBXML + * @parma rights the instance of DRMRights to be filled. + * + * @return #JNI_DRM_SUCCESS if succeed. + * #JNI_DRM_FAILURE if fail. + */ + private native int nativeInstallDrmRights(InputStream data, int len, int mimeType, DrmRights rights); + + /** + * native method: query the given DRM content's rights object. + * + * @param content the given DRM content. + * @param rights the instance of rights to set if have. + * + * @return #JNI_DRM_SUCCESS if succeed. + * #JNI_DRM_FAILURE if fail. + */ + private native int nativeQueryRights(DrmRawContent content, DrmRights rights); + + /** + * native method: get how many rights object in current DRM agent. + * + * @return the number of the rights object. + * #JNI_DRM_FAILURE if fail. + */ + private native int nativeGetNumOfRights(); + + /** + * native method: get all the rights object in current local agent. + * + * @param rights the array instance of rights object. + * @param numRights how many rights can be saved. + * + * @return the number of the rights object has been gotten. + * #JNI_DRM_FAILURE if fail. + */ + private native int nativeGetRightsList(DrmRights[] rights, int numRights); + + /** + * native method: delete a specified rights object. + * + * @param rights the specified rights object to be deleted. + * + * @return #JNI_DRM_SUCCESS if succeed. + * #JNI_DRM_FAILURE if fail. + */ + private native int nativeDeleteRights(DrmRights rights); + + + /** + * Load the shared library to link the native methods. + */ + static { + try { + System.loadLibrary("drm1_jni"); + } + catch (UnsatisfiedLinkError ule) { + System.err.println("WARNING: Could not load libdrm1_jni.so"); + } + } +} diff --git a/media/java/android/drm/mobile1/package.html b/media/java/android/drm/mobile1/package.html new file mode 100644 index 0000000..1c9bf9d --- /dev/null +++ b/media/java/android/drm/mobile1/package.html @@ -0,0 +1,5 @@ +<html> +<body> + {@hide} +</body> +</html> diff --git a/media/java/android/media/AmrInputStream.java b/media/java/android/media/AmrInputStream.java new file mode 100644 index 0000000..d40ca5a --- /dev/null +++ b/media/java/android/media/AmrInputStream.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2008 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 android.util.Config; +import android.util.Log; + +import java.io.InputStream; +import java.io.IOException; + + +/** + * AmrInputStream + * @hide + */ +public final class AmrInputStream extends InputStream +{ + static { + System.loadLibrary("media_jni"); + } + + private final static String TAG = "AmrInputStream"; + + // frame is 20 msec at 8.000 khz + private final static int SAMPLES_PER_FRAME = 8000 * 20 / 1000; + + // pcm input stream + private InputStream mInputStream; + + // native handle + private int mGae; + + // result amr stream + private byte[] mBuf = new byte[SAMPLES_PER_FRAME * 2]; + private int mBufIn = 0; + private int mBufOut = 0; + + // helper for bytewise read() + private byte[] mOneByte = new byte[1]; + + /** + * Create a new AmrInputStream, which converts 16 bit PCM to AMR + * @param inputStream InputStream containing 16 bit PCM. + */ + public AmrInputStream(InputStream inputStream) { + mInputStream = inputStream; + mGae = GsmAmrEncoderNew(); + GsmAmrEncoderInitialize(mGae); + } + + @Override + public int read() throws IOException { + int rtn = read(mOneByte, 0, 1); + return rtn == 1 ? (0xff & mOneByte[0]) : -1; + } + + @Override + public int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } + + @Override + public int read(byte[] b, int offset, int length) throws IOException { + if (mGae == 0) throw new IllegalStateException("not open"); + + // local buffer of amr encoded audio empty + if (mBufOut >= mBufIn) { + // reset the buffer + mBufOut = 0; + mBufIn = 0; + + // fetch a 20 msec frame of pcm + for (int i = 0; i < SAMPLES_PER_FRAME * 2; ) { + int n = mInputStream.read(mBuf, i, SAMPLES_PER_FRAME * 2 - i); + if (n == -1) return -1; + i += n; + } + + // encode it + mBufIn = GsmAmrEncoderEncode(mGae, mBuf, 0, mBuf, 0); + } + + // return encoded audio to user + if (length > mBufIn - mBufOut) length = mBufIn - mBufOut; + System.arraycopy(mBuf, mBufOut, b, offset, length); + mBufOut += length; + + return length; + } + + @Override + public void close() throws IOException { + try { + if (mInputStream != null) mInputStream.close(); + } finally { + mInputStream = null; + try { + if (mGae != 0) GsmAmrEncoderCleanup(mGae); + } finally { + try { + if (mGae != 0) GsmAmrEncoderDelete(mGae); + } finally { + mGae = 0; + } + } + } + } + + @Override + protected void finalize() throws Throwable { + if (mGae != 0) { + close(); + throw new IllegalStateException("someone forgot to close AmrInputStream"); + } + } + + // + // AudioRecord JNI interface + // + private static native int GsmAmrEncoderNew(); + private static native void GsmAmrEncoderInitialize(int gae); + private static native int GsmAmrEncoderEncode(int gae, + byte[] pcm, int pcmOffset, byte[] amr, int amrOffset) throws IOException; + private static native void GsmAmrEncoderCleanup(int gae); + private static native void GsmAmrEncoderDelete(int gae); + +} diff --git a/media/java/android/media/AsyncPlayer.java b/media/java/android/media/AsyncPlayer.java new file mode 100644 index 0000000..35f0409 --- /dev/null +++ b/media/java/android/media/AsyncPlayer.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2008 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 android.content.Context; +import android.net.Uri; +import android.os.PowerManager; +import android.util.Log; + +import java.io.IOException; +import java.lang.IllegalStateException; + +/** + * Plays a series of audio URIs, but does all the hard work on another thread + * so that any slowness with preparing or loading doesn't block the calling thread. + */ +public class AsyncPlayer { + private static final int PLAY = 1; + private static final int STOP = 2; + + private static final class Command { + Command next; + int code; + Context context; + Uri uri; + boolean looping; + int stream; + + public String toString() { + return "{ code=" + code + " looping=" + looping + " stream=" + stream + + " uri=" + uri + " }"; + } + } + + private final class Thread extends java.lang.Thread { + Thread() { + super("AsyncPlayer-" + mTag); + } + + public void run() { + while (true) { + Command cmd = null; + + synchronized (mLock) { + if (mHead != null) { + cmd = mHead; + mHead = cmd.next; + if (mTail == cmd) { + mTail = null; + } + } + } + + switch (cmd.code) { + case PLAY: + try { + // Preparing can be slow, so if there is something else + // is playing, let it continue until we're done, so there + // is less of a glitch. + MediaPlayer player = new MediaPlayer(); + player.setAudioStreamType(cmd.stream); + player.setDataSource(cmd.context, cmd.uri); + player.setLooping(cmd.looping); + player.prepare(); + player.start(); + if (mPlayer != null) { + mPlayer.release(); + } + mPlayer = player; + } + catch (IOException e) { + Log.w(mTag, "error loading sound for " + cmd.uri, e); + } catch (IllegalStateException e) { + Log.w(mTag, "IllegalStateException (content provider died?) " + cmd.uri, e); + } + break; + case STOP: + if (mPlayer != null) { + mPlayer.stop(); + mPlayer.release(); + mPlayer = null; + } else { + Log.w(mTag, "STOP command without a player"); + } + break; + } + + synchronized (mLock) { + if (mHead == null) { + // nothing left to do, quit + // doing this check after we're done prevents the case where they + // added it during the operation from spawning two threads and + // trying to do them in parallel. + mThread = null; + releaseWakeLock(); + return; + } + } + } + } + } + + private String mTag; + private Command mHead; + private Command mTail; + private Thread mThread; + private MediaPlayer mPlayer; + private Object mLock = new Object(); + private PowerManager.WakeLock mWakeLock; + + // The current state according to the caller. Reality lags behind + // because of the asynchronous nature of this class. + private int mState = STOP; + + /** + * Construct an AsyncPlayer object. + * + * @param tag a string to use for debugging + */ + public AsyncPlayer(String tag) { + if (tag != null) { + mTag = tag; + } else { + mTag = "AsyncPlayer"; + } + } + + /** + * Start playing the sound. It will actually start playing at some + * point in the future. There are no guarantees about latency here. + * Calling this before another audio file is done playing will stop + * that one and start the new one. + * + * @param context Your application's context. + * @param uri The URI to play. (see {@link MediaPlayer#setDataSource(Context, Uri)}) + * @param looping Whether the audio should loop forever. + * (see {@link MediaPlayer#setLooping(boolean)}) + * @param stream the AudioStream to use. + * (see {@link MediaPlayer#setAudioStreamType(int)}) + */ + public void play(Context context, Uri uri, boolean looping, int stream) { + Command cmd = new Command(); + cmd.code = PLAY; + cmd.context = context; + cmd.uri = uri; + cmd.looping = looping; + cmd.stream = stream; + synchronized (mLock) { + enqueueLocked(cmd); + mState = PLAY; + } + } + + /** + * Stop a previously played sound. It can't be played again or unpaused + * at this point. Calling this multiple times has no ill effects. + */ + public void stop() { + synchronized (mLock) { + // This check allows stop to be called multiple times without starting + // a thread that ends up doing nothing. + if (mState != STOP) { + Command cmd = new Command(); + cmd.code = STOP; + enqueueLocked(cmd); + mState = STOP; + } + } + } + + private void enqueueLocked(Command cmd) { + if (mTail == null) { + mHead = cmd; + } else { + mTail.next = cmd; + } + mTail = cmd; + if (mThread == null) { + acquireWakeLock(); + mThread = new Thread(); + mThread.start(); + } + } + + /** + * We want to hold a wake lock while we do the prepare and play. The stop probably is + * optional, but it won't hurt to have it too. The problem is that if you start a sound + * while you're holding a wake lock (e.g. an alarm starting a notification), you want the + * sound to play, but if the CPU turns off before mThread gets to work, it won't. The + * simplest way to deal with this is to make it so there is a wake lock held while the + * thread is starting or running. You're going to need the WAKE_LOCK permission if you're + * going to call this. + * + * This must be called before the first time play is called. + * + * @hide + */ + public void setUsesWakeLock(Context context) { + if (mWakeLock != null || mThread != null) { + // if either of these has happened, we've already played something. + // and our releases will be out of sync. + throw new RuntimeException("assertion failed mWakeLock=" + mWakeLock + + " mThread=" + mThread); + } + PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE); + mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, mTag); + } + + private void acquireWakeLock() { + if (mWakeLock != null) { + mWakeLock.acquire(); + } + } + + private void releaseWakeLock() { + if (mWakeLock != null) { + mWakeLock.release(); + } + } +} + diff --git a/media/java/android/media/AudioFormat.java b/media/java/android/media/AudioFormat.java new file mode 100644 index 0000000..0732b61 --- /dev/null +++ b/media/java/android/media/AudioFormat.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2008 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; + +/** + * The AudioFormat class is used to access a number of audio format and + * channel configuration constants. They are for instance used + * in __link AudioTrack} and __link AudioRecord}. + * + */ +public class AudioFormat { + + //--------------------------------------------------------- + // Constants + //-------------------- + /** Invalid audio data format */ + public static final int ENCODING_INVALID = 0; + /** Default audio data format */ + public static final int ENCODING_DEFAULT = 1; + /** Audio data format: PCM 16 bit per sample */ + public static final int ENCODING_PCM_16BIT = 2; // accessed by native code + /** Audio data format: PCM 8 bit per sample */ + public static final int ENCODING_PCM_8BIT = 3; // accessed by native code + + /** Invalid audio channel configuration */ + public static final int CHANNEL_CONFIGURATION_INVALID = 0; + /** Default audio channel configuration */ + public static final int CHANNEL_CONFIGURATION_DEFAULT = 1; + /** Mono audio configuration */ + public static final int CHANNEL_CONFIGURATION_MONO = 2; + /** Stereo (2 channel) audio configuration */ + public static final int CHANNEL_CONFIGURATION_STEREO = 3; + +} + + + diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java new file mode 100644 index 0000000..077d016 --- /dev/null +++ b/media/java/android/media/AudioManager.java @@ -0,0 +1,1086 @@ +/* + * Copyright (C) 2007 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 android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.content.Context; +import android.database.ContentObserver; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.provider.Settings; +import android.util.Log; + +/** + * AudioManager provides access to volume and ringer mode control. + * <p> + * Use <code>Context.getSystemService(Context.AUDIO_SERVICE)</code> to get + * an instance of this class. + */ +public class AudioManager { + + private final Context mContext; + private final Handler mHandler; + + // used to listen for updates to the sound effects settings so we don't + // poll it for every UI sound + private ContentObserver mContentObserver; + + + private static String TAG = "AudioManager"; + private static boolean DEBUG = false; + private static boolean localLOGV = DEBUG || android.util.Config.LOGV; + + /** + * Broadcast intent, a hint for applications that audio is about to become + * 'noisy' due to a change in audio outputs. For example, this intent may + * be sent when a wired headset is unplugged, or when an A2DP audio + * sink is disconnected, and the audio system is about to automatically + * switch audio route to the speaker. Applications that are controlling + * audio streams may consider pausing, reducing volume or some other action + * on receipt of this intent so as not to surprise the user with audio + * from the speaker. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_AUDIO_BECOMING_NOISY = "android.media.AUDIO_BECOMING_NOISY"; + + /** + * Sticky broadcast intent action indicating that the ringer mode has + * changed. Includes the new ringer mode. + * + * @see #EXTRA_RINGER_MODE + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String RINGER_MODE_CHANGED_ACTION = "android.media.RINGER_MODE_CHANGED"; + + /** + * The new ringer mode. + * + * @see #RINGER_MODE_CHANGED_ACTION + * @see #RINGER_MODE_NORMAL + * @see #RINGER_MODE_SILENT + * @see #RINGER_MODE_VIBRATE + */ + public static final String EXTRA_RINGER_MODE = "android.media.EXTRA_RINGER_MODE"; + + /** + * Broadcast intent action indicating that the vibrate setting has + * changed. Includes the vibrate type and its new setting. + * + * @see #EXTRA_VIBRATE_TYPE + * @see #EXTRA_VIBRATE_SETTING + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String VIBRATE_SETTING_CHANGED_ACTION = "android.media.VIBRATE_SETTING_CHANGED"; + + /** + * @hide Broadcast intent when the volume for a particular stream type changes. + * Includes the stream and the new volume + * + * @see #EXTRA_VOLUME_STREAM_TYPE + * @see #EXTRA_VOLUME_STREAM_VALUE + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String VOLUME_CHANGED_ACTION = "android.media.VOLUME_CHANGED_ACTION"; + + /** + * The new vibrate setting for a particular type. + * + * @see #VIBRATE_SETTING_CHANGED_ACTION + * @see #EXTRA_VIBRATE_TYPE + * @see #VIBRATE_SETTING_ON + * @see #VIBRATE_SETTING_OFF + * @see #VIBRATE_SETTING_ONLY_SILENT + */ + public static final String EXTRA_VIBRATE_SETTING = "android.media.EXTRA_VIBRATE_SETTING"; + + /** + * The vibrate type whose setting has changed. + * + * @see #VIBRATE_SETTING_CHANGED_ACTION + * @see #VIBRATE_TYPE_NOTIFICATION + * @see #VIBRATE_TYPE_RINGER + */ + public static final String EXTRA_VIBRATE_TYPE = "android.media.EXTRA_VIBRATE_TYPE"; + + /** + * @hide The stream type for the volume changed intent. + */ + public static final String EXTRA_VOLUME_STREAM_TYPE = "android.media.EXTRA_VOLUME_STREAM_TYPE"; + + /** + * @hide The volume associated with the stream for the volume changed intent. + */ + public static final String EXTRA_VOLUME_STREAM_VALUE = + "android.media.EXTRA_VOLUME_STREAM_VALUE"; + + /** The audio stream for phone calls */ + public static final int STREAM_VOICE_CALL = AudioSystem.STREAM_VOICE_CALL; + /** The audio stream for system sounds */ + public static final int STREAM_SYSTEM = AudioSystem.STREAM_SYSTEM; + /** The audio stream for the phone ring */ + public static final int STREAM_RING = AudioSystem.STREAM_RING; + /** The audio stream for music playback */ + public static final int STREAM_MUSIC = AudioSystem.STREAM_MUSIC; + /** The audio stream for alarms */ + public static final int STREAM_ALARM = AudioSystem.STREAM_ALARM; + /** The audio stream for notifications */ + public static final int STREAM_NOTIFICATION = AudioSystem.STREAM_NOTIFICATION; + /** @hide The audio stream for phone calls when connected to bluetooth */ + public static final int STREAM_BLUETOOTH_SCO = AudioSystem.STREAM_BLUETOOTH_SCO; + /** Number of audio streams */ + /** + * @deprecated Use AudioSystem.getNumStreamTypes() instead + */ + public static final int NUM_STREAMS = AudioSystem.NUM_STREAMS; + + + /** @hide Maximum volume index values for audio streams */ + public static final int[] MAX_STREAM_VOLUME = new int[] { + 6, // STREAM_VOICE_CALL + 8, // STREAM_SYSTEM + 8, // STREAM_RING + 16, // STREAM_MUSIC + 8, // STREAM_ALARM + 8, // STREAM_NOTIFICATION + 15, // STREAM_BLUETOOTH_SCO + }; + + /** @hide Default volume index values for audio streams */ + public static final int[] DEFAULT_STREAM_VOLUME = new int[] { + 4, // STREAM_VOICE_CALL + 5, // STREAM_SYSTEM + 5, // STREAM_RING + 11, // STREAM_MUSIC + 6, // STREAM_ALARM + 5, // STREAM_NOTIFICATION + 7 // STREAM_BLUETOOTH_SCO + }; + + /** + * Increase the ringer volume. + * + * @see #adjustVolume(int, int) + * @see #adjustStreamVolume(int, int, int) + */ + public static final int ADJUST_RAISE = 1; + + /** + * Decrease the ringer volume. + * + * @see #adjustVolume(int, int) + * @see #adjustStreamVolume(int, int, int) + */ + public static final int ADJUST_LOWER = -1; + + /** + * Maintain the previous ringer volume. This may be useful when needing to + * show the volume toast without actually modifying the volume. + * + * @see #adjustVolume(int, int) + * @see #adjustStreamVolume(int, int, int) + */ + public static final int ADJUST_SAME = 0; + + // Flags should be powers of 2! + + /** + * Show a toast containing the current volume. + * + * @see #adjustStreamVolume(int, int, int) + * @see #adjustVolume(int, int) + * @see #setStreamVolume(int, int, int) + * @see #setRingerMode(int) + */ + public static final int FLAG_SHOW_UI = 1 << 0; + + /** + * Whether to include ringer modes as possible options when changing volume. + * For example, if true and volume level is 0 and the volume is adjusted + * with {@link #ADJUST_LOWER}, then the ringer mode may switch the silent or + * vibrate mode. + * <p> + * By default this is on for the ring stream. If this flag is included, + * this behavior will be present regardless of the stream type being + * affected by the ringer mode. + * + * @see #adjustVolume(int, int) + * @see #adjustStreamVolume(int, int, int) + */ + public static final int FLAG_ALLOW_RINGER_MODES = 1 << 1; + + /** + * Whether to play a sound when changing the volume. + * <p> + * If this is given to {@link #adjustVolume(int, int)} or + * {@link #adjustSuggestedStreamVolume(int, int, int)}, it may be ignored + * in some cases (for example, the decided stream type is not + * {@link AudioManager#STREAM_RING}, or the volume is being adjusted + * downward). + * + * @see #adjustStreamVolume(int, int, int) + * @see #adjustVolume(int, int) + * @see #setStreamVolume(int, int, int) + */ + public static final int FLAG_PLAY_SOUND = 1 << 2; + + /** + * Removes any sounds/vibrate that may be in the queue, or are playing (related to + * changing volume). + */ + public static final int FLAG_REMOVE_SOUND_AND_VIBRATE = 1 << 3; + + /** + * Whether to vibrate if going into the vibrate ringer mode. + */ + public static final int FLAG_VIBRATE = 1 << 4; + + /** + * Ringer mode that will be silent and will not vibrate. (This overrides the + * vibrate setting.) + * + * @see #setRingerMode(int) + * @see #getRingerMode() + */ + public static final int RINGER_MODE_SILENT = 0; + + /** + * Ringer mode that will be silent and will vibrate. (This will cause the + * phone ringer to always vibrate, but the notification vibrate to only + * vibrate if set.) + * + * @see #setRingerMode(int) + * @see #getRingerMode() + */ + public static final int RINGER_MODE_VIBRATE = 1; + + /** + * Ringer mode that may be audible and may vibrate. It will be audible if + * the volume before changing out of this mode was audible. It will vibrate + * if the vibrate setting is on. + * + * @see #setRingerMode(int) + * @see #getRingerMode() + */ + public static final int RINGER_MODE_NORMAL = 2; + + /** + * Vibrate type that corresponds to the ringer. + * + * @see #setVibrateSetting(int, int) + * @see #getVibrateSetting(int) + * @see #shouldVibrate(int) + */ + public static final int VIBRATE_TYPE_RINGER = 0; + + /** + * Vibrate type that corresponds to notifications. + * + * @see #setVibrateSetting(int, int) + * @see #getVibrateSetting(int) + * @see #shouldVibrate(int) + */ + public static final int VIBRATE_TYPE_NOTIFICATION = 1; + + /** + * Vibrate setting that suggests to never vibrate. + * + * @see #setVibrateSetting(int, int) + * @see #getVibrateSetting(int) + */ + public static final int VIBRATE_SETTING_OFF = 0; + + /** + * Vibrate setting that suggests to vibrate when possible. + * + * @see #setVibrateSetting(int, int) + * @see #getVibrateSetting(int) + */ + public static final int VIBRATE_SETTING_ON = 1; + + /** + * Vibrate setting that suggests to only vibrate when in the vibrate ringer + * mode. + * + * @see #setVibrateSetting(int, int) + * @see #getVibrateSetting(int) + */ + public static final int VIBRATE_SETTING_ONLY_SILENT = 2; + + /** + * Suggests using the default stream type. This may not be used in all + * places a stream type is needed. + */ + public static final int USE_DEFAULT_STREAM_TYPE = Integer.MIN_VALUE; + + private static IAudioService sService; + + /** + * @hide + */ + public AudioManager(Context context) { + mContext = context; + mHandler = new Handler(context.getMainLooper()); + } + + private static IAudioService getService() + { + if (sService != null) { + return sService; + } + IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE); + sService = IAudioService.Stub.asInterface(b); + return sService; + } + + /** + * Adjusts the volume of a particular stream by one step in a direction. + * + * @param streamType The stream type to adjust. One of {@link #STREAM_VOICE_CALL}, + * {@link #STREAM_SYSTEM}, {@link #STREAM_RING}, {@link #STREAM_MUSIC} or + * {@link #STREAM_ALARM} + * @param direction The direction to adjust the volume. One of + * {@link #ADJUST_LOWER}, {@link #ADJUST_RAISE}, or + * {@link #ADJUST_SAME}. + * @param flags One or more flags. + * @see #adjustVolume(int, int) + * @see #setStreamVolume(int, int, int) + */ + public void adjustStreamVolume(int streamType, int direction, int flags) { + IAudioService service = getService(); + try { + service.adjustStreamVolume(streamType, direction, flags); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in adjustStreamVolume", e); + } + } + + /** + * Adjusts the volume of the most relevant stream. For example, if a call is + * active, it will have the highest priority regardless of if the in-call + * screen is showing. Another example, if music is playing in the background + * and a call is not active, the music stream will be adjusted. + * + * @param direction The direction to adjust the volume. One of + * {@link #ADJUST_LOWER}, {@link #ADJUST_RAISE}, or + * {@link #ADJUST_SAME}. + * @param flags One or more flags. + * @see #adjustSuggestedStreamVolume(int, int, int) + * @see #adjustStreamVolume(int, int, int) + * @see #setStreamVolume(int, int, int) + */ + public void adjustVolume(int direction, int flags) { + IAudioService service = getService(); + try { + service.adjustVolume(direction, flags); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in adjustVolume", e); + } + } + + /** + * Adjusts the volume of the most relevant stream, or the given fallback + * stream. + * + * @param direction The direction to adjust the volume. One of + * {@link #ADJUST_LOWER}, {@link #ADJUST_RAISE}, or + * {@link #ADJUST_SAME}. + * @param suggestedStreamType The stream type that will be used if there + * isn't a relevant stream. {@link #USE_DEFAULT_STREAM_TYPE} is valid here. + * @param flags One or more flags. + * @see #adjustVolume(int, int) + * @see #adjustStreamVolume(int, int, int) + * @see #setStreamVolume(int, int, int) + */ + public void adjustSuggestedStreamVolume(int direction, int suggestedStreamType, int flags) { + IAudioService service = getService(); + try { + service.adjustSuggestedStreamVolume(direction, suggestedStreamType, flags); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in adjustVolume", e); + } + } + + /** + * Returns the current ringtone mode. + * + * @return The current ringtone mode, one of {@link #RINGER_MODE_NORMAL}, + * {@link #RINGER_MODE_SILENT}, or {@link #RINGER_MODE_VIBRATE}. + * @see #setRingerMode(int) + */ + public int getRingerMode() { + IAudioService service = getService(); + try { + return service.getRingerMode(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getRingerMode", e); + return RINGER_MODE_NORMAL; + } + } + + /** + * Returns the maximum volume index for a particular stream. + * + * @param streamType The stream type whose maximum volume index is returned. + * @return The maximum valid volume index for the stream. + * @see #getStreamVolume(int) + */ + public int getStreamMaxVolume(int streamType) { + IAudioService service = getService(); + try { + return service.getStreamMaxVolume(streamType); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getStreamMaxVolume", e); + return 0; + } + } + + /** + * Returns the current volume index for a particular stream. + * + * @param streamType The stream type whose volume index is returned. + * @return The current volume index for the stream. + * @see #getStreamMaxVolume(int) + * @see #setStreamVolume(int, int, int) + */ + public int getStreamVolume(int streamType) { + IAudioService service = getService(); + try { + return service.getStreamVolume(streamType); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getStreamVolume", e); + return 0; + } + } + + /** + * Sets the ringer mode. + * <p> + * Silent mode will mute the volume and will not vibrate. Vibrate mode will + * mute the volume and vibrate. Normal mode will be audible and may vibrate + * according to user settings. + * + * @param ringerMode The ringer mode, one of {@link #RINGER_MODE_NORMAL}, + * {@link #RINGER_MODE_SILENT}, or {@link #RINGER_MODE_VIBRATE}. + * @see #getRingerMode() + */ + public void setRingerMode(int ringerMode) { + IAudioService service = getService(); + try { + service.setRingerMode(ringerMode); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in setRingerMode", e); + } + } + + /** + * Sets the volume index for a particular stream. + * + * @param streamType The stream whose volume index should be set. + * @param index The volume index to set. See + * {@link #getStreamMaxVolume(int)} for the largest valid value. + * @param flags One or more flags. + * @see #getStreamMaxVolume(int) + * @see #getStreamVolume(int) + */ + public void setStreamVolume(int streamType, int index, int flags) { + IAudioService service = getService(); + try { + service.setStreamVolume(streamType, index, flags); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in setStreamVolume", e); + } + } + + /** + * Solo or unsolo a particular stream. All other streams are muted. + * <p> + * The solo command is protected against client process death: if a process + * with an active solo request on a stream dies, all streams that were muted + * because of this request will be unmuted automatically. + * <p> + * The solo requests for a given stream are cumulative: the AudioManager + * can receive several solo requests from one or more clients and the stream + * will be unsoloed only when the same number of unsolo requests are received. + * <p> + * For a better user experience, applications MUST unsolo a soloed stream + * in onPause() and solo is again in onResume() if appropriate. + * + * @param streamType The stream to be soloed/unsoloed. + * @param state The required solo state: true for solo ON, false for solo OFF + */ + public void setStreamSolo(int streamType, boolean state) { + IAudioService service = getService(); + try { + service.setStreamSolo(streamType, state, mICallBack); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in setStreamSolo", e); + } + } + + /** + * Mute or unmute an audio stream. + * <p> + * The mute command is protected against client process death: if a process + * with an active mute request on a stream dies, this stream will be unmuted + * automatically. + * <p> + * The mute requests for a given stream are cumulative: the AudioManager + * can receive several mute requests from one or more clients and the stream + * will be unmuted only when the same number of unmute requests are received. + * <p> + * For a better user experience, applications MUST unmute a muted stream + * in onPause() and mute is again in onResume() if appropriate. + * + * @param streamType The stream to be muted/unmuted. + * @param state The required mute state: true for mute ON, false for mute OFF + */ + public void setStreamMute(int streamType, boolean state) { + IAudioService service = getService(); + try { + service.setStreamMute(streamType, state, mICallBack); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in setStreamMute", e); + } + } + + /** + * Returns whether a particular type should vibrate according to user + * settings and the current ringer mode. + * <p> + * This shouldn't be needed by most clients that use notifications to + * vibrate. The notification manager will not vibrate if the policy doesn't + * allow it, so the client should always set a vibrate pattern and let the + * notification manager control whether or not to actually vibrate. + * + * @param vibrateType The type of vibrate. One of + * {@link #VIBRATE_TYPE_NOTIFICATION} or + * {@link #VIBRATE_TYPE_RINGER}. + * @return Whether the type should vibrate at the instant this method is + * called. + * @see #setVibrateSetting(int, int) + * @see #getVibrateSetting(int) + */ + public boolean shouldVibrate(int vibrateType) { + IAudioService service = getService(); + try { + return service.shouldVibrate(vibrateType); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in shouldVibrate", e); + return false; + } + } + + /** + * Returns whether the user's vibrate setting for a vibrate type. + * <p> + * This shouldn't be needed by most clients that want to vibrate, instead + * see {@link #shouldVibrate(int)}. + * + * @param vibrateType The type of vibrate. One of + * {@link #VIBRATE_TYPE_NOTIFICATION} or + * {@link #VIBRATE_TYPE_RINGER}. + * @return The vibrate setting, one of {@link #VIBRATE_SETTING_ON}, + * {@link #VIBRATE_SETTING_OFF}, or + * {@link #VIBRATE_SETTING_ONLY_SILENT}. + * @see #setVibrateSetting(int, int) + * @see #shouldVibrate(int) + */ + public int getVibrateSetting(int vibrateType) { + IAudioService service = getService(); + try { + return service.getVibrateSetting(vibrateType); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getVibrateSetting", e); + return VIBRATE_SETTING_OFF; + } + } + + /** + * Sets the setting for when the vibrate type should vibrate. + * + * @param vibrateType The type of vibrate. One of + * {@link #VIBRATE_TYPE_NOTIFICATION} or + * {@link #VIBRATE_TYPE_RINGER}. + * @param vibrateSetting The vibrate setting, one of + * {@link #VIBRATE_SETTING_ON}, + * {@link #VIBRATE_SETTING_OFF}, or + * {@link #VIBRATE_SETTING_ONLY_SILENT}. + * @see #getVibrateSetting(int) + * @see #shouldVibrate(int) + */ + public void setVibrateSetting(int vibrateType, int vibrateSetting) { + IAudioService service = getService(); + try { + service.setVibrateSetting(vibrateType, vibrateSetting); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in setVibrateSetting", e); + } + } + + /** + * Sets the speakerphone on or off. + * + * @param on set <var>true</var> to turn on speakerphone; + * <var>false</var> to turn it off + */ + public void setSpeakerphoneOn(boolean on){ + setRouting(MODE_IN_CALL, on ? ROUTE_SPEAKER : ROUTE_EARPIECE, ROUTE_ALL); + } + + /** + * Checks whether the speakerphone is on or off. + * + * @return true if speakerphone is on, false if it's off + */ + public boolean isSpeakerphoneOn() { + return (getRouting(MODE_IN_CALL) & ROUTE_SPEAKER) == 0 ? false : true; + } + + /** + * Sets audio routing to the Bluetooth headset on or off. + * + * @param on set <var>true</var> to route SCO (voice) audio to/from Bluetooth + * headset; <var>false</var> to route audio to/from phone earpiece + */ + public void setBluetoothScoOn(boolean on){ + // Don't disable A2DP when turning off SCO. + // A2DP does not affect in-call routing. + setRouting(MODE_RINGTONE, + on ? ROUTE_BLUETOOTH_SCO: ROUTE_SPEAKER, ROUTE_ALL & ~ROUTE_BLUETOOTH_A2DP); + setRouting(MODE_NORMAL, + on ? ROUTE_BLUETOOTH_SCO: ROUTE_SPEAKER, ROUTE_ALL & ~ROUTE_BLUETOOTH_A2DP); + setRouting(MODE_IN_CALL, + on ? ROUTE_BLUETOOTH_SCO: ROUTE_EARPIECE, ROUTE_ALL); + } + + /** + * Checks whether audio routing to the Bluetooth headset is on or off. + * + * @return true if SCO audio is being routed to/from Bluetooth headset; + * false if otherwise + */ + public boolean isBluetoothScoOn() { + return (getRouting(MODE_IN_CALL) & ROUTE_BLUETOOTH_SCO) == 0 ? false : true; + } + + /** + * Sets A2DP audio routing to the Bluetooth headset on or off. + * + * @param on set <var>true</var> to route A2DP audio to/from Bluetooth + * headset; <var>false</var> disable A2DP audio + */ + public void setBluetoothA2dpOn(boolean on){ + // the audio flinger chooses A2DP as a higher priority, + // so there is no need to disable other routes. + setRouting(MODE_RINGTONE, + on ? ROUTE_BLUETOOTH_A2DP: 0, ROUTE_BLUETOOTH_A2DP); + setRouting(MODE_NORMAL, + on ? ROUTE_BLUETOOTH_A2DP: 0, ROUTE_BLUETOOTH_A2DP); + } + + /** + * Checks whether A2DP audio routing to the Bluetooth headset is on or off. + * + * @return true if A2DP audio is being routed to/from Bluetooth headset; + * false if otherwise + */ + public boolean isBluetoothA2dpOn() { + return (getRouting(MODE_NORMAL) & ROUTE_BLUETOOTH_A2DP) == 0 ? false : true; + } + + /** + * Sets audio routing to the wired headset on or off. + * + * @param on set <var>true</var> to route audio to/from wired + * headset; <var>false</var> disable wired headset audio + * @hide + */ + public void setWiredHeadsetOn(boolean on){ + // A2DP has higher priority than wired headset, so headset connect/disconnect events + // should not affect A2DP routing + setRouting(MODE_NORMAL, + on ? ROUTE_HEADSET : ROUTE_SPEAKER, ROUTE_ALL & ~ROUTE_BLUETOOTH_A2DP); + setRouting(MODE_RINGTONE, + on ? ROUTE_HEADSET | ROUTE_SPEAKER : ROUTE_SPEAKER, ROUTE_ALL & ~ROUTE_BLUETOOTH_A2DP); + setRouting(MODE_IN_CALL, + on ? ROUTE_HEADSET : ROUTE_EARPIECE, ROUTE_ALL); + } + + /** + * Checks whether audio routing to the wired headset is on or off. + * + * @return true if audio is being routed to/from wired headset; + * false if otherwise + * @hide + */ + public boolean isWiredHeadsetOn() { + return (getRouting(MODE_NORMAL) & ROUTE_HEADSET) == 0 ? false : true; + } + + /** + * Sets the microphone mute on or off. + * + * @param on set <var>true</var> to mute the microphone; + * <var>false</var> to turn mute off + */ + public void setMicrophoneMute(boolean on){ + IAudioService service = getService(); + try { + service.setMicrophoneMute(on); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in setMicrophoneMute", e); + } + } + + /** + * Checks whether the microphone mute is on or off. + * + * @return true if microphone is muted, false if it's not + */ + public boolean isMicrophoneMute() { + IAudioService service = getService(); + try { + return service.isMicrophoneMute(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in isMicrophoneMute", e); + return false; + } + } + + /** + * Sets the audio mode. + * + * @param mode the requested audio mode (NORMAL, RINGTONE, or IN_CALL). + * Informs the HAL about the current audio state so that + * it can route the audio appropriately. + */ + public void setMode(int mode) { + IAudioService service = getService(); + try { + service.setMode(mode); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in setMode", e); + } + } + + /** + * Returns the current audio mode. + * + * @return the current audio mode (NORMAL, RINGTONE, or IN_CALL). + * Returns the current current audio state from the HAL. + */ + public int getMode() { + IAudioService service = getService(); + try { + return service.getMode(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getMode", e); + return MODE_INVALID; + } + } + + /* modes for setMode/getMode/setRoute/getRoute */ + /** + * Audio harware modes. + */ + /** + * Invalid audio mode. + */ + public static final int MODE_INVALID = AudioSystem.MODE_INVALID; + /** + * Current audio mode. Used to apply audio routing to current mode. + */ + public static final int MODE_CURRENT = AudioSystem.MODE_CURRENT; + /** + * Normal audio mode: not ringing and no call established. + */ + public static final int MODE_NORMAL = AudioSystem.MODE_NORMAL; + /** + * Ringing audio mode. An incoming is being signaled. + */ + public static final int MODE_RINGTONE = AudioSystem.MODE_RINGTONE; + /** + * In call audio mode. A call is established. + */ + public static final int MODE_IN_CALL = AudioSystem.MODE_IN_CALL; + + /* Routing bits for setRouting/getRouting API */ + /** + * Routing audio output to earpiece + */ + public static final int ROUTE_EARPIECE = AudioSystem.ROUTE_EARPIECE; + /** + * Routing audio output to spaker + */ + public static final int ROUTE_SPEAKER = AudioSystem.ROUTE_SPEAKER; + /** + * @deprecated use {@link #ROUTE_BLUETOOTH_SCO} + */ + @Deprecated public static final int ROUTE_BLUETOOTH = AudioSystem.ROUTE_BLUETOOTH_SCO; + /** + * Routing audio output to bluetooth SCO + */ + public static final int ROUTE_BLUETOOTH_SCO = AudioSystem.ROUTE_BLUETOOTH_SCO; + /** + * Routing audio output to headset + */ + public static final int ROUTE_HEADSET = AudioSystem.ROUTE_HEADSET; + /** + * Routing audio output to bluetooth A2DP + */ + public static final int ROUTE_BLUETOOTH_A2DP = AudioSystem.ROUTE_BLUETOOTH_A2DP; + /** + * Used for mask parameter of {@link #setRouting(int,int,int)}. + */ + public static final int ROUTE_ALL = AudioSystem.ROUTE_ALL; + + /** + * Sets the audio routing for a specified mode + * + * @param mode audio mode to change route. E.g., MODE_RINGTONE. + * @param routes bit vector of routes requested, created from one or + * more of ROUTE_xxx types. Set bits indicate that route should be on + * @param mask bit vector of routes to change, created from one or more of + * ROUTE_xxx types. Unset bits indicate the route should be left unchanged + */ + public void setRouting(int mode, int routes, int mask) { + IAudioService service = getService(); + try { + service.setRouting(mode, routes, mask); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in setRouting", e); + } + } + + /** + * Returns the current audio routing bit vector for a specified mode. + * + * @param mode audio mode to get route (e.g., MODE_RINGTONE) + * @return an audio route bit vector that can be compared with ROUTE_xxx + * bits + */ + public int getRouting(int mode) { + IAudioService service = getService(); + try { + return service.getRouting(mode); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getRouting", e); + return -1; + } + } + + /** + * Checks whether any music is active. + * + * @return true if any music tracks are active. + */ + public boolean isMusicActive() { + IAudioService service = getService(); + try { + return service.isMusicActive(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in isMusicActive", e); + return false; + } + } + + /* + * Sets a generic audio configuration parameter. The use of these parameters + * are platform dependant, see libaudio + * + * ** Temporary interface - DO NOT USE + * + * TODO: Replace with a more generic key:value get/set mechanism + * + * param key name of parameter to set. Must not be null. + * param value value of parameter. Must not be null. + */ + /** + * @hide + */ + public void setParameter(String key, String value) { + IAudioService service = getService(); + try { + service.setParameter(key, value); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in setParameter", e); + } + } + + /* Sound effect identifiers */ + /** + * Keyboard and direction pad click sound + * @see #playSoundEffect(int) + */ + public static final int FX_KEY_CLICK = 0; + /** + * Focus has moved up + * @see #playSoundEffect(int) + */ + public static final int FX_FOCUS_NAVIGATION_UP = 1; + /** + * Focus has moved down + * @see #playSoundEffect(int) + */ + public static final int FX_FOCUS_NAVIGATION_DOWN = 2; + /** + * Focus has moved left + * @see #playSoundEffect(int) + */ + public static final int FX_FOCUS_NAVIGATION_LEFT = 3; + /** + * Focus has moved right + * @see #playSoundEffect(int) + */ + public static final int FX_FOCUS_NAVIGATION_RIGHT = 4; + /** + * IME standard keypress sound + * @see #playSoundEffect(int) + * @hide FIXME: Unhide before release + */ + public static final int FX_KEYPRESS_STANDARD = 5; + /** + * IME spacebar keypress sound + * @see #playSoundEffect(int) + * @hide FIXME: Unhide before release + */ + public static final int FX_KEYPRESS_SPACEBAR = 6; + /** + * IME delete keypress sound + * @see #playSoundEffect(int) + * @hide FIXME: Unhide before release + */ + public static final int FX_KEYPRESS_DELETE = 7; + /** + * IME return_keypress sound + * @see #playSoundEffect(int) + * @hide FIXME: Unhide before release + */ + public static final int FX_KEYPRESS_RETURN = 8; + /** + * @hide Number of sound effects + */ + public static final int NUM_SOUND_EFFECTS = 9; + + /** + * Plays a sound effect (Key clicks, lid open/close...) + * @param effectType The type of sound effect. One of + * {@link #FX_KEY_CLICK}, + * {@link #FX_FOCUS_NAVIGATION_UP}, + * {@link #FX_FOCUS_NAVIGATION_DOWN}, + * {@link #FX_FOCUS_NAVIGATION_LEFT}, + * {@link #FX_FOCUS_NAVIGATION_RIGHT}, + * FIXME: include links before release + * {link #FX_KEYPRESS_STANDARD}, + * {link #FX_KEYPRESS_SPACEBAR}, + * {link #FX_KEYPRESS_DELETE}, + * {link #FX_KEYPRESS_RETURN}, + * NOTE: This version uses the UI settings to determine + * whether sounds are heard or not. + */ + public void playSoundEffect(int effectType) { + if (effectType < 0 || effectType >= NUM_SOUND_EFFECTS) { + return; + } + + if (!querySoundEffectsEnabled()) { + return; + } + + IAudioService service = getService(); + try { + service.playSoundEffect(effectType); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in playSoundEffect"+e); + } + } + + /** + * Plays a sound effect (Key clicks, lid open/close...) + * @param effectType The type of sound effect. One of + * {@link #FX_KEY_CLICK}, + * {@link #FX_FOCUS_NAVIGATION_UP}, + * {@link #FX_FOCUS_NAVIGATION_DOWN}, + * {@link #FX_FOCUS_NAVIGATION_LEFT}, + * {@link #FX_FOCUS_NAVIGATION_RIGHT}, + * FIXME: include links before release + * {link #FX_KEYPRESS_STANDARD}, + * {link #FX_KEYPRESS_SPACEBAR}, + * {link #FX_KEYPRESS_DELETE}, + * {link #FX_KEYPRESS_RETURN}, + * @param volume Sound effect volume + * NOTE: This version is for applications that have their own + * settings panel for enabling and controlling volume. + * @hide FIXME: Unhide before release + */ + public void playSoundEffect(int effectType, float volume) { + if (effectType < 0 || effectType >= NUM_SOUND_EFFECTS) { + return; + } + + IAudioService service = getService(); + try { + service.playSoundEffectVolume(effectType, volume); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in playSoundEffect"+e); + } + } + + /** + * Settings has an in memory cache, so this is fast. + */ + private boolean querySoundEffectsEnabled() { + return Settings.System.getInt(mContext.getContentResolver(), Settings.System.SOUND_EFFECTS_ENABLED, 0) != 0; + } + + + /** + * Load Sound effects. + * This method must be called when sound effects are enabled. + */ + public void loadSoundEffects() { + IAudioService service = getService(); + try { + service.loadSoundEffects(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in loadSoundEffects"+e); + } + } + + /** + * Unload Sound effects. + * This method can be called to free some memory when + * sound effects are disabled. + */ + public void unloadSoundEffects() { + IAudioService service = getService(); + try { + service.unloadSoundEffects(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in unloadSoundEffects"+e); + } + } + + /** + * {@hide} + */ + private IBinder mICallBack = new Binder(); +} diff --git a/media/java/android/media/AudioRecord.java b/media/java/android/media/AudioRecord.java new file mode 100644 index 0000000..0ef7760 --- /dev/null +++ b/media/java/android/media/AudioRecord.java @@ -0,0 +1,798 @@ +/* + * Copyright (C) 2008 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 java.lang.ref.WeakReference; +import java.io.OutputStream; +import java.io.IOException; +import java.lang.IllegalArgumentException; +import java.lang.IllegalStateException; +import java.lang.Thread; +import java.nio.ByteBuffer; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Log; + +/** + * The AudioRecord class manages the audio resources for Java applications + * to record audio from the audio input hardware of the platform. This is + * achieved by "pulling" (reading) the data from the AudioRecord object. The + * application is responsible for polling the AudioRecord object in time using one of + * the following three methods: {@link #read(byte[],int, int)}, {@link #read(short[], int, int)} + * or {@link #read(ByteBuffer, int)}. The choice of which method to use will be based + * on the audio data storage format that is the most convenient for the user of AudioRecord. + * <p>Upon creation, an AudioRecord object initializes its associated audio buffer that it will + * fill with the new audio data. The size of this buffer, specified during the construction, + * determines how long an AudioRecord can record before "over-running" data that has not + * been read yet. Data should be from the audio hardware in chunks of sizes inferior to + * the total recording buffer size. + */ +public class AudioRecord +{ + //--------------------------------------------------------- + // Constants + //-------------------- + /** + * State of an AudioRecord that was not successfully initialized upon creation + */ + public static final int STATE_UNINITIALIZED = 0; + /** + * State of an AudioRecord that is ready to be used + */ + public static final int STATE_INITIALIZED = 1; + + /** + * State of an AudioRecord this is not recording + */ + public static final int RECORDSTATE_STOPPED = 1; // matches SL_RECORDSTATE_STOPPED + /** + * State of an AudioRecord this is recording + */ + public static final int RECORDSTATE_RECORDING = 3;// matches SL_RECORDSTATE_RECORDING + + // Error codes: + // to keep in sync with frameworks/base/core/jni/android_media_AudioRecord.cpp + /** + * Denotes a successful operation. + */ + public static final int SUCCESS = 0; + /** + * Denotes a generic operation failure. + */ + public static final int ERROR = -1; + /** + * Denotes a failure due to the use of an invalid value. + */ + public static final int ERROR_BAD_VALUE = -2; + /** + * Denotes a failure due to the improper use of a method. + */ + public static final int ERROR_INVALID_OPERATION = -3; + + private static final int AUDIORECORD_ERROR_SETUP_ZEROFRAMECOUNT = -16; + private static final int AUDIORECORD_ERROR_SETUP_INVALIDCHANNELCOUNT = -17; + private static final int AUDIORECORD_ERROR_SETUP_INVALIDFORMAT = -18; + private static final int AUDIORECORD_ERROR_SETUP_INVALIDSTREAMTYPE = -19; + private static final int AUDIORECORD_ERROR_SETUP_NATIVEINITFAILED = -20; + + // Events: + // to keep in sync with frameworks/base/include/media/AudioRecord.h + /** + * Event id for when the recording head has reached a previously set marker. + */ + private static final int NATIVE_EVENT_MARKER = 2; + /** + * Event id for when the previously set update period has passed during recording. + */ + private static final int NATIVE_EVENT_NEW_POS = 3; + + private final static String TAG = "AudioRecord-Java"; + + + //--------------------------------------------------------- + // Used exclusively by native code + //-------------------- + /** + * Accessed by native methods: provides access to C++ AudioRecord object + */ + @SuppressWarnings("unused") + private int mNativeRecorderInJavaObj; + /** + * Accessed by native methods: provides access to record source constants + */ + @SuppressWarnings("unused") + private final static int SOURCE_DEFAULT = MediaRecorder.AudioSource.DEFAULT; + @SuppressWarnings("unused") + private final static int SOURCE_MIC = MediaRecorder.AudioSource.MIC; + /** + * Accessed by native methods: provides access to the callback data. + */ + @SuppressWarnings("unused") + private int mNativeCallbackCookie; + + + //--------------------------------------------------------- + // Member variables + //-------------------- + /** + * The audio data sampling rate in Hz. + */ + private int mSampleRate = 22050; + /** + * The number of input audio channels (1 is mono, 2 is stereo) + */ + private int mChannelCount = 1; + /** + * The current audio channel configuration + */ + private int mChannelConfiguration = AudioFormat.CHANNEL_CONFIGURATION_MONO; + /** + * The encoding of the audio samples. + * @see AudioFormat#ENCODING_PCM_8BIT + * @see AudioFormat#ENCODING_PCM_16BIT + */ + private int mAudioFormat = AudioFormat.ENCODING_PCM_16BIT; + /** + * Where the audio data is recorded from. + */ + private int mRecordSource = MediaRecorder.AudioSource.DEFAULT; + /** + * Indicates the state of the AudioRecord instance. + */ + private int mState = STATE_UNINITIALIZED; + /** + * Indicates the recording state of the AudioRecord instance. + */ + private int mRecordingState = RECORDSTATE_STOPPED; + /** + * Lock to make sure mRecordingState updates are reflecting the actual state of the object. + */ + private Object mRecordingStateLock = new Object(); + /** + * The listener the AudioRecord notifies when a previously set marker is reached. + * @see #setMarkerReachedListener(OnMarkerReachedListener) + */ + private OnMarkerReachedListener mMarkerListener = null; + /** + * Lock to protect marker listener updates against event notifications + */ + private final Object mMarkerListenerLock = new Object(); + /** + * The listener the AudioRecord notifies periodically during recording. + * @see #setPeriodicNotificationListener(OnPeriodicNotificationListener) + */ + private OnPeriodicNotificationListener mPeriodicListener = null; + /** + * Lock to protect periodic listener updates against event notifications + */ + private final Object mPeriodicListenerLock = new Object(); + /** + * Handler for events coming from the native code + */ + private NativeEventHandler mNativeEventHandler = null; + /** + * Size of the native audio buffer. + */ + private int mNativeBufferSizeInBytes = 0; + + + //--------------------------------------------------------- + // Constructor, Finalize + //-------------------- + /** + * Class constructor. + * @param audioSource the recording source. See {@link MediaRecorder.AudioSource} for + * recording source definitions. + * @param sampleRateInHz the sample rate expressed in Hertz. Examples of rates are (but + * not limited to) 44100, 22050 and 11025. + * @param channelConfig describes the configuration of the audio channels. + * See {@link AudioFormat#CHANNEL_CONFIGURATION_MONO} and + * {@link AudioFormat#CHANNEL_CONFIGURATION_STEREO} + * @param audioFormat the format in which the audio data is represented. + * See {@link AudioFormat#ENCODING_PCM_16BIT} and + * {@link AudioFormat#ENCODING_PCM_8BIT} + * @param bufferSizeInBytes the total size (in bytes) of the buffer where audio data is written + * to during the recording. New audio data can be read from this buffer in smaller chunks + * than this size. + * @throws java.lang.IllegalArgumentException + */ + public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, + int bufferSizeInBytes) + throws IllegalArgumentException { + mState = STATE_UNINITIALIZED; + mRecordingState = RECORDSTATE_STOPPED; + + audioParamCheck(audioSource, sampleRateInHz, channelConfig, audioFormat); + + audioBuffSizeCheck(bufferSizeInBytes); + + // native initialization + //TODO: update native initialization when information about hardware init failure + // due to capture device already open is available. + int initResult = native_setup( new WeakReference<AudioRecord>(this), + mRecordSource, mSampleRate, mChannelCount, mAudioFormat, mNativeBufferSizeInBytes); + if (initResult != SUCCESS) { + loge("Error code "+initResult+" when initializing native AudioRecord object."); + return; // with mState == STATE_UNINITIALIZED + } + + mState = STATE_INITIALIZED; + } + + + // Convenience method for the constructor's parameter checks. + // This is where constructor IllegalArgumentException-s are thrown + // postconditions: + // mRecordSource is valid + // mChannelCount is valid + // mAudioFormat is valid + // mSampleRate is valid + private void audioParamCheck(int audioSource, int sampleRateInHz, + int channelConfig, int audioFormat) { + + //-------------- + // audio source + if ( (audioSource != MediaRecorder.AudioSource.DEFAULT) + && (audioSource != MediaRecorder.AudioSource.MIC) ) { + throw (new IllegalArgumentException("Invalid audio source.")); + } else { + mRecordSource = audioSource; + } + + //-------------- + // sample rate + if ( (sampleRateInHz < 4000) || (sampleRateInHz > 48000) ) { + throw (new IllegalArgumentException(sampleRateInHz + + "Hz is not a supported sample rate.")); + } else { + mSampleRate = sampleRateInHz; + } + + //-------------- + // channel config + switch (channelConfig) { + case AudioFormat.CHANNEL_CONFIGURATION_DEFAULT: + case AudioFormat.CHANNEL_CONFIGURATION_MONO: + mChannelCount = 1; + mChannelConfiguration = AudioFormat.CHANNEL_CONFIGURATION_MONO; + break; + case AudioFormat.CHANNEL_CONFIGURATION_STEREO: + mChannelCount = 2; + mChannelConfiguration = AudioFormat.CHANNEL_CONFIGURATION_STEREO; + break; + default: + mChannelCount = 0; + mChannelConfiguration = AudioFormat.CHANNEL_CONFIGURATION_INVALID; + throw (new IllegalArgumentException("Unsupported channel configuration.")); + } + + //-------------- + // audio format + switch (audioFormat) { + case AudioFormat.ENCODING_DEFAULT: + mAudioFormat = AudioFormat.ENCODING_PCM_16BIT; + break; + case AudioFormat.ENCODING_PCM_16BIT: + case AudioFormat.ENCODING_PCM_8BIT: + mAudioFormat = audioFormat; + break; + default: + mAudioFormat = AudioFormat.ENCODING_INVALID; + throw (new IllegalArgumentException("Unsupported sample encoding." + + " Should be ENCODING_PCM_8BIT or ENCODING_PCM_16BIT.")); + } + } + + + // Convenience method for the contructor's audio buffer size check. + // preconditions: + // mChannelCount is valid + // mAudioFormat is AudioFormat.ENCODING_PCM_8BIT OR AudioFormat.ENCODING_PCM_16BIT + // postcondition: + // mNativeBufferSizeInBytes is valid (multiple of frame size, positive) + private void audioBuffSizeCheck(int audioBufferSize) { + // NB: this section is only valid with PCM data. + // To update when supporting compressed formats + int frameSizeInBytes = mChannelCount + * (mAudioFormat == AudioFormat.ENCODING_PCM_8BIT ? 1 : 2); + if ((audioBufferSize % frameSizeInBytes != 0) || (audioBufferSize < 1)) { + throw (new IllegalArgumentException("Invalid audio buffer size.")); + } + + mNativeBufferSizeInBytes = audioBufferSize; + } + + + // Convenience method for the creation of the native event handler + // It is called only when a non-null event listener is set. + // precondition: + // mNativeEventHandler is null + private void createNativeEventHandler() { + Looper looper; + if ((looper = Looper.myLooper()) != null) { + mNativeEventHandler = new NativeEventHandler(this, looper); + } else if ((looper = Looper.getMainLooper()) != null) { + mNativeEventHandler = new NativeEventHandler(this, looper); + } else { + mNativeEventHandler = null; + } + } + + + /** + * Releases the native AudioRecord resources. + */ + public void release() { + try { + stop(); + } catch(IllegalStateException ise) { + // don't raise an exception, we're releasing the resources. + } + native_release(); + mState = STATE_UNINITIALIZED; + } + + + @Override + protected void finalize() { + native_finalize(); + } + + + //-------------------------------------------------------------------------- + // Getters + //-------------------- + /** + * Returns the configured audio data sample rate in Hz + */ + public int getSampleRate() { + return mSampleRate; + } + + /** + * Returns the audio recording source. + * @see MediaRecorder.AudioSource + */ + public int getAudioSource() { + return mRecordSource; + } + + /** + * Returns the configured audio data format. See {@link AudioFormat#ENCODING_PCM_16BIT} + * and {@link AudioFormat#ENCODING_PCM_8BIT}. + */ + public int getAudioFormat() { + return mAudioFormat; + } + + /** + * Returns the configured channel configuration. + * See {@link AudioFormat#CHANNEL_CONFIGURATION_MONO} + * and {@link AudioFormat#CHANNEL_CONFIGURATION_STEREO}. + */ + public int getChannelConfiguration() { + return mChannelConfiguration; + } + + /** + * Returns the configured number of channels. + */ + public int getChannelCount() { + return mChannelCount; + } + + /** + * Returns the state of the AudioRecord instance. This is useful after the + * AudioRecord instance has been created to check if it was initialized + * properly. This ensures that the appropriate hardware resources have been + * acquired. + * @see AudioRecord#STATE_INITIALIZED + * @see AudioRecord#STATE_UNINITIALIZED + */ + public int getState() { + return mState; + } + + /** + * Returns the recording state of the AudioRecord instance. + * @see AudioRecord#RECORDSTATE_STOPPED + * @see AudioRecord#RECORDSTATE_RECORDING + */ + public int getRecordingState() { + return mRecordingState; + } + + /** + * @return marker position in frames + */ + public int getNotificationMarkerPosition() { + return native_get_marker_pos(); + } + + /** + * @return update period in frames + */ + public int getPositionNotificationPeriod() { + return native_get_pos_update_period(); + } + + /** + * {@hide} + * Returns the minimum buffer size required for the successful creation of an AudioRecord + * object. + * @param sampleRateInHz the sample rate expressed in Hertz. + * @param channelConfig describes the configuration of the audio channels. + * See {@link AudioFormat#CHANNEL_CONFIGURATION_MONO} and + * {@link AudioFormat#CHANNEL_CONFIGURATION_STEREO} + * @param audioFormat the format in which the audio data is represented. + * See {@link AudioFormat#ENCODING_PCM_16BIT}. + * @return {@link #ERROR_BAD_VALUE} if the recording parameters are not supported by the + * hardware, or an invalid parameter was passed, + * or {@link #ERROR} if the implementation was unable to query the hardware for its + * output properties, + * or the minimum buffer size expressed in of bytes. + */ + static public int getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat) { + int channelCount = 0; + switch(channelConfig) { + case AudioFormat.CHANNEL_CONFIGURATION_DEFAULT: + case AudioFormat.CHANNEL_CONFIGURATION_MONO: + channelCount = 1; + break; + case AudioFormat.CHANNEL_CONFIGURATION_STEREO: + channelCount = 2; + break; + case AudioFormat.CHANNEL_CONFIGURATION_INVALID: + default: + loge("getMinBufferSize(): Invalid channel configuration."); + return AudioRecord.ERROR_BAD_VALUE; + } + + // PCM_8BIT is not supported at the moment + if (audioFormat != AudioFormat.ENCODING_PCM_16BIT) { + loge("getMinBufferSize(): Invalid audio format."); + return AudioRecord.ERROR_BAD_VALUE; + } + + int size = native_get_min_buff_size(sampleRateInHz, channelCount, audioFormat); + if (size == 0) { + return AudioRecord.ERROR_BAD_VALUE; + } + else if (size == -1) { + return AudioRecord.ERROR; + } + else { + return size; + } + } + + + //--------------------------------------------------------- + // Transport control methods + //-------------------- + /** + * Starts recording from the AudioRecord instance. + * @throws IllegalStateException + */ + public void startRecording() + throws IllegalStateException { + if (mState != STATE_INITIALIZED) { + throw(new IllegalStateException("startRecording() called on an " + +"uninitialized AudioRecord.")); + } + + // start recording + synchronized(mRecordingStateLock) { + native_start(); + mRecordingState = RECORDSTATE_RECORDING; + } + } + + + + /** + * Stops recording. + * @throws IllegalStateException + */ + public void stop() + throws IllegalStateException { + if (mState != STATE_INITIALIZED) { + throw(new IllegalStateException("stop() called on an uninitialized AudioRecord.")); + } + + // stop recording + synchronized(mRecordingStateLock) { + native_stop(); + mRecordingState = RECORDSTATE_STOPPED; + } + } + + + //--------------------------------------------------------- + // Audio data supply + //-------------------- + /** + * Reads audio data from the audio hardware for recording into a buffer. + * @param audioData the array to which the recorded audio data is written. + * @param offsetInBytes index in audioData from which the data is written. + * @param sizeInBytes the number of requested bytes. + * @return the number of bytes that were read or or {@link #ERROR_INVALID_OPERATION} + * if the object wasn't properly initialized, or {@link #ERROR_BAD_VALUE} if + * the parameters don't resolve to valid data and indexes. + * The number of bytes will not exceed sizeInBytes. + */ + public int read(byte[] audioData, int offsetInBytes, int sizeInBytes) { + if (mState != STATE_INITIALIZED) { + return ERROR_INVALID_OPERATION; + } + + if ( (audioData == null) || (offsetInBytes < 0 ) || (sizeInBytes < 0) + || (offsetInBytes + sizeInBytes > audioData.length)) { + return ERROR_BAD_VALUE; + } + + return native_read_in_byte_array(audioData, offsetInBytes, sizeInBytes); + } + + + /** + * Reads audio data from the audio hardware for recording into a buffer. + * @param audioData the array to which the recorded audio data is written. + * @param offsetInShorts index in audioData from which the data is written. + * @param sizeInShorts the number of requested shorts. + * @return the number of bytes that were read or or {@link #ERROR_INVALID_OPERATION} + * if the object wasn't properly initialized, or {@link #ERROR_BAD_VALUE} if + * the parameters don't resolve to valid data and indexes. + * The number of shorts will not exceed sizeInShorts. + */ + public int read(short[] audioData, int offsetInShorts, int sizeInShorts) { + if (mState != STATE_INITIALIZED) { + return ERROR_INVALID_OPERATION; + } + + if ( (audioData == null) || (offsetInShorts < 0 ) || (sizeInShorts < 0) + || (offsetInShorts + sizeInShorts > audioData.length)) { + return ERROR_BAD_VALUE; + } + + return native_read_in_short_array(audioData, offsetInShorts, sizeInShorts); + } + + + /** + * Reads audio data from the audio hardware for recording into a direct buffer. If this buffer + * is not a direct buffer, this method will always return 0. + * @param audioBuffer the direct buffer to which the recorded audio data is written. + * @param sizeInBytes the number of requested bytes. + * @return the number of bytes that were read or or {@link #ERROR_INVALID_OPERATION} + * if the object wasn't properly initialized, or {@link #ERROR_BAD_VALUE} if + * the parameters don't resolve to valid data and indexes. + * The number of bytes will not exceed sizeInBytes. + */ + public int read(ByteBuffer audioBuffer, int sizeInBytes) { + if (mState != STATE_INITIALIZED) { + return ERROR_INVALID_OPERATION; + } + + if ( (audioBuffer == null) || (sizeInBytes < 0) ) { + return ERROR_BAD_VALUE; + } + + return native_read_in_direct_buffer(audioBuffer, sizeInBytes); + } + + + //-------------------------------------------------------------------------- + // Initialization / configuration + //-------------------- + /** + * Sets the listener the AudioRecord notifies when a previously set marker is reached. + * @param listener + */ + public void setMarkerReachedListener(OnMarkerReachedListener listener) { + synchronized (mMarkerListenerLock) { + mMarkerListener = listener; + } + if ((listener != null) && (mNativeEventHandler == null)) { + createNativeEventHandler(); + } + } + + + /** + * Sets the listener the AudioRecord notifies periodically during recording. + * @param listener + */ + public void setPeriodicNotificationListener(OnPeriodicNotificationListener listener) { + synchronized (mPeriodicListenerLock) { + mPeriodicListener = listener; + } + if ((listener != null) && (mNativeEventHandler == null)) { + createNativeEventHandler(); + } + } + + + /** + * Sets the marker position at which the listener, if set with + * {@link #setMarkerReachedListener(OnMarkerReachedListener)}, is called. + * @param markerInFrames marker position expressed in frames + * @return error code or success, see {@link #SUCCESS}, {@link #ERROR_BAD_VALUE}, + * {@link #ERROR_INVALID_OPERATION} + */ + public int setNotificationMarkerPosition(int markerInFrames) { + return native_set_marker_pos(markerInFrames); + } + + + /** + * Sets the period at which the listener, if set with + * {@link #setPositionNotificationPeriod(int)}, is called. + * @param periodInFrames update period expressed in frames + * @return error code or success, see {@link #SUCCESS}, {@link #ERROR_INVALID_OPERATION} + */ + public int setPositionNotificationPeriod(int periodInFrames) { + return native_set_pos_update_period(periodInFrames); + } + + + //--------------------------------------------------------- + // Interface definitions + //-------------------- + /** + * Interface definition for a callback to be invoked when an AudioRecord has + * reached a notification marker set by setNotificationMarkerPosition(). + */ + public interface OnMarkerReachedListener { + /** + * Called on the listener to notify it that the previously set marker has been reached + * by the recording head. + */ + void onMarkerReached(AudioRecord recorder); + } + + + /** + * Interface definition for a callback to be invoked for each periodic AudioRecord + * update during recording. The update interval is set by setPositionNotificationPeriod(). + */ + public interface OnPeriodicNotificationListener { + /** + * Called on the listener to periodically notify it that the recording head has reached + * a multiple of the notification period. + */ + void onPeriodicNotification(AudioRecord recorder); + } + + + //--------------------------------------------------------- + // Inner classes + //-------------------- + /** + * Helper class to handle the forwarding of native events to the appropriate listeners + */ + private class NativeEventHandler extends Handler + { + private AudioRecord mAudioRecord; + + public NativeEventHandler(AudioRecord ar, Looper looper) { + super(looper); + mAudioRecord = ar; + } + + @Override + public void handleMessage(Message msg) { + if (mAudioRecord == null) { + return; + } + switch(msg.what) { + case NATIVE_EVENT_MARKER: + synchronized (mMarkerListenerLock) { + if (mAudioRecord.mMarkerListener != null) { + mAudioRecord.mMarkerListener.onMarkerReached(mAudioRecord); + } + } + break; + case NATIVE_EVENT_NEW_POS: + synchronized (mPeriodicListenerLock) { + if (mAudioRecord.mPeriodicListener != null) { + mAudioRecord.mPeriodicListener.onPeriodicNotification(mAudioRecord); + } + } + break; + default: + Log.e(TAG, "[ android.media.AudioRecord.NativeEventHandler ] " + + "Unknown event type: " + msg.what); + break; + } + } + } + + + //--------------------------------------------------------- + // Java methods called from the native side + //-------------------- + @SuppressWarnings("unused") + private static void postEventFromNative(Object audiorecord_ref, + int what, int arg1, int arg2, Object obj) { + //logd("Event posted from the native side: event="+ what + " args="+ arg1+" "+arg2); + AudioRecord recorder = (AudioRecord)((WeakReference)audiorecord_ref).get(); + if (recorder == null) { + return; + } + + if (recorder.mNativeEventHandler != null) { + Message m = recorder.mNativeEventHandler.obtainMessage(what, arg1, arg2, obj); + recorder.mNativeEventHandler.sendMessage(m); + } + + } + + + //--------------------------------------------------------- + // Native methods called from the Java side + //-------------------- + + private native final int native_setup(Object audiorecord_this, + int recordSource, int sampleRate, int nbChannels, int audioFormat, int buffSizeInBytes); + + private native final void native_finalize(); + + private native final void native_release(); + + private native final void native_start(); + + private native final void native_stop(); + + private native final int native_read_in_byte_array(byte[] audioData, + int offsetInBytes, int sizeInBytes); + + private native final int native_read_in_short_array(short[] audioData, + int offsetInShorts, int sizeInShorts); + + private native final int native_read_in_direct_buffer(Object jBuffer, int sizeInBytes); + + private native final int native_set_marker_pos(int marker); + private native final int native_get_marker_pos(); + + private native final int native_set_pos_update_period(int updatePeriod); + private native final int native_get_pos_update_period(); + + static private native final int native_get_min_buff_size( + int sampleRateInHz, int channelCount, int audioFormat); + + + //--------------------------------------------------------- + // Utility methods + //------------------ + + private static void logd(String msg) { + Log.d(TAG, "[ android.media.AudioRecord ] " + msg); + } + + private static void loge(String msg) { + Log.e(TAG, "[ android.media.AudioRecord ] " + msg); + } + +} + + + + + diff --git a/media/java/android/media/AudioService.java b/media/java/android/media/AudioService.java new file mode 100644 index 0000000..83ede0d --- /dev/null +++ b/media/java/android/media/AudioService.java @@ -0,0 +1,1236 @@ +/* + * Copyright (C) 2006 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 android.app.ActivityManagerNative; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.media.MediaPlayer.OnCompletionListener; +import android.media.MediaPlayer.OnErrorListener; +import android.os.Binder; +import android.os.Environment; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.provider.Settings; +import android.provider.Settings.System; +import android.util.Log; +import android.view.VolumePanel; + +import com.android.internal.telephony.ITelephony; + +import java.io.IOException; +import java.util.ArrayList; + + +/** + * The implementation of the volume manager service. + * <p> + * This implementation focuses on delivering a responsive UI. Most methods are + * asynchronous to external calls. For example, the task of setting a volume + * will update our internal state, but in a separate thread will set the system + * volume and later persist to the database. Similarly, setting the ringer mode + * will update the state and broadcast a change and in a separate thread later + * persist the ringer mode. + * + * @hide + */ +public class AudioService extends IAudioService.Stub { + + private static final String TAG = "AudioService"; + + /** How long to delay before persisting a change in volume/ringer mode. */ + private static final int PERSIST_DELAY = 3000; + + private Context mContext; + private ContentResolver mContentResolver; + + /** The UI */ + private VolumePanel mVolumePanel; + + // sendMsg() flags + /** Used when a message should be shared across all stream types. */ + private static final int SHARED_MSG = -1; + /** If the msg is already queued, replace it with this one. */ + private static final int SENDMSG_REPLACE = 0; + /** If the msg is already queued, ignore this one and leave the old. */ + private static final int SENDMSG_NOOP = 1; + /** If the msg is already queued, queue this one and leave the old. */ + private static final int SENDMSG_QUEUE = 2; + + // AudioHandler message.whats + private static final int MSG_SET_SYSTEM_VOLUME = 0; + private static final int MSG_PERSIST_VOLUME = 1; + private static final int MSG_PERSIST_RINGER_MODE = 3; + private static final int MSG_PERSIST_VIBRATE_SETTING = 4; + private static final int MSG_MEDIA_SERVER_DIED = 5; + private static final int MSG_MEDIA_SERVER_STARTED = 6; + private static final int MSG_PLAY_SOUND_EFFECT = 7; + + /** @see AudioSystemThread */ + private AudioSystemThread mAudioSystemThread; + /** @see AudioHandler */ + private AudioHandler mAudioHandler; + /** @see VolumeStreamState */ + private VolumeStreamState[] mStreamStates; + + private boolean mMicMute; + private int mMode; + private int[] mRoutes = new int[AudioSystem.NUM_MODES]; + private Object mSettingsLock = new Object(); + private boolean mMediaServerOk; + + private SoundPool mSoundPool; + private Object mSoundEffectsLock = new Object(); + private static final int NUM_SOUNDPOOL_CHANNELS = 4; + private static final int SOUND_EFFECT_VOLUME = 1000; + + /* Sound effect file names */ + private static final String SOUND_EFFECTS_PATH = "/media/audio/ui/"; + private static final String[] SOUND_EFFECT_FILES = new String[] { + "Effect_Tick.ogg", + "KeypressStandard.ogg", + "KeypressSpacebar.ogg", + "KeypressDelete.ogg", + "KeypressReturn.ogg" + }; + + /* Sound effect file name mapping sound effect id (AudioManager.FX_xxx) to + * file index in SOUND_EFFECT_FILES[] (first column) and indicating if effect + * uses soundpool (second column) */ + private int[][] SOUND_EFFECT_FILES_MAP = new int[][] { + {0, -1}, // FX_KEY_CLICK + {0, -1}, // FX_FOCUS_NAVIGATION_UP + {0, -1}, // FX_FOCUS_NAVIGATION_DOWN + {0, -1}, // FX_FOCUS_NAVIGATION_LEFT + {0, -1}, // FX_FOCUS_NAVIGATION_RIGHT + {1, -1}, // FX_KEYPRESS_STANDARD + {2, -1}, // FX_KEYPRESS_SPACEBAR + {3, -1}, // FX_FOCUS_DELETE + {4, -1} // FX_FOCUS_RETURN + }; + + private AudioSystem.ErrorCallback mAudioSystemCallback = new AudioSystem.ErrorCallback() { + public void onError(int error) { + switch (error) { + case AudioSystem.AUDIO_STATUS_SERVER_DIED: + if (mMediaServerOk) { + sendMsg(mAudioHandler, MSG_MEDIA_SERVER_DIED, SHARED_MSG, SENDMSG_NOOP, 0, 0, + null, 1500); + } + break; + case AudioSystem.AUDIO_STATUS_OK: + if (!mMediaServerOk) { + sendMsg(mAudioHandler, MSG_MEDIA_SERVER_STARTED, SHARED_MSG, SENDMSG_NOOP, 0, 0, + null, 0); + } + break; + default: + break; + } + } + }; + + /** + * Current ringer mode from one of {@link AudioManager#RINGER_MODE_NORMAL}, + * {@link AudioManager#RINGER_MODE_SILENT}, or + * {@link AudioManager#RINGER_MODE_VIBRATE}. + */ + private int mRingerMode; + + /** @see System#MODE_RINGER_STREAMS_AFFECTED */ + private int mRingerModeAffectedStreams; + + /** @see System#MUTE_STREAMS_AFFECTED */ + private int mMuteAffectedStreams; + + /** + * Has multiple bits per vibrate type to indicate the type's vibrate + * setting. See {@link #setVibrateSetting(int, int)}. + * <p> + * NOTE: This is not the final decision of whether vibrate is on/off for the + * type since it depends on the ringer mode. See {@link #shouldVibrate(int)}. + */ + private int mVibrateSetting; + + /////////////////////////////////////////////////////////////////////////// + // Construction + /////////////////////////////////////////////////////////////////////////// + + /** @hide */ + public AudioService(Context context) { + mContext = context; + mContentResolver = context.getContentResolver(); + mVolumePanel = new VolumePanel(context, this); + + createAudioSystemThread(); + createStreamStates(); + readPersistedSettings(); + readAudioSettings(); + mMediaServerOk = true; + AudioSystem.setErrorCallback(mAudioSystemCallback); + loadSoundEffects(); + } + + private void createAudioSystemThread() { + mAudioSystemThread = new AudioSystemThread(); + mAudioSystemThread.start(); + waitForAudioHandlerCreation(); + } + + /** Waits for the volume handler to be created by the other thread. */ + private void waitForAudioHandlerCreation() { + synchronized(this) { + while (mAudioHandler == null) { + try { + // Wait for mAudioHandler to be set by the other thread + wait(); + } catch (InterruptedException e) { + Log.e(TAG, "Interrupted while waiting on volume handler."); + } + } + } + } + + private void createStreamStates() { + final int[] volumeLevelsPhone = + createVolumeLevels(0, AudioManager.MAX_STREAM_VOLUME[AudioManager.STREAM_VOICE_CALL]); + final int[] volumeLevelsCoarse = + createVolumeLevels(0, AudioManager.MAX_STREAM_VOLUME[AudioManager.STREAM_SYSTEM]); + final int[] volumeLevelsFine = + createVolumeLevels(0, AudioManager.MAX_STREAM_VOLUME[AudioManager.STREAM_MUSIC]); + final int[] volumeLevelsBtPhone = + createVolumeLevels(0, + AudioManager.MAX_STREAM_VOLUME[AudioManager.STREAM_BLUETOOTH_SCO]); + + int numStreamTypes = AudioSystem.getNumStreamTypes(); + VolumeStreamState[] streams = mStreamStates = new VolumeStreamState[numStreamTypes]; + + for (int i = 0; i < numStreamTypes; i++) { + final int[] levels; + + switch (i) { + + case AudioSystem.STREAM_MUSIC: + levels = volumeLevelsFine; + break; + + case AudioSystem.STREAM_VOICE_CALL: + levels = volumeLevelsPhone; + break; + + case AudioSystem.STREAM_BLUETOOTH_SCO: + levels = volumeLevelsBtPhone; + break; + + default: + levels = volumeLevelsCoarse; + break; + } + + if (i == AudioSystem.STREAM_BLUETOOTH_SCO) { + streams[i] = new VolumeStreamState(AudioManager.DEFAULT_STREAM_VOLUME[i], i,levels); + } else { + streams[i] = new VolumeStreamState(System.VOLUME_SETTINGS[i], i, levels); + } + } + } + + private static int[] createVolumeLevels(int offset, int numlevels) { + double curve = 1.0f; // 1.4f + int [] volumes = new int[numlevels + offset]; + for (int i = 0; i < offset; i++) { + volumes[i] = 0; + } + + double val = 0; + double max = Math.pow(numlevels - 1, curve); + for (int i = 0; i < numlevels; i++) { + val = Math.pow(i, curve) / max; + volumes[offset + i] = (int) (val * 100.0f); + } + return volumes; + } + + private void readPersistedSettings() { + final ContentResolver cr = mContentResolver; + + mRingerMode = System.getInt(cr, System.MODE_RINGER, AudioManager.RINGER_MODE_NORMAL); + mRingerModeAffectedStreams = System.getInt(mContentResolver, + System.MODE_RINGER_STREAMS_AFFECTED, 1 << AudioSystem.STREAM_RING); + + mVibrateSetting = System.getInt(cr, System.VIBRATE_ON, 0); + + mMuteAffectedStreams = System.getInt(cr, + System.MUTE_STREAMS_AFFECTED, + ((1 << AudioSystem.STREAM_MUSIC)|(1 << AudioSystem.STREAM_RING)|(1 << AudioSystem.STREAM_SYSTEM))); + + // Each stream will read its own persisted settings + + // Broadcast the sticky intent + broadcastRingerMode(); + + // Broadcast vibrate settings + broadcastVibrateSetting(AudioManager.VIBRATE_TYPE_RINGER); + broadcastVibrateSetting(AudioManager.VIBRATE_TYPE_NOTIFICATION); + } + + private void readAudioSettings() { + synchronized (mSettingsLock) { + mMicMute = AudioSystem.isMicrophoneMuted(); + mMode = AudioSystem.getMode(); + for (int mode = 0; mode < AudioSystem.NUM_MODES; mode++) { + mRoutes[mode] = AudioSystem.getRouting(mode); + } + } + } + + private void applyAudioSettings() { + synchronized (mSettingsLock) { + AudioSystem.muteMicrophone(mMicMute); + AudioSystem.setMode(mMode); + for (int mode = 0; mode < AudioSystem.NUM_MODES; mode++) { + AudioSystem.setRouting(mode, mRoutes[mode], AudioSystem.ROUTE_ALL); + } + } + } + + /////////////////////////////////////////////////////////////////////////// + // IPC methods + /////////////////////////////////////////////////////////////////////////// + + /** @see AudioManager#adjustVolume(int, int) */ + public void adjustVolume(int direction, int flags) { + adjustSuggestedStreamVolume(direction, AudioManager.USE_DEFAULT_STREAM_TYPE, flags); + } + + /** @see AudioManager#adjustVolume(int, int, int) */ + public void adjustSuggestedStreamVolume(int direction, int suggestedStreamType, int flags) { + + int streamType = getActiveStreamType(suggestedStreamType); + + // Don't play sound on other streams + if (streamType != AudioSystem.STREAM_RING && (flags & AudioManager.FLAG_PLAY_SOUND) != 0) { + flags &= ~AudioManager.FLAG_PLAY_SOUND; + } + + adjustStreamVolume(streamType, direction, flags); + } + + /** @see AudioManager#adjustStreamVolume(int, int, int) */ + public void adjustStreamVolume(int streamType, int direction, int flags) { + ensureValidDirection(direction); + ensureValidStreamType(streamType); + + boolean notificationsUseRingVolume = Settings.System.getInt(mContentResolver, + Settings.System.NOTIFICATIONS_USE_RING_VOLUME, 1) == 1; + if (notificationsUseRingVolume && streamType == AudioManager.STREAM_NOTIFICATION) { + // Redirect the volume change to the ring stream + streamType = AudioManager.STREAM_RING; + } + + VolumeStreamState streamState = mStreamStates[streamType]; + final int oldIndex = streamState.mIndex; + boolean adjustVolume = true; + + // If either the client forces allowing ringer modes for this adjustment, + // or the stream type is one that is affected by ringer modes + if ((flags & AudioManager.FLAG_ALLOW_RINGER_MODES) != 0 + || streamType == AudioManager.STREAM_RING) { + // Check if the ringer mode changes with this volume adjustment. If + // it does, it will handle adjusting the volume, so we won't below + adjustVolume = checkForRingerModeChange(oldIndex, direction); + } + + if (adjustVolume && streamState.adjustIndex(direction)) { + + boolean alsoUpdateNotificationVolume = notificationsUseRingVolume && + streamType == AudioManager.STREAM_RING; + if (alsoUpdateNotificationVolume) { + mStreamStates[AudioManager.STREAM_NOTIFICATION].adjustIndex(direction); + } + + // Post message to set system volume (it in turn will post a message + // to persist). Do not change volume if stream is muted. + if (streamState.muteCount() == 0) { + sendMsg(mAudioHandler, MSG_SET_SYSTEM_VOLUME, streamType, SENDMSG_NOOP, 0, 0, + streamState, 0); + + if (alsoUpdateNotificationVolume) { + sendMsg(mAudioHandler, MSG_SET_SYSTEM_VOLUME, AudioManager.STREAM_NOTIFICATION, + SENDMSG_NOOP, 0, 0, mStreamStates[AudioManager.STREAM_NOTIFICATION], 0); + } + } + } + + // UI + mVolumePanel.postVolumeChanged(streamType, flags); + // Broadcast Intent + sendVolumeUpdate(streamType); + } + + /** @see AudioManager#setStreamVolume(int, int, int) */ + public void setStreamVolume(int streamType, int index, int flags) { + ensureValidStreamType(streamType); + syncRingerAndNotificationStreamVolume(streamType, index, false); + + setStreamVolumeInt(streamType, index, false); + + // UI, etc. + mVolumePanel.postVolumeChanged(streamType, flags); + // Broadcast Intent + sendVolumeUpdate(streamType); + } + + private void sendVolumeUpdate(int streamType) { + Intent intent = new Intent(AudioManager.VOLUME_CHANGED_ACTION); + intent.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, streamType); + intent.putExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, getStreamVolume(streamType)); + + // Currently, sending the intent only when the stream is BLUETOOTH_SCO + if (streamType == AudioManager.STREAM_BLUETOOTH_SCO) { + mContext.sendBroadcast(intent); + } + } + + /** + * Sync the STREAM_RING and STREAM_NOTIFICATION volumes if mandated by the + * value in Settings. + * + * @param streamType Type of the stream + * @param index Volume index for the stream + * @param force If true, set the volume even if the current and desired + * volume as same + */ + private void syncRingerAndNotificationStreamVolume(int streamType, int index, boolean force) { + boolean notificationsUseRingVolume = Settings.System.getInt(mContentResolver, + Settings.System.NOTIFICATIONS_USE_RING_VOLUME, 1) == 1; + if (notificationsUseRingVolume) { + if (streamType == AudioManager.STREAM_NOTIFICATION) { + // Redirect the volume change to the ring stream + streamType = AudioManager.STREAM_RING; + } + if (streamType == AudioManager.STREAM_RING) { + // One-off to sync notification volume to ringer volume + setStreamVolumeInt(AudioManager.STREAM_NOTIFICATION, index, force); + } + } + } + + + /** + * Sets the stream state's index, and posts a message to set system volume. + * This will not call out to the UI. Assumes a valid stream type. + * + * @param streamType Type of the stream + * @param index Desired volume index of the stream + * @param force If true, set the volume even if the desired volume is same + * as the current volume. + */ + private void setStreamVolumeInt(int streamType, int index, boolean force) { + VolumeStreamState streamState = mStreamStates[streamType]; + if (streamState.setIndex(index) || force) { + // Post message to set system volume (it in turn will post a message + // to persist). Do not change volume if stream is muted. + if (streamState.muteCount() == 0) { + sendMsg(mAudioHandler, MSG_SET_SYSTEM_VOLUME, streamType, SENDMSG_NOOP, 0, 0, + streamState, 0); + } + } + } + + /** @see AudioManager#setStreamSolo(int, boolean) */ + public void setStreamSolo(int streamType, boolean state, IBinder cb) { + for (int stream = 0; stream < mStreamStates.length; stream++) { + if (!isStreamAffectedByMute(stream) || stream == streamType) continue; + // Bring back last audible volume + mStreamStates[stream].mute(cb, state); + } + } + + /** @see AudioManager#setStreamMute(int, boolean) */ + public void setStreamMute(int streamType, boolean state, IBinder cb) { + if (isStreamAffectedByMute(streamType)) { + mStreamStates[streamType].mute(cb, state); + } + } + + /** @see AudioManager#getStreamVolume(int) */ + public int getStreamVolume(int streamType) { + ensureValidStreamType(streamType); + return mStreamStates[streamType].mIndex; + } + + /** @see AudioManager#getStreamMaxVolume(int) */ + public int getStreamMaxVolume(int streamType) { + ensureValidStreamType(streamType); + return mStreamStates[streamType].getMaxIndex(); + } + + /** @see AudioManager#getRingerMode() */ + public int getRingerMode() { + return mRingerMode; + } + + /** @see AudioManager#setRingerMode(int) */ + public void setRingerMode(int ringerMode) { + if (ringerMode != mRingerMode) { + mRingerMode = ringerMode; + + // Adjust volumes via posting message + int numStreamTypes = AudioSystem.getNumStreamTypes(); + if (mRingerMode == AudioManager.RINGER_MODE_NORMAL) { + for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) { + if (!isStreamAffectedByRingerMode(streamType)) continue; + // Bring back last audible volume + setStreamVolumeInt(streamType, mStreamStates[streamType].mLastAudibleIndex, + false); + } + } else { + for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) { + if (!isStreamAffectedByRingerMode(streamType)) continue; + // Either silent or vibrate, either way volume is 0 + setStreamVolumeInt(streamType, 0, false); + } + } + + // Send sticky broadcast + broadcastRingerMode(); + + // Post a persist ringer mode msg + sendMsg(mAudioHandler, MSG_PERSIST_RINGER_MODE, SHARED_MSG, + SENDMSG_REPLACE, 0, 0, null, PERSIST_DELAY); + } + } + + /** @see AudioManager#shouldVibrate(int) */ + public boolean shouldVibrate(int vibrateType) { + + switch (getVibrateSetting(vibrateType)) { + + case AudioManager.VIBRATE_SETTING_ON: + return mRingerMode != AudioManager.RINGER_MODE_SILENT; + + case AudioManager.VIBRATE_SETTING_ONLY_SILENT: + return mRingerMode == AudioManager.RINGER_MODE_VIBRATE; + + case AudioManager.VIBRATE_SETTING_OFF: + // Phone ringer should always vibrate in vibrate mode + if (vibrateType == AudioManager.VIBRATE_TYPE_RINGER) { + return mRingerMode == AudioManager.RINGER_MODE_VIBRATE; + } + + default: + return false; + } + } + + /** @see AudioManager#getVibrateSetting(int) */ + public int getVibrateSetting(int vibrateType) { + return (mVibrateSetting >> (vibrateType * 2)) & 3; + } + + /** @see AudioManager#setVibrateSetting(int, int) */ + public void setVibrateSetting(int vibrateType, int vibrateSetting) { + + mVibrateSetting = getValueForVibrateSetting(mVibrateSetting, vibrateType, vibrateSetting); + + // Broadcast change + broadcastVibrateSetting(vibrateType); + + // Post message to set ringer mode (it in turn will post a message + // to persist) + sendMsg(mAudioHandler, MSG_PERSIST_VIBRATE_SETTING, SHARED_MSG, SENDMSG_NOOP, 0, 0, + null, 0); + } + + /** + * @see #setVibrateSetting(int, int) + * @hide + */ + public static int getValueForVibrateSetting(int existingValue, int vibrateType, + int vibrateSetting) { + + // First clear the existing setting. Each vibrate type has two bits in + // the value. Note '3' is '11' in binary. + existingValue &= ~(3 << (vibrateType * 2)); + + // Set into the old value + existingValue |= (vibrateSetting & 3) << (vibrateType * 2); + + return existingValue; + } + + /** @see AudioManager#setMicrophoneMute(boolean) */ + public void setMicrophoneMute(boolean on) { + if (!checkAudioSettingsPermission("setMicrophoneMute()")) { + return; + } + synchronized (mSettingsLock) { + if (on != mMicMute) { + AudioSystem.muteMicrophone(on); + mMicMute = on; + } + } + } + + /** @see AudioManager#isMicrophoneMute() */ + public boolean isMicrophoneMute() { + return mMicMute; + } + + /** @see AudioManager#setMode(int) */ + public void setMode(int mode) { + if (!checkAudioSettingsPermission("setMode()")) { + return; + } + synchronized (mSettingsLock) { + if (mode != mMode) { + AudioSystem.setMode(mode); + mMode = mode; + } + int streamType = getActiveStreamType(AudioManager.USE_DEFAULT_STREAM_TYPE); + int index = mStreamStates[streamType].mIndex; + syncRingerAndNotificationStreamVolume(streamType, index, true); + setStreamVolumeInt(streamType, index, true); + } + } + + /** @see AudioManager#getMode() */ + public int getMode() { + return mMode; + } + + /** @see AudioManager#setRouting(int, int, int) */ + public void setRouting(int mode, int routes, int mask) { + if (!checkAudioSettingsPermission("setRouting()")) { + return; + } + synchronized (mSettingsLock) { + if ((mRoutes[mode] & mask) != (routes & mask)) { + AudioSystem.setRouting(mode, routes, mask); + mRoutes[mode] = (mRoutes[mode] & ~mask) | (routes & mask); + } + int streamType = getActiveStreamType(AudioManager.USE_DEFAULT_STREAM_TYPE); + int index = mStreamStates[streamType].mIndex; + syncRingerAndNotificationStreamVolume(streamType, index, true); + setStreamVolumeInt(streamType, index, true); + } + } + + /** @see AudioManager#getRouting(int) */ + public int getRouting(int mode) { + return mRoutes[mode]; + } + + /** @see AudioManager#isMusicActive() */ + public boolean isMusicActive() { + return AudioSystem.isMusicActive(); + } + + /** @see AudioManager#setParameter(String, String) */ + public void setParameter(String key, String value) { + AudioSystem.setParameter(key, value); + } + + /** @see AudioManager#playSoundEffect(int) */ + public void playSoundEffect(int effectType) { + sendMsg(mAudioHandler, MSG_PLAY_SOUND_EFFECT, SHARED_MSG, SENDMSG_NOOP, + effectType, SOUND_EFFECT_VOLUME, null, 0); + } + + /** @see AudioManager#playSoundEffect(int, float) */ + /* @hide FIXME: unhide before release */ + public void playSoundEffectVolume(int effectType, float volume) { + sendMsg(mAudioHandler, MSG_PLAY_SOUND_EFFECT, SHARED_MSG, SENDMSG_NOOP, + effectType, (int) (volume * 1000), null, 0); + } + + /** + * Loads samples into the soundpool. + * This method must be called at when sound effects are enabled + */ + public boolean loadSoundEffects() { + synchronized (mSoundEffectsLock) { + mSoundPool = new SoundPool(NUM_SOUNDPOOL_CHANNELS, AudioSystem.STREAM_SYSTEM, 0); + if (mSoundPool == null) { + return false; + } + /* + * poolId table: The value -1 in this table indicates that corresponding + * file (same index in SOUND_EFFECT_FILES[] has not been loaded. + * Once loaded, the value in poolId is the sample ID and the same + * sample can be reused for another effect using the same file. + */ + int[] poolId = new int[SOUND_EFFECT_FILES.length]; + for (int fileIdx = 0; fileIdx < SOUND_EFFECT_FILES.length; fileIdx++) { + poolId[fileIdx] = -1; + } + /* + * Effects whose value in SOUND_EFFECT_FILES_MAP[effect][1] is -1 must be loaded. + * If load succeeds, value in SOUND_EFFECT_FILES_MAP[effect][1] is > 0: + * this indicates we have a valid sample loaded for this effect. + */ + for (int effect = 0; effect < AudioManager.NUM_SOUND_EFFECTS; effect++) { + // Do not load sample if this effect uses the MediaPlayer + if (SOUND_EFFECT_FILES_MAP[effect][1] == 0) { + continue; + } + if (poolId[SOUND_EFFECT_FILES_MAP[effect][0]] == -1) { + String filePath = Environment.getRootDirectory() + SOUND_EFFECTS_PATH + SOUND_EFFECT_FILES[SOUND_EFFECT_FILES_MAP[effect][0]]; + int sampleId = mSoundPool.load(filePath, 0); + SOUND_EFFECT_FILES_MAP[effect][1] = sampleId; + poolId[SOUND_EFFECT_FILES_MAP[effect][0]] = sampleId; + if (sampleId <= 0) { + Log.w(TAG, "Soundpool could not load file: "+filePath); + } + } else { + SOUND_EFFECT_FILES_MAP[effect][1] = poolId[SOUND_EFFECT_FILES_MAP[effect][0]]; + } + } + } + + return true; + } + + /** + * Unloads samples from the sound pool. + * This method can be called to free some memory when + * sound effects are disabled. + */ + public void unloadSoundEffects() { + synchronized (mSoundEffectsLock) { + if (mSoundPool == null) { + return; + } + int[] poolId = new int[SOUND_EFFECT_FILES.length]; + for (int fileIdx = 0; fileIdx < SOUND_EFFECT_FILES.length; fileIdx++) { + poolId[fileIdx] = 0; + } + + for (int effect = 0; effect < AudioManager.NUM_SOUND_EFFECTS; effect++) { + if (SOUND_EFFECT_FILES_MAP[effect][1] <= 0) { + continue; + } + if (poolId[SOUND_EFFECT_FILES_MAP[effect][0]] == 0) { + mSoundPool.unload(SOUND_EFFECT_FILES_MAP[effect][1]); + SOUND_EFFECT_FILES_MAP[effect][1] = -1; + poolId[SOUND_EFFECT_FILES_MAP[effect][0]] = -1; + } + } + mSoundPool = null; + } + } + + /////////////////////////////////////////////////////////////////////////// + // Internal methods + /////////////////////////////////////////////////////////////////////////// + + /** + * Checks if the adjustment should change ringer mode instead of just + * adjusting volume. If so, this will set the proper ringer mode and volume + * indices on the stream states. + */ + private boolean checkForRingerModeChange(int oldIndex, int direction) { + boolean adjustVolumeIndex = true; + int newRingerMode = mRingerMode; + + if (mRingerMode == AudioManager.RINGER_MODE_NORMAL && oldIndex == 1 + && direction == AudioManager.ADJUST_LOWER) { + newRingerMode = AudioManager.RINGER_MODE_VIBRATE; + } else if (mRingerMode == AudioManager.RINGER_MODE_VIBRATE) { + if (direction == AudioManager.ADJUST_RAISE) { + newRingerMode = AudioManager.RINGER_MODE_NORMAL; + } else if (direction == AudioManager.ADJUST_LOWER) { + newRingerMode = AudioManager.RINGER_MODE_SILENT; + } + } else if (direction == AudioManager.ADJUST_RAISE + && mRingerMode == AudioManager.RINGER_MODE_SILENT) { + newRingerMode = AudioManager.RINGER_MODE_VIBRATE; + } + + if (newRingerMode != mRingerMode) { + setRingerMode(newRingerMode); + + /* + * If we are changing ringer modes, do not increment/decrement the + * volume index. Instead, the handler for the message above will + * take care of changing the index. + */ + adjustVolumeIndex = false; + } + + return adjustVolumeIndex; + } + + public boolean isStreamAffectedByRingerMode(int streamType) { + return (mRingerModeAffectedStreams & (1 << streamType)) != 0; + } + + public boolean isStreamAffectedByMute(int streamType) { + return (mMuteAffectedStreams & (1 << streamType)) != 0; + } + + private void ensureValidDirection(int direction) { + if (direction < AudioManager.ADJUST_LOWER || direction > AudioManager.ADJUST_RAISE) { + throw new IllegalArgumentException("Bad direction " + direction); + } + } + + private void ensureValidStreamType(int streamType) { + if (streamType < 0 || streamType >= mStreamStates.length) { + throw new IllegalArgumentException("Bad stream type " + streamType); + } + } + + private int getActiveStreamType(int suggestedStreamType) { + boolean isOffhook = false; + try { + ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone")); + if (phone != null) isOffhook = phone.isOffhook(); + } catch (RemoteException e) { + Log.w(TAG, "Couldn't connect to phone service", e); + } + + if ((getRouting(AudioSystem.MODE_IN_CALL) & AudioSystem.ROUTE_BLUETOOTH_SCO) != 0) { + // Log.v(TAG, "getActiveStreamType: Forcing STREAM_BLUETOOTH_SCO..."); + return AudioSystem.STREAM_BLUETOOTH_SCO; + } else if (isOffhook) { + // Log.v(TAG, "getActiveStreamType: Forcing STREAM_VOICE_CALL..."); + return AudioSystem.STREAM_VOICE_CALL; + } else if (AudioSystem.isMusicActive()) { + // Log.v(TAG, "getActiveStreamType: Forcing STREAM_MUSIC..."); + return AudioSystem.STREAM_MUSIC; + } else if (suggestedStreamType == AudioManager.USE_DEFAULT_STREAM_TYPE) { + // Log.v(TAG, "getActiveStreamType: Forcing STREAM_RING..."); + return AudioSystem.STREAM_RING; + } else { + // Log.v(TAG, "getActiveStreamType: Returning suggested type " + suggestedStreamType); + return suggestedStreamType; + } + } + + private void broadcastRingerMode() { + // Send sticky broadcast + if (ActivityManagerNative.isSystemReady()) { + Intent broadcast = new Intent(AudioManager.RINGER_MODE_CHANGED_ACTION); + broadcast.putExtra(AudioManager.EXTRA_RINGER_MODE, mRingerMode); + long origCallerIdentityToken = Binder.clearCallingIdentity(); + mContext.sendStickyBroadcast(broadcast); + Binder.restoreCallingIdentity(origCallerIdentityToken); + } + } + + private void broadcastVibrateSetting(int vibrateType) { + // Send broadcast + if (ActivityManagerNative.isSystemReady()) { + Intent broadcast = new Intent(AudioManager.VIBRATE_SETTING_CHANGED_ACTION); + broadcast.putExtra(AudioManager.EXTRA_VIBRATE_TYPE, vibrateType); + broadcast.putExtra(AudioManager.EXTRA_VIBRATE_SETTING, getVibrateSetting(vibrateType)); + mContext.sendBroadcast(broadcast); + } + } + + // Message helper methods + private static int getMsg(int baseMsg, int streamType) { + return (baseMsg & 0xffff) | streamType << 16; + } + + private static int getMsgBase(int msg) { + return msg & 0xffff; + } + + private static void sendMsg(Handler handler, int baseMsg, int streamType, + int existingMsgPolicy, int arg1, int arg2, Object obj, int delay) { + int msg = (streamType == SHARED_MSG) ? baseMsg : getMsg(baseMsg, streamType); + + if (existingMsgPolicy == SENDMSG_REPLACE) { + handler.removeMessages(msg); + } else if (existingMsgPolicy == SENDMSG_NOOP && handler.hasMessages(msg)) { + return; + } + + handler + .sendMessageDelayed(handler.obtainMessage(msg, arg1, arg2, obj), delay); + } + + boolean checkAudioSettingsPermission(String method) { + if (mContext.checkCallingOrSelfPermission("android.permission.MODIFY_AUDIO_SETTINGS") + == PackageManager.PERMISSION_GRANTED) { + return true; + } + String msg = "Audio Settings Permission Denial: " + method + " from pid=" + + Binder.getCallingPid() + + ", uid=" + Binder.getCallingUid(); + Log.w(TAG, msg); + return false; + } + + + /////////////////////////////////////////////////////////////////////////// + // Inner classes + /////////////////////////////////////////////////////////////////////////// + + public class VolumeStreamState { + private final String mVolumeIndexSettingName; + private final String mLastAudibleVolumeIndexSettingName; + private final int mStreamType; + + private final int[] mVolumes; + private int mIndex; + private int mLastAudibleIndex; + private ArrayList<VolumeDeathHandler> mDeathHandlers; //handles mute/solo requests client death + + private VolumeStreamState(String settingName, int streamType, int[] volumes) { + + mVolumeIndexSettingName = settingName; + mLastAudibleVolumeIndexSettingName = settingName + System.APPEND_FOR_LAST_AUDIBLE; + + mStreamType = streamType; + mVolumes = volumes; + + final ContentResolver cr = mContentResolver; + mIndex = getValidIndex(Settings.System.getInt(cr, mVolumeIndexSettingName, AudioManager.DEFAULT_STREAM_VOLUME[streamType])); + mLastAudibleIndex = getValidIndex(Settings.System.getInt(cr, + mLastAudibleVolumeIndexSettingName, mIndex > 0 ? mIndex : AudioManager.DEFAULT_STREAM_VOLUME[streamType])); + + AudioSystem.setVolume(streamType, volumes[mIndex]); + mDeathHandlers = new ArrayList<VolumeDeathHandler>(); + } + + /** + * Constructor to be used when there is no setting associated with the VolumeStreamState. + * + * @param defaultVolume Default volume of the stream to use. + * @param streamType Type of the stream. + * @param volumes Volumes levels associated with this stream. + */ + private VolumeStreamState(int defaultVolume, int streamType, int[] volumes) { + mVolumeIndexSettingName = null; + mLastAudibleVolumeIndexSettingName = null; + mIndex = mLastAudibleIndex = defaultVolume; + mStreamType = streamType; + mVolumes = volumes; + AudioSystem.setVolume(mStreamType, defaultVolume); + mDeathHandlers = new ArrayList<VolumeDeathHandler>(); + } + + public boolean adjustIndex(int deltaIndex) { + return setIndex(mIndex + deltaIndex); + } + + public boolean setIndex(int index) { + int oldIndex = mIndex; + mIndex = getValidIndex(index); + + if (oldIndex != mIndex) { + if (mIndex > 0) { + mLastAudibleIndex = mIndex; + } + return true; + } else { + return false; + } + } + + public int getMaxIndex() { + return mVolumes.length - 1; + } + + public void mute(IBinder cb, boolean state) { + VolumeDeathHandler handler = getDeathHandler(cb, state); + if (handler == null) { + Log.e(TAG, "Could not get client death handler for stream: "+mStreamType); + return; + } + handler.mute(state); + } + + private int getValidIndex(int index) { + if (index < 0) { + return 0; + } else if (index >= mVolumes.length) { + return mVolumes.length - 1; + } + + return index; + } + + private class VolumeDeathHandler implements IBinder.DeathRecipient { + private IBinder mICallback; // To be notified of client's death + private int mMuteCount; // Number of active mutes for this client + + VolumeDeathHandler(IBinder cb) { + mICallback = cb; + } + + public void mute(boolean state) { + synchronized(mDeathHandlers) { + if (state) { + if (mMuteCount == 0) { + // Register for client death notification + try { + mICallback.linkToDeath(this, 0); + mDeathHandlers.add(this); + // If the stream is not yet muted by any client, set lvel to 0 + if (muteCount() == 0) { + setIndex(0); + sendMsg(mAudioHandler, MSG_SET_SYSTEM_VOLUME, mStreamType, SENDMSG_NOOP, 0, 0, + VolumeStreamState.this, 0); + } + } catch (RemoteException e) { + // Client has died! + binderDied(); + mDeathHandlers.notify(); + return; + } + } else { + Log.w(TAG, "stream: "+mStreamType+" was already muted by this client"); + } + mMuteCount++; + } else { + if (mMuteCount == 0) { + Log.e(TAG, "unexpected unmute for stream: "+mStreamType); + } else { + mMuteCount--; + if (mMuteCount == 0) { + // Unregistr from client death notification + mDeathHandlers.remove(this); + mICallback.unlinkToDeath(this, 0); + if (muteCount() == 0) { + // If the stream is not mut any more, restore it's volume if + // ringer mode allows it + if (!isStreamAffectedByRingerMode(mStreamType) || mRingerMode == AudioManager.RINGER_MODE_NORMAL) { + setIndex(mLastAudibleIndex); + sendMsg(mAudioHandler, MSG_SET_SYSTEM_VOLUME, mStreamType, SENDMSG_NOOP, 0, 0, + VolumeStreamState.this, 0); + } + } + } + } + } + mDeathHandlers.notify(); + } + } + + public void binderDied() { + Log.w(TAG, "Volume service client died for stream: "+mStreamType); + if (mMuteCount != 0) { + // Reset all active mute requests from this client. + mMuteCount = 1; + mute(false); + } + } + } + + private int muteCount() { + int count = 0; + int size = mDeathHandlers.size(); + for (int i = 0; i < size; i++) { + count += mDeathHandlers.get(i).mMuteCount; + } + return count; + } + + private VolumeDeathHandler getDeathHandler(IBinder cb, boolean state) { + synchronized(mDeathHandlers) { + VolumeDeathHandler handler; + int size = mDeathHandlers.size(); + for (int i = 0; i < size; i++) { + handler = mDeathHandlers.get(i); + if (cb.equals(handler.mICallback)) { + return handler; + } + } + // If this is the first mute request for this client, create a new + // client death handler. Otherwise, it is an out of sequence unmute request. + if (state) { + handler = new VolumeDeathHandler(cb); + } else { + Log.w(TAG, "stream was not muted by this client"); + handler = null; + } + return handler; + } + } + } + + /** Thread that handles native AudioSystem control. */ + private class AudioSystemThread extends Thread { + AudioSystemThread() { + super("AudioService"); + } + + @Override + public void run() { + // Set this thread up so the handler will work on it + Looper.prepare(); + + synchronized(AudioService.this) { + mAudioHandler = new AudioHandler(); + + // Notify that the handler has been created + AudioService.this.notify(); + } + + // Listen for volume change requests that are set by VolumePanel + Looper.loop(); + } + } + + /** Handles internal volume messages in separate volume thread. */ + private class AudioHandler extends Handler { + + private void setSystemVolume(VolumeStreamState streamState) { + + // Adjust volume + AudioSystem + .setVolume(streamState.mStreamType, streamState.mVolumes[streamState.mIndex]); + + // Post a persist volume msg + sendMsg(mAudioHandler, MSG_PERSIST_VOLUME, streamState.mStreamType, + SENDMSG_REPLACE, 0, 0, streamState, PERSIST_DELAY); + } + + private void persistVolume(VolumeStreamState streamState) { + System.putInt(mContentResolver, streamState.mVolumeIndexSettingName, + streamState.mIndex); + System.putInt(mContentResolver, streamState.mLastAudibleVolumeIndexSettingName, + streamState.mLastAudibleIndex); + } + + private void persistRingerMode() { + System.putInt(mContentResolver, System.MODE_RINGER, mRingerMode); + } + + private void persistVibrateSetting() { + System.putInt(mContentResolver, System.VIBRATE_ON, mVibrateSetting); + } + + private void playSoundEffect(int effectType, int volume) { + synchronized (mSoundEffectsLock) { + if (mSoundPool == null) { + return; + } + + if (SOUND_EFFECT_FILES_MAP[effectType][1] > 0) { + float v = (float) volume / 1000.0f; + mSoundPool.play(SOUND_EFFECT_FILES_MAP[effectType][1], v, v, 0, 0, 1.0f); + } else { + MediaPlayer mediaPlayer = new MediaPlayer(); + if (mediaPlayer != null) { + try { + String filePath = Environment.getRootDirectory() + SOUND_EFFECTS_PATH + SOUND_EFFECT_FILES[SOUND_EFFECT_FILES_MAP[effectType][0]]; + mediaPlayer.setDataSource(filePath); + mediaPlayer.setAudioStreamType(AudioSystem.STREAM_SYSTEM); + mediaPlayer.prepare(); + mediaPlayer.setOnCompletionListener(new OnCompletionListener() { + public void onCompletion(MediaPlayer mp) { + cleanupPlayer(mp); + } + }); + mediaPlayer.setOnErrorListener(new OnErrorListener() { + public boolean onError(MediaPlayer mp, int what, int extra) { + cleanupPlayer(mp); + return true; + } + }); + mediaPlayer.start(); + } catch (IOException ex) { + Log.w(TAG, "MediaPlayer IOException: "+ex); + } catch (IllegalArgumentException ex) { + Log.w(TAG, "MediaPlayer IllegalArgumentException: "+ex); + } catch (IllegalStateException ex) { + Log.w(TAG, "MediaPlayer IllegalStateException: "+ex); + } + } + } + } + } + + private void cleanupPlayer(MediaPlayer mp) { + if (mp != null) { + try { + mp.stop(); + mp.release(); + } catch (IllegalStateException ex) { + Log.w(TAG, "MediaPlayer IllegalStateException: "+ex); + } + } + } + + @Override + public void handleMessage(Message msg) { + int baseMsgWhat = getMsgBase(msg.what); + + switch (baseMsgWhat) { + + case MSG_SET_SYSTEM_VOLUME: + setSystemVolume((VolumeStreamState) msg.obj); + break; + + case MSG_PERSIST_VOLUME: + persistVolume((VolumeStreamState) msg.obj); + break; + + case MSG_PERSIST_RINGER_MODE: + persistRingerMode(); + break; + + case MSG_PERSIST_VIBRATE_SETTING: + persistVibrateSetting(); + break; + + case MSG_MEDIA_SERVER_DIED: + Log.e(TAG, "Media server died."); + // Force creation of new IAudioflinger interface + mMediaServerOk = false; + AudioSystem.getMode(); + break; + + case MSG_MEDIA_SERVER_STARTED: + Log.e(TAG, "Media server started."); + // Restore audio routing and stream volumes + applyAudioSettings(); + int numStreamTypes = AudioSystem.getNumStreamTypes(); + for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) { + int volume; + VolumeStreamState streamState = mStreamStates[streamType]; + if (streamState.muteCount() == 0) { + volume = streamState.mVolumes[streamState.mIndex]; + } else { + volume = streamState.mVolumes[0]; + } + AudioSystem.setVolume(streamType, volume); + } + setRingerMode(mRingerMode); + mMediaServerOk = true; + break; + + case MSG_PLAY_SOUND_EFFECT: + playSoundEffect(msg.arg1, msg.arg2); + break; + } + } + } + +} diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java new file mode 100644 index 0000000..d0fa795 --- /dev/null +++ b/media/java/android/media/AudioSystem.java @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2006 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; + + +/* IF YOU CHANGE ANY OF THE CONSTANTS IN THIS FILE, DO NOT FORGET + * TO UPDATE THE CORRESPONDING NATIVE GLUE. THANK YOU FOR YOUR COOPERATION + */ + +/** + * @hide + */ +public class AudioSystem +{ + /* FIXME: Need to finalize this and correlate with native layer */ + /* + * If these are modified, please also update Settings.System.VOLUME_SETTINGS + * and attrs.xml + */ + /* The audio stream for phone calls */ + public static final int STREAM_VOICE_CALL = 0; + /* The audio stream for system sounds */ + public static final int STREAM_SYSTEM = 1; + /* The audio stream for the phone ring and message alerts */ + public static final int STREAM_RING = 2; + /* The audio stream for music playback */ + public static final int STREAM_MUSIC = 3; + /* The audio stream for alarms */ + public static final int STREAM_ALARM = 4; + /* The audio stream for notifications */ + public static final int STREAM_NOTIFICATION = 5; + /* @hide The audio stream for phone calls when connected on bluetooth */ + public static final int STREAM_BLUETOOTH_SCO = 6; + /** + * @deprecated Use {@link #numStreamTypes() instead} + */ + public static final int NUM_STREAMS = 5; + + // Expose only the getter method publicly so we can change it in the future + private static final int NUM_STREAM_TYPES = 7; + public static final int getNumStreamTypes() { return NUM_STREAM_TYPES; } + + /* max and min volume levels */ + /* Maximum volume setting, for use with setVolume(int,int) */ + public static final int MAX_VOLUME = 100; + /* Minimum volume setting, for use with setVolume(int,int) */ + public static final int MIN_VOLUME = 0; + + /* + * Sets the volume of a specified audio stream. + * + * param type the stream type to set the volume of (e.g. STREAM_MUSIC) + * param volume the volume level to set (0-100) + * return command completion status see AUDIO_STATUS_OK, see AUDIO_STATUS_ERROR + */ + public static native int setVolume(int type, int volume); + + /* + * Returns the volume of a specified audio stream. + * + * param type the stream type to get the volume of (e.g. STREAM_MUSIC) + * return the current volume (0-100) + */ + public static native int getVolume(int type); + + /* + * Sets the microphone mute on or off. + * + * param on set <var>true</var> to mute the microphone; + * <var>false</var> to turn mute off + * return command completion status see AUDIO_STATUS_OK, see AUDIO_STATUS_ERROR + */ + public static native int muteMicrophone(boolean on); + + /* + * Checks whether the microphone mute is on or off. + * + * return true if microphone is muted, false if it's not + */ + public static native boolean isMicrophoneMuted(); + + /* + * Sets the audio mode. + * + * param mode the requested audio mode (NORMAL, RINGTONE, or IN_CALL). + * Informs the HAL about the current audio state so that + * it can route the audio appropriately. + * return command completion status see AUDIO_STATUS_OK, see AUDIO_STATUS_ERROR + */ + public static native int setMode(int mode); + + /* + * Returns the current audio mode. + * + * return the current audio mode (NORMAL, RINGTONE, or IN_CALL). + * Returns the current current audio state from the HAL. + */ + public static native int getMode(); + + /* modes for setMode/getMode/setRoute/getRoute */ + public static final int MODE_INVALID = -2; + public static final int MODE_CURRENT = -1; + public static final int MODE_NORMAL = 0; + public static final int MODE_RINGTONE = 1; + public static final int MODE_IN_CALL = 2; + public static final int NUM_MODES = 3; + + + /* Routing bits for setRouting/getRouting API */ + public static final int ROUTE_EARPIECE = (1 << 0); + public static final int ROUTE_SPEAKER = (1 << 1); + + /** @deprecated use {@link #ROUTE_BLUETOOTH_SCO} */ + @Deprecated public static final int ROUTE_BLUETOOTH = (1 << 2); + public static final int ROUTE_BLUETOOTH_SCO = (1 << 2); + public static final int ROUTE_HEADSET = (1 << 3); + public static final int ROUTE_BLUETOOTH_A2DP = (1 << 4); + public static final int ROUTE_ALL = 0xFFFFFFFF; + + /* + * Sets the audio routing for a specified mode + * + * param mode audio mode to change route. E.g., MODE_RINGTONE. + * param routes bit vector of routes requested, created from one or + * more of ROUTE_xxx types. Set bits indicate that route should be on + * param mask bit vector of routes to change, created from one or more of + * ROUTE_xxx types. Unset bits indicate the route should be left unchanged + * return command completion status see AUDIO_STATUS_OK, see AUDIO_STATUS_ERROR + */ + public static native int setRouting(int mode, int routes, int mask); + + /* + * Returns the current audio routing bit vector for a specified mode. + * + * param mode audio mode to change route (e.g., MODE_RINGTONE) + * return an audio route bit vector that can be compared with ROUTE_xxx + * bits + */ + public static native int getRouting(int mode); + + /* + * Checks whether any music is active. + * + * return true if any music tracks are active. + */ + public static native boolean isMusicActive(); + + /* + * Sets a generic audio configuration parameter. The use of these parameters + * are platform dependant, see libaudio + * + * ** Temporary interface - DO NOT USE + * + * TODO: Replace with a more generic key:value get/set mechanism + * + * param key name of parameter to set. Must not be null. + * param value value of parameter. Must not be null. + */ + public static native void setParameter(String key, String value); + + /* + private final static String TAG = "audio"; + + private void log(String msg) { + Log.d(TAG, "[AudioSystem] " + msg); + } + */ + + // These match the enum in libs/android_runtime/android_media_AudioSystem.cpp + /* Command sucessful or Media server restarted. see ErrorCallback */ + public static final int AUDIO_STATUS_OK = 0; + /* Command failed or unspecified audio error. see ErrorCallback */ + public static final int AUDIO_STATUS_ERROR = 1; + /* Media server died. see ErrorCallback */ + public static final int AUDIO_STATUS_SERVER_DIED = 100; + + private static ErrorCallback mErrorCallback; + + /* + * Handles the audio error callback. + */ + public interface ErrorCallback + { + /* + * Callback for audio server errors. + * param error error code: + * - AUDIO_STATUS_OK + * - AUDIO_STATUS_SERVER_DIED + * - UDIO_STATUS_ERROR + */ + void onError(int error); + }; + + /* + * Registers a callback to be invoked when an error occurs. + * param cb the callback to run + */ + public static void setErrorCallback(ErrorCallback cb) + { + mErrorCallback = cb; + } + + private static void errorCallbackFromNative(int error) + { + if (mErrorCallback != null) { + mErrorCallback.onError(error); + } + } +} diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java new file mode 100644 index 0000000..997cd44 --- /dev/null +++ b/media/java/android/media/AudioTrack.java @@ -0,0 +1,1050 @@ +/* + * Copyright (C) 2008 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 java.lang.ref.WeakReference; +import java.lang.IllegalArgumentException; +import java.lang.IllegalStateException; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.media.AudioManager; +import android.util.Log; + + +/** + * The AudioTrack class manages and plays a single audio resource for Java applications. + * It allows to stream PCM audio buffers to the audio hardware for playback. This is + * achieved by "pushing" the data to the AudioTrack object using one of the + * {@link #write(byte[], int, int)} and {@link #write(short[], int, int)} methods. + * <p>An AudioTrack instance can operate under two modes: static of streaming.<br> + * The Streaming mode consists in continuously writing data to the AudioTrack, using one + * of the write() methods. These are blocking and return when the data has been transferred + * from the Java layer to the native layer, and is queued for playback. The streaming mode + * is most useful when playing blocks of audio data that for instance are: + * <ul> + * <li>too big to fit in memory because of the duration of the sound to play,</li> + * <li>too big to fit in memory because of the characteristics of the audio data + * (high sampling rate, bits per sample ...)</li> + * <li>chosen, received or generated as the audio keeps playing.</li> + * </ul> + * The static mode is to be chosen when dealing with short sounds that fit in memory and + * that need to be played with the smallest latency possible. Static mode AudioTrack instances can + * play the sound without the need to transfer the audio data from Java to the audio hardware + * each time the sound is to be played. The static mode will therefore be preferred for UI and + * game sounds that are played often, and with the smallest overhead possible. + * <p>Upon creation, an AudioTrack object initializes its associated audio buffer. + * The size of this buffer, specified during the construction, determines how long an AudioTrack + * can play before running out of data.<br> + * For an AudioTrack using the static mode, this size is the maximum size of the sound that can + * be played from it.<br> + * For the streaming mode, data will be written to the hardware in chunks of + * sizes inferior to the total buffer size. + */ +public class AudioTrack +{ + //--------------------------------------------------------- + // Constants + //-------------------- + /** Minimum value for a channel volume */ + private static final float VOLUME_MIN = 0.0f; + /** Maximum value for a channel volume */ + private static final float VOLUME_MAX = 1.0f; + + /** state of an AudioTrack this is stopped */ + public static final int PLAYSTATE_STOPPED = 1; // matches SL_PLAYSTATE_STOPPED + /** state of an AudioTrack this is paused */ + public static final int PLAYSTATE_PAUSED = 2; // matches SL_PLAYSTATE_PAUSED + /** state of an AudioTrack this is playing */ + public static final int PLAYSTATE_PLAYING = 3; // matches SL_PLAYSTATE_PLAYING + + /** + * Creation mode where audio data is transferred from Java to the native layer + * only once before the audio starts playing. + */ + public static final int MODE_STATIC = 0; + /** + * Creation mode where audio data is streamed from Java to the native layer + * as the audio is playing. + */ + public static final int MODE_STREAM = 1; + + /** + * State of an AudioTrack that was not successfully initialized upon creation + */ + public static final int STATE_UNINITIALIZED = 0; + /** + * State of an AudioTrack that is ready to be used. + */ + public static final int STATE_INITIALIZED = 1; + /** + * State of a successfully initialized AudioTrack that uses static data, + * but that hasn't received that data yet. + */ + public static final int STATE_NO_STATIC_DATA = 2; + + // Error codes: + // to keep in sync with frameworks/base/core/jni/android_media_AudioTrack.cpp + /** + * Denotes a successful operation. + */ + public static final int SUCCESS = 0; + /** + * Denotes a generic operation failure. + */ + public static final int ERROR = -1; + /** + * Denotes a failure due to the use of an invalid value. + */ + public static final int ERROR_BAD_VALUE = -2; + /** + * Denotes a failure due to the improper use of a method. + */ + public static final int ERROR_INVALID_OPERATION = -3; + + private static final int ERROR_NATIVESETUP_AUDIOSYSTEM = -16; + private static final int ERROR_NATIVESETUP_INVALIDCHANNELCOUNT = -17; + private static final int ERROR_NATIVESETUP_INVALIDFORMAT = -18; + private static final int ERROR_NATIVESETUP_INVALIDSTREAMTYPE = -19; + private static final int ERROR_NATIVESETUP_NATIVEINITFAILED = -20; + + // Events: + // to keep in sync with frameworks/base/include/media/AudioTrack.h + /** + * Event id for when the playback head has reached a previously set marker. + */ + private static final int NATIVE_EVENT_MARKER = 3; + /** + * Event id for when the previously set update period has passed during playback. + */ + private static final int NATIVE_EVENT_NEW_POS = 4; + + private final static String TAG = "AudioTrack-Java"; + + + //-------------------------------------------------------------------------- + // Member variables + //-------------------- + /** + * Indicates the state of the AudioTrack instance + */ + private int mState = STATE_UNINITIALIZED; + /** + * Indicates the play state of the AudioTrack instance + */ + private int mPlayState = PLAYSTATE_STOPPED; + /** + * Lock to make sure mPlayState updates are reflecting the actual state of the object. + */ + private final Object mPlayStateLock = new Object(); + /** + * The listener the AudioTrack notifies previously set marker is reached. + * @see #setMarkerReachedListener(OnMarkerReachedListener) + */ + private OnMarkerReachedListener mMarkerListener = null; + /** + * Lock to protect marker listener updates against event notifications + */ + private final Object mMarkerListenerLock = new Object(); + /** + * The listener the AudioTrack notifies periodically during playback. + * @see #setPeriodicNotificationListener(OnPeriodicNotificationListener) + */ + private OnPeriodicNotificationListener mPeriodicListener = null; + /** + * Lock to protect periodic listener updates against event notifications + */ + private final Object mPeriodicListenerLock = new Object(); + /** + * Size of the native audio buffer. + */ + private int mNativeBufferSizeInBytes = 0; + /** + * Handler for events coming from the native code + */ + private NativeEventHandler mNativeEventHandler = null; + /** + * The audio data sampling rate in Hz. + */ + private int mSampleRate = 22050; + /** + * The number of input audio channels (1 is mono, 2 is stereo) + */ + private int mChannelCount = 1; + /** + * The type of the audio stream to play. See + * {@link AudioManager#STREAM_VOICE_CALL}, {@link AudioManager#STREAM_SYSTEM}, + * {@link AudioManager#STREAM_RING}, {@link AudioManager#STREAM_MUSIC} and + * {@link AudioManager#STREAM_ALARM} + */ + private int mStreamType = AudioManager.STREAM_MUSIC; + /** + * The way audio is consumed by the hardware, streaming or static. + */ + private int mDataLoadMode = MODE_STREAM; + /** + * The current audio channel configuration + */ + private int mChannelConfiguration = AudioFormat.CHANNEL_CONFIGURATION_MONO; + /** + * The encoding of the audio samples. + * @see AudioFormat#ENCODING_PCM_8BIT + * @see AudioFormat#ENCODING_PCM_16BIT + */ + private int mAudioFormat = AudioFormat.ENCODING_PCM_16BIT; + + + //-------------------------------- + // Used exclusively by native code + //-------------------- + /** + * Accessed by native methods: provides access to C++ AudioTrack object + */ + @SuppressWarnings("unused") + private int mNativeTrackInJavaObj; + /** + * Accessed by native methods: provides access to the JNI data (i.e. resources used by + * the native AudioTrack object, but not stored in it). + */ + @SuppressWarnings("unused") + private int mJniData; + + + //-------------------------------------------------------------------------- + // Constructor, Finalize + //-------------------- + /** + * Class constructor. + * @param streamType the type of the audio stream. See + + * {@link AudioManager#STREAM_VOICE_CALL}, {@link AudioManager#STREAM_SYSTEM}, + * {@link AudioManager#STREAM_RING}, {@link AudioManager#STREAM_MUSIC} and + * {@link AudioManager#STREAM_ALARM} + * @param sampleRateInHz the sample rate expressed in Hertz. Examples of rates are (but + * not limited to) 44100, 22050 and 11025. + * @param channelConfig describes the configuration of the audio channels. + + * See {@link AudioFormat#CHANNEL_CONFIGURATION_MONO} and + * {@link AudioFormat#CHANNEL_CONFIGURATION_STEREO} + + * @param audioFormat the format in which the audio data is represented. + * See {@link AudioFormat#ENCODING_PCM_16BIT} and + * {@link AudioFormat#ENCODING_PCM_8BIT} + * @param bufferSizeInBytes the total size (in bytes) of the buffer where audio data is read + * from for playback. If using the AudioTrack in streaming mode, you can write data into + * this buffer in smaller chunks than this size. If using the AudioTrack in static mode, + * this is the maximum size of the sound that will be played for this instance. + * @param mode streaming or static buffer. See {@link #MODE_STATIC} and {@link #MODE_STREAM} + * @throws java.lang.IllegalArgumentException + */ + public AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat, + int bufferSizeInBytes, int mode) + throws IllegalArgumentException { + mState = STATE_UNINITIALIZED; + + audioParamCheck(streamType, sampleRateInHz, channelConfig, audioFormat, mode); + + audioBuffSizeCheck(bufferSizeInBytes); + + // native initialization + int initResult = native_setup(new WeakReference<AudioTrack>(this), + mStreamType, mSampleRate, mChannelCount, mAudioFormat, + mNativeBufferSizeInBytes, mDataLoadMode); + if (initResult != SUCCESS) { + loge("Error code "+initResult+" when initializing AudioTrack."); + return; // with mState == STATE_UNINITIALIZED + } + + if (mDataLoadMode == MODE_STATIC) { + mState = STATE_NO_STATIC_DATA; + } else { + mState = STATE_INITIALIZED; + } + } + + + // Convenience method for the constructor's parameter checks. + // This is where constructor IllegalArgumentException-s are thrown + // postconditions: + // mStreamType is valid + // mChannelCount is valid + // mAudioFormat is valid + // mSampleRate is valid + // mDataLoadMode is valid + private void audioParamCheck(int streamType, int sampleRateInHz, + int channelConfig, int audioFormat, int mode) { + + //-------------- + // stream type + if( (streamType != AudioManager.STREAM_ALARM) && (streamType != AudioManager.STREAM_MUSIC) + && (streamType != AudioManager.STREAM_RING) && (streamType != AudioManager.STREAM_SYSTEM) + && (streamType != AudioManager.STREAM_VOICE_CALL) + && (streamType != AudioManager.STREAM_NOTIFICATION) + && (streamType != AudioManager.STREAM_BLUETOOTH_SCO)) { + throw (new IllegalArgumentException("Invalid stream type.")); + } else { + mStreamType = streamType; + } + + //-------------- + // sample rate + if ( (sampleRateInHz < 4000) || (sampleRateInHz > 48000) ) { + throw (new IllegalArgumentException(sampleRateInHz + + "Hz is not a supported sample rate.")); + } else { + mSampleRate = sampleRateInHz; + } + + //-------------- + // channel config + switch (channelConfig) { + case AudioFormat.CHANNEL_CONFIGURATION_DEFAULT: + case AudioFormat.CHANNEL_CONFIGURATION_MONO: + mChannelCount = 1; + mChannelConfiguration = AudioFormat.CHANNEL_CONFIGURATION_MONO; + break; + case AudioFormat.CHANNEL_CONFIGURATION_STEREO: + mChannelCount = 2; + mChannelConfiguration = AudioFormat.CHANNEL_CONFIGURATION_STEREO; + break; + default: + mChannelCount = 0; + mChannelConfiguration = AudioFormat.CHANNEL_CONFIGURATION_INVALID; + throw(new IllegalArgumentException("Unsupported channel configuration.")); + } + + //-------------- + // audio format + switch (audioFormat) { + case AudioFormat.ENCODING_DEFAULT: + mAudioFormat = AudioFormat.ENCODING_PCM_16BIT; + break; + case AudioFormat.ENCODING_PCM_16BIT: + case AudioFormat.ENCODING_PCM_8BIT: + mAudioFormat = audioFormat; + break; + default: + mAudioFormat = AudioFormat.ENCODING_INVALID; + throw(new IllegalArgumentException("Unsupported sample encoding." + + " Should be ENCODING_PCM_8BIT or ENCODING_PCM_16BIT.")); + } + + //-------------- + // audio load mode + if ( (mode != MODE_STREAM) && (mode != MODE_STATIC) ) { + throw(new IllegalArgumentException("Invalid mode.")); + } else { + mDataLoadMode = mode; + } + } + + + // Convenience method for the contructor's audio buffer size check. + // preconditions: + // mChannelCount is valid + // mAudioFormat is valid + // postcondition: + // mNativeBufferSizeInBytes is valid (multiple of frame size, positive) + private void audioBuffSizeCheck(int audioBufferSize) { + // NB: this section is only valid with PCM data. + // To update when supporting compressed formats + int frameSizeInBytes = mChannelCount + * (mAudioFormat == AudioFormat.ENCODING_PCM_8BIT ? 1 : 2); + if ((audioBufferSize % frameSizeInBytes != 0) || (audioBufferSize < 1)) { + throw (new IllegalArgumentException("Invalid audio buffer size.")); + } + + mNativeBufferSizeInBytes = audioBufferSize; + } + + + // Convenience method for the creation of the native event handler + // It is called only when a non-null event listener is set. + // precondition: + // mNativeEventHandler is null + private void createNativeEventHandler() { + Looper looper; + if ((looper = Looper.myLooper()) != null) { + mNativeEventHandler = new NativeEventHandler(this, looper); + } else if ((looper = Looper.getMainLooper()) != null) { + mNativeEventHandler = new NativeEventHandler(this, looper); + } else { + mNativeEventHandler = null; + } + } + + + /** + * Releases the native AudioTrack resources. + */ + public void release() { + // even though native_release() stops the native AudioTrack, we need to stop + // AudioTrack subclasses too. + try { + stop(); + } catch(IllegalStateException ise) { + // don't raise an exception, we're releasing the resources. + } + native_release(); + mState = STATE_UNINITIALIZED; + } + + @Override + protected void finalize() { + native_finalize(); + } + + //-------------------------------------------------------------------------- + // Getters + //-------------------- + /** + * Returns the minimum valid volume value. Volume values set under this one will + * be clamped at this value. + * @return the minimum volume expressed as a linear attenuation. + */ + static public float getMinVolume() { + return AudioTrack.VOLUME_MIN; + } + + /** + * Returns the maximum valid volume value. Volume values set above this one will + * be clamped at this value. + * @return the maximum volume expressed as a linear attenuation. + */ + static public float getMaxVolume() { + return AudioTrack.VOLUME_MAX; + } + + /** + * Returns the configured audio data sample rate in Hz + */ + public int getSampleRate() { + return mSampleRate; + } + + /** + * @hide + * Returns the current playback rate in Hz. Note that this rate may differ from one set using + * {@link #setPlaybackRate(int)} as the value effectively set is implementation-dependent. + */ + public int getPlaybackRate() { + return native_get_playback_rate(); + } + + /** + * Returns the configured audio data format. See {@link AudioFormat#ENCODING_PCM_16BIT} + * and {@link AudioFormat#ENCODING_PCM_8BIT}. + */ + public int getAudioFormat() { + return mAudioFormat; + } + + /** + * Returns the type of audio stream this AudioTrack is configured for. + * Compare the result against {@link AudioManager#STREAM_VOICE_CALL}, + * {@link AudioManager#STREAM_SYSTEM}, {@link AudioManager#STREAM_RING}, + * {@link AudioManager#STREAM_MUSIC} or {@link AudioManager#STREAM_ALARM} + */ + public int getStreamType() { + return mStreamType; + } + + /** + * Returns the configured channel configuration. + + * See {@link AudioFormat#CHANNEL_CONFIGURATION_MONO} + * and {@link AudioFormat#CHANNEL_CONFIGURATION_STEREO}. + */ + public int getChannelConfiguration() { + return mChannelConfiguration; + } + + /** + * Returns the configured number of channels. + */ + public int getChannelCount() { + return mChannelCount; + } + + /** + * Returns the state of the AudioTrack instance. This is useful after the + * AudioTrack instance has been created to check if it was initialized + * properly. This ensures that the appropriate hardware resources have been + * acquired. + */ + public int getState() { + return mState; + } + + /** + * Returns the playback state of the AudioTrack instance. + * @see #PLAYSTATE_STOPPED + * @see #PLAYSTATE_PAUSED + * @see #PLAYSTATE_PLAYING + */ + public int getPlayState() { + return mPlayState; + } + + /** + * Returns the native frame count used by the hardware + */ + protected int getNativeFrameCount() { + return native_get_native_frame_count(); + } + + /** + * @return marker position in frames + */ + public int getNotificationMarkerPosition() { + return native_get_marker_pos(); + } + + /** + * @return update period in frames + */ + public int getPositionNotificationPeriod() { + return native_get_pos_update_period(); + } + + /** + * @return playback head position in frames + */ + public int getPlaybackHeadPosition() { + return native_get_position(); + } + + /** + * Returns the hardware output sample rate + */ + static public int getNativeOutputSampleRate(int streamType) { + return native_get_output_sample_rate(streamType); + } + + /** + * {@hide} + * Returns the minimum buffer size required for the successful creation of an AudioTrack + * object to be created in the {@link #MODE_STREAM} mode. + * @param sampleRateInHz the sample rate expressed in Hertz. + * @param channelConfig describes the configuration of the audio channels. + * See {@link AudioFormat#CHANNEL_CONFIGURATION_MONO} and + * {@link AudioFormat#CHANNEL_CONFIGURATION_STEREO} + * @param audioFormat the format in which the audio data is represented. + * See {@link AudioFormat#ENCODING_PCM_16BIT} and + * {@link AudioFormat#ENCODING_PCM_8BIT} + * @return {@link #ERROR_BAD_VALUE} if an invalid parameter was passed, + * or {@link #ERROR} if the implementation was unable to query the hardware for its output + * properties, + * or the minimum buffer size expressed in number of bytes. + */ + static public int getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat) { + int channelCount = 0; + switch(channelConfig) { + case AudioFormat.CHANNEL_CONFIGURATION_MONO: + channelCount = 1; + break; + case AudioFormat.CHANNEL_CONFIGURATION_STEREO: + channelCount = 2; + break; + default: + loge("getMinBufferSize(): Invalid channel configuration."); + return AudioTrack.ERROR_BAD_VALUE; + } + + if ((audioFormat != AudioFormat.ENCODING_PCM_16BIT) + && (audioFormat != AudioFormat.ENCODING_PCM_8BIT)) { + loge("getMinBufferSize(): Invalid audio format."); + return AudioTrack.ERROR_BAD_VALUE; + } + + if ( (sampleRateInHz < 4000) || (sampleRateInHz > 48000) ) { + loge("getMinBufferSize(): " + sampleRateInHz +"Hz is not a supported sample rate."); + return AudioTrack.ERROR_BAD_VALUE; + } + + int size = native_get_min_buff_size(sampleRateInHz, channelCount, audioFormat); + if ((size == -1) || (size == 0)) { + loge("getMinBufferSize(): error querying hardware"); + return AudioTrack.ERROR; + } + else { + return size; + } + } + + + //-------------------------------------------------------------------------- + // Initialization / configuration + //-------------------- + /** + * Sets the listener the AudioTrack notifies when a previously set marker is reached. + * @param listener + */ + public void setMarkerReachedListener(OnMarkerReachedListener listener) { + synchronized (mMarkerListenerLock) { + mMarkerListener = listener; + } + if ((listener != null) && (mNativeEventHandler == null)) { + createNativeEventHandler(); + } + } + + + /** + * Sets the listener the AudioTrack notifies periodically during playback. + * @param listener + */ + public void setPeriodicNotificationListener(OnPeriodicNotificationListener listener) { + synchronized (mPeriodicListenerLock) { + mPeriodicListener = listener; + } + if ((listener != null) && (mNativeEventHandler == null)) { + createNativeEventHandler(); + } + } + + + /** + * Sets the specified left/right output volume values on the AudioTrack. Values are clamped + * to the ({@link #getMinVolume()}, {@link #getMaxVolume()}) interval if outside this range. + * @param leftVolume output attenuation for the left channel. A value of 0.0f is silence, + * a value of 1.0f is no attenuation. + * @param rightVolume output attenuation for the right channel + * @return error code or success, see {@link #SUCCESS}, + * {@link #ERROR_INVALID_OPERATION} + */ + public int setStereoVolume(float leftVolume, float rightVolume) { + if (mState != STATE_INITIALIZED) { + return ERROR_INVALID_OPERATION; + } + + // clamp the volumes + if (leftVolume < getMinVolume()) { + leftVolume = getMinVolume(); + } + if (leftVolume > getMaxVolume()) { + leftVolume = getMaxVolume(); + } + if (rightVolume < getMinVolume()) { + rightVolume = getMinVolume(); + } + if (rightVolume > getMaxVolume()) { + rightVolume = getMaxVolume(); + } + + native_setVolume(leftVolume, rightVolume); + + return SUCCESS; + } + + + /** + * Sets the playback sample rate for this track. This sets the sampling rate at which + * the audio data will be consumed and played back, not the original sampling rate of the + * content. Setting it to half the sample rate of the content will cause the playback to + * last twice as long, but will also result result in a negative pitch shift. + * The current implementation supports a maximum sample rate of twice the hardware output + * sample rate (see {@link #getNativeOutputSampleRate(int)}). Use {@link #getSampleRate()} to + * check the rate actually used in hardware after potential clamping. + * @param sampleRateInHz + * @return error code or success, see {@link #SUCCESS}, {@link #ERROR_BAD_VALUE}, + * {@link #ERROR_INVALID_OPERATION} + */ + public int setPlaybackRate(int sampleRateInHz) { + if (mState != STATE_INITIALIZED) { + return ERROR_INVALID_OPERATION; + } + if (sampleRateInHz <= 0) { + return ERROR_BAD_VALUE; + } + native_set_playback_rate(sampleRateInHz); + return SUCCESS; + } + + + /** + * + * @param markerInFrames marker in frames + * @return error code or success, see {@link #SUCCESS}, {@link #ERROR_BAD_VALUE}, + * {@link #ERROR_INVALID_OPERATION} + */ + public int setNotificationMarkerPosition(int markerInFrames) { + if (mState != STATE_INITIALIZED) { + return ERROR_INVALID_OPERATION; + } + return native_set_marker_pos(markerInFrames); + } + + + /** + * @param periodInFrames update period in frames + * @return error code or success, see {@link #SUCCESS}, {@link #ERROR_INVALID_OPERATION} + */ + public int setPositionNotificationPeriod(int periodInFrames) { + if (mState != STATE_INITIALIZED) { + return ERROR_INVALID_OPERATION; + } + return native_set_pos_update_period(periodInFrames); + } + + + /** + * Sets the playback head position. The track must be stopped for the position to be changed. + * @param positionInFrames playback head position in frames + * @return error code or success, see {@link #SUCCESS}, {@link #ERROR_BAD_VALUE}, + * {@link #ERROR_INVALID_OPERATION} + */ + public int setPlaybackHeadPosition(int positionInFrames) { + synchronized(mPlayStateLock) { + if ((mPlayState == PLAYSTATE_STOPPED) || (mPlayState == PLAYSTATE_PAUSED)) { + return native_set_position(positionInFrames); + } else { + return ERROR_INVALID_OPERATION; + } + } + } + + /** + * Sets the loop points and the loop count. The loop can be infinite. + * @param startInFrames loop start marker in frames + * @param endInFrames loop end marker in frames + * @param loopCount the number of times the loop is looped. + * A value of -1 means infinite looping. + * @return error code or success, see {@link #SUCCESS}, {@link #ERROR_BAD_VALUE}, + * {@link #ERROR_INVALID_OPERATION} + */ + public int setLoopPoints(int startInFrames, int endInFrames, int loopCount) { + if (mDataLoadMode == MODE_STREAM) { + return ERROR_INVALID_OPERATION; + } + return native_set_loop(startInFrames, endInFrames, loopCount); + } + + /** + * Sets the initialization state of the instance. To be used in an AudioTrack subclass + * constructor to set a subclass-specific post-initialization state. + * @param state the state of the AudioTrack instance + */ + protected void setState(int state) { + mState = state; + } + + + //--------------------------------------------------------- + // Transport control methods + //-------------------- + /** + * Starts playing an AudioTrack. + * @throws IllegalStateException + */ + public void play() + throws IllegalStateException { + if (mState != STATE_INITIALIZED) { + throw(new IllegalStateException("play() called on uninitialized AudioTrack.")); + } + + synchronized(mPlayStateLock) { + native_start(); + mPlayState = PLAYSTATE_PLAYING; + } + } + + /** + * Stops playing the audio data. + * @throws IllegalStateException + */ + public void stop() + throws IllegalStateException { + if (mState != STATE_INITIALIZED) { + throw(new IllegalStateException("stop() called on uninitialized AudioTrack.")); + } + + // stop playing + synchronized(mPlayStateLock) { + native_stop(); + mPlayState = PLAYSTATE_STOPPED; + } + } + + /** + * Pauses the playback of the audio data. + * @throws IllegalStateException + */ + public void pause() + throws IllegalStateException { + if (mState != STATE_INITIALIZED) { + throw(new IllegalStateException("pause() called on uninitialized AudioTrack.")); + } + //logd("pause()"); + + // pause playback + synchronized(mPlayStateLock) { + native_pause(); + mPlayState = PLAYSTATE_PAUSED; + } + } + + + //--------------------------------------------------------- + // Audio data supply + //-------------------- + + /** + * Flushes the audio data currently queued for playback. + */ + + public void flush() { + if (mState == STATE_INITIALIZED) { + // flush the data in native layer + native_flush(); + } + + } + + /** + * Writes the audio data to the audio hardware for playback. + * @param audioData the array that holds the data to play. + * @param offsetInBytes the offset in audioData where the data to play starts. + * @param sizeInBytes the number of bytes to read in audioData after the offset. + * @return the number of bytes that were written or {@link #ERROR_INVALID_OPERATION} + * if the object wasn't properly initialized, or {@link #ERROR_BAD_VALUE} if + * the parameters don't resolve to valid data and indexes. + */ + + public int write(byte[] audioData,int offsetInBytes, int sizeInBytes) { + if ((mDataLoadMode == MODE_STATIC) + && (mState == STATE_NO_STATIC_DATA) + && (sizeInBytes > 0)) { + mState = STATE_INITIALIZED; + } + + if (mState != STATE_INITIALIZED) { + return ERROR_INVALID_OPERATION; + } + + if ( (audioData == null) || (offsetInBytes < 0 ) || (sizeInBytes < 0) + || (offsetInBytes + sizeInBytes > audioData.length)) { + return ERROR_BAD_VALUE; + } + + return native_write_byte(audioData, offsetInBytes, sizeInBytes, mAudioFormat); + } + + + /** + * Writes the audio data to the audio hardware for playback. + * @param audioData the array that holds the data to play. + * @param offsetInShorts the offset in audioData where the data to play starts. + * @param sizeInShorts the number of bytes to read in audioData after the offset. + * @return the number of shorts that were written or {@link #ERROR_INVALID_OPERATION} + * if the object wasn't properly initialized, or {@link #ERROR_BAD_VALUE} if + * the parameters don't resolve to valid data and indexes. + */ + + public int write(short[] audioData, int offsetInShorts, int sizeInShorts) { + if ((mDataLoadMode == MODE_STATIC) + && (mState == STATE_NO_STATIC_DATA) + && (sizeInShorts > 0)) { + mState = STATE_INITIALIZED; + } + + if (mState != STATE_INITIALIZED) { + return ERROR_INVALID_OPERATION; + } + + if ( (audioData == null) || (offsetInShorts < 0 ) || (sizeInShorts < 0) + || (offsetInShorts + sizeInShorts > audioData.length)) { + return ERROR_BAD_VALUE; + } + + return native_write_short(audioData, offsetInShorts, sizeInShorts, mAudioFormat); + } + + + /** + * Notifies the native resource to reuse the audio data already loaded in the native + * layer. This call is only valid with AudioTrack instances that don't use the streaming + * model. + * @return error code or success, see {@link #SUCCESS}, {@link #ERROR_BAD_VALUE}, + * {@link #ERROR_INVALID_OPERATION} + */ + public int reloadStaticData() { + if (mDataLoadMode == MODE_STREAM) { + return ERROR_INVALID_OPERATION; + } + return native_reload_static(); + } + + + //--------------------------------------------------------- + // Interface definitions + //-------------------- + /** + * Interface definition for a callback to be invoked when an AudioTrack has + * reached a notification marker set by setNotificationMarkerPosition(). + */ + public interface OnMarkerReachedListener { + /** + * Called on the listener to notify it that the previously set marker has been reached + * by the playback head. + */ + void onMarkerReached(AudioTrack track); + } + + + /** + * Interface definition for a callback to be invoked for each periodic AudioTrack + * update during playback. The update interval is set by setPositionNotificationPeriod(). + */ + public interface OnPeriodicNotificationListener { + /** + * Called on the listener to periodically notify it that the playback head has reached + * a multiple of the notification period. + */ + void onPeriodicNotification(AudioTrack track); + } + + + //--------------------------------------------------------- + // Inner classes + //-------------------- + /** + * Helper class to handle the forwarding of native events to the appropriate listeners + */ + private class NativeEventHandler extends Handler + { + private AudioTrack mAudioTrack; + + public NativeEventHandler(AudioTrack mp, Looper looper) { + super(looper); + mAudioTrack = mp; + } + + @Override + public void handleMessage(Message msg) { + if (mAudioTrack == null) { + return; + } + switch(msg.what) { + case NATIVE_EVENT_MARKER: + synchronized (mMarkerListenerLock) { + if (mAudioTrack.mMarkerListener != null) { + mAudioTrack.mMarkerListener.onMarkerReached(mAudioTrack); + } + } + break; + case NATIVE_EVENT_NEW_POS: + synchronized (mPeriodicListenerLock) { + if (mAudioTrack.mPeriodicListener != null) { + mAudioTrack.mPeriodicListener.onPeriodicNotification(mAudioTrack); + } + } + break; + default: + Log.e(TAG, "[ android.media.AudioTrack.NativeEventHandler ] " + + "Unknown event type: " + msg.what); + break; + } + } + } + + + //--------------------------------------------------------- + // Java methods called from the native side + //-------------------- + @SuppressWarnings("unused") + private static void postEventFromNative(Object audiotrack_ref, + int what, int arg1, int arg2, Object obj) { + //logd("Event posted from the native side: event="+ what + " args="+ arg1+" "+arg2); + AudioTrack track = (AudioTrack)((WeakReference)audiotrack_ref).get(); + if (track == null) { + return; + } + + if (track.mNativeEventHandler != null) { + Message m = track.mNativeEventHandler.obtainMessage(what, arg1, arg2, obj); + track.mNativeEventHandler.sendMessage(m); + } + + } + + + //--------------------------------------------------------- + // Native methods called from the Java side + //-------------------- + + private native final int native_setup(Object audiotrack_this, + int streamType, int sampleRate, int nbChannels, int audioFormat, + int buffSizeInBytes, int mode); + + private native final void native_finalize(); + + private native final void native_release(); + + private native final void native_start(); + + private native final void native_stop(); + + private native final void native_pause(); + + private native final void native_flush(); + + private native final int native_write_byte(byte[] audioData, + int offsetInBytes, int sizeInBytes, int format); + + private native final int native_write_short(short[] audioData, + int offsetInShorts, int sizeInShorts, int format); + + private native final int native_reload_static(); + + private native final int native_get_native_frame_count(); + + private native final void native_setVolume(float leftVolume, float rightVolume); + + private native final void native_set_playback_rate(int sampleRateInHz); + private native final int native_get_playback_rate(); + + private native final int native_set_marker_pos(int marker); + private native final int native_get_marker_pos(); + + private native final int native_set_pos_update_period(int updatePeriod); + private native final int native_get_pos_update_period(); + + private native final int native_set_position(int position); + private native final int native_get_position(); + + private native final int native_set_loop(int start, int end, int loopCount); + + static private native final int native_get_output_sample_rate(int streamType); + static private native final int native_get_min_buff_size( + int sampleRateInHz, int channelConfig, int audioFormat); + + + //--------------------------------------------------------- + // Utility methods + //------------------ + + private static void logd(String msg) { + Log.d(TAG, "[ android.media.AudioTrack ] " + msg); + } + + private static void loge(String msg) { + Log.e(TAG, "[ android.media.AudioTrack ] " + msg); + } + +} diff --git a/media/java/android/media/FaceDetector.java b/media/java/android/media/FaceDetector.java new file mode 100644 index 0000000..3b41cf8 --- /dev/null +++ b/media/java/android/media/FaceDetector.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2006 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 android.graphics.Bitmap; +import android.graphics.PointF; +import android.util.Log; + +import java.lang.IllegalArgumentException; + +/** + * Identifies the faces of people in a + * {@link android.graphics.Bitmap} graphic object. + */ +public class FaceDetector { + + /** + * A Face contains all the information identifying the location + * of a face in a bitmap. + */ + public class Face { + /** The minimum confidence factor of good face recognition */ + public static final float CONFIDENCE_THRESHOLD = 0.4f; + /** The x-axis Euler angle of a face. */ + public static final int EULER_X = 0; + /** The y-axis Euler angle of a face. */ + public static final int EULER_Y = 1; + /** The z-axis Euler angle of a face. */ + public static final int EULER_Z = 2; + + /** + * Returns a confidence factor between 0 and 1. This indicates how + * certain what has been found is actually a face. A confidence + * factor above 0.3 is usually good enough. + */ + public float confidence() { + return mConfidence; + } + /** + * Sets the position of the mid-point between the eyes. + * @param point the PointF coordinates (float values) of the + * face's mid-point + */ + public void getMidPoint(PointF point) { + // don't return a PointF to avoid allocations + point.set(mMidPointX, mMidPointY); + } + /** + * Returns the distance between the eyes. + */ + public float eyesDistance() { + return mEyesDist; + } + /** + * Returns the face's pose. That is, the rotations around either + * the X, Y or Z axis (the positions in 3-dimensional Euclidean space). + * + * @param euler the Euler axis to retrieve an angle from + * (<var>EULER_X</var>, <var>EULER_Y</var> or + * <var>EULER_Z</var>) + * @return the Euler angle of the of the face, for the given axis + */ + public float pose(int euler) { + // don't use an array to avoid allocations + if (euler == EULER_X) + return mPoseEulerX; + else if (euler == EULER_Y) + return mPoseEulerY; + else if (euler == EULER_Z) + return mPoseEulerZ; + throw new IllegalArgumentException(); + } + + // private ctor, user not supposed to build this object + private Face() { + } + private float mConfidence; + private float mMidPointX; + private float mMidPointY; + private float mEyesDist; + private float mPoseEulerX; + private float mPoseEulerY; + private float mPoseEulerZ; + } + + + /** + * Creates a FaceDetector, configured with the size of the images to + * be analysed and the maximum number of faces that can be detected. + * These parameters cannot be changed once the object is constructed. + * + * @param width the width of the image + * @param height the height of the image + * @param maxFaces the maximum number of faces to identify + * + */ + public FaceDetector(int width, int height, int maxFaces) + { + if (!sInitialized) { + return; + } + fft_initialize(width, height, maxFaces); + mWidth = width; + mHeight = height; + mMaxFaces = maxFaces; + mBWBuffer = new byte[width * height]; + } + + /** + * Finds all the faces found in a given {@link android.graphics.Bitmap}. + * The supplied array is populated with {@link FaceDetector.Face}s for each + * face found. The bitmap must be in 565 format (for now). + * + * @param bitmap the {@link android.graphics.Bitmap} graphic to be analyzed + * @param faces an array in which to place all found + * {@link FaceDetector.Face}s. The array must be sized equal + * to the <var>maxFaces</var> value set at initialization + * @return the number of faces found + * @throws IllegalArgumentException if the Bitmap dimensions don't match + * the dimensions defined at initialization or the given array + * is not sized equal to the <var>maxFaces</var> value defined + * at initialization + */ + public int findFaces(Bitmap bitmap, Face[] faces) + { + if (!sInitialized) { + return 0; + } + if (bitmap.getWidth() != mWidth || bitmap.getHeight() != mHeight) { + throw new IllegalArgumentException( + "bitmap size doesn't match initialization"); + } + if (faces.length < mMaxFaces) { + throw new IllegalArgumentException( + "faces[] smaller than maxFaces"); + } + + int numFaces = fft_detect(bitmap); + if (numFaces >= mMaxFaces) + numFaces = mMaxFaces; + for (int i=0 ; i<numFaces ; i++) { + if (faces[i] == null) + faces[i] = new Face(); + fft_get_face(faces[i], i); + } + return numFaces; + } + + + /* no user serviceable parts here ... */ + @Override + protected void finalize() throws Throwable { + fft_destroy(); + } + + /* + * We use a class initializer to allow the native code to cache some + * field offsets. + */ + private static boolean sInitialized; + native private static void nativeClassInit(); + + static { + sInitialized = false; + try { + System.loadLibrary("FFTEm"); + nativeClassInit(); + sInitialized = true; + } catch (UnsatisfiedLinkError e) { + Log.d("FFTEm", "face detection library not found!"); + } + } + + native private int fft_initialize(int width, int height, int maxFaces); + native private int fft_detect(Bitmap bitmap); + native private void fft_get_face(Face face, int i); + native private void fft_destroy(); + + private int mFD; + private int mSDK; + private int mDCR; + private int mWidth; + private int mHeight; + private int mMaxFaces; + private byte mBWBuffer[]; +} + diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl new file mode 100644 index 0000000..f5e242d --- /dev/null +++ b/media/java/android/media/IAudioService.aidl @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2007 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; + +/** + * {@hide} + */ +interface IAudioService { + + void adjustVolume(int direction, int flags); + + void adjustSuggestedStreamVolume(int direction, int suggestedStreamType, int flags); + + void adjustStreamVolume(int streamType, int direction, int flags); + + void setStreamVolume(int streamType, int index, int flags); + + void setStreamSolo(int streamType, boolean state, IBinder cb); + + void setStreamMute(int streamType, boolean state, IBinder cb); + + int getStreamVolume(int streamType); + + int getStreamMaxVolume(int streamType); + + void setRingerMode(int ringerMode); + + int getRingerMode(); + + void setVibrateSetting(int vibrateType, int vibrateSetting); + + int getVibrateSetting(int vibrateType); + + boolean shouldVibrate(int vibrateType); + + void setMicrophoneMute(boolean on); + + boolean isMicrophoneMute(); + + void setMode(int mode); + + int getMode(); + + void setRouting(int mode, int routes, int mask); + + int getRouting(int mode); + + boolean isMusicActive(); + + void setParameter(String key, String value); + + oneway void playSoundEffect(int effectType); + + oneway void playSoundEffectVolume(int effectType, float volume); + + boolean loadSoundEffects(); + + oneway void unloadSoundEffects(); + +} diff --git a/media/java/android/media/IMediaScannerListener.aidl b/media/java/android/media/IMediaScannerListener.aidl new file mode 100644 index 0000000..4e85563 --- /dev/null +++ b/media/java/android/media/IMediaScannerListener.aidl @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2008 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 android.net.Uri; + +/** + * {@hide} + */ +oneway interface IMediaScannerListener +{ + /** + * Called when a IMediaScannerService.scanFile() call has completed. + * @param path the path to the file that has been scanned. + * @param uri the Uri for the file if the scanning operation succeeded + * and the file was added to the media database, or null if scanning failed. + */ + void scanCompleted(String path, in Uri uri); +} diff --git a/media/java/android/media/IMediaScannerService.aidl b/media/java/android/media/IMediaScannerService.aidl new file mode 100644 index 0000000..c531646 --- /dev/null +++ b/media/java/android/media/IMediaScannerService.aidl @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2007 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 android.media.IMediaScannerListener; + +/** + * {@hide} + */ +interface IMediaScannerService +{ + /** + * Requests the media scanner to scan a file. + * @param path the path to the file to be scanned. + * @param mimeType an optional mimeType for the file. + * If mimeType is null, then the mimeType will be inferred from the file extension. + * @param listener an optional IMediaScannerListener. + * If specified, the caller will be notified when scanning is complete via the listener. + */ + void requestScanFile(String path, String mimeType, in IMediaScannerListener listener); + + /** + * Older API, left in for backward compatibility. + * Requests the media scanner to scan a file. + * @param path the path to the file to be scanned. + * @param mimeType an optional mimeType for the file. + * If mimeType is null, then the mimeType will be inferred from the file extension. + */ + void scanFile(String path, String mimeType); +} diff --git a/media/java/android/media/JetPlayer.java b/media/java/android/media/JetPlayer.java new file mode 100644 index 0000000..9de0eec --- /dev/null +++ b/media/java/android/media/JetPlayer.java @@ -0,0 +1,423 @@ +/* + * Copyright (C) 2008 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 java.io.FileDescriptor; +import java.lang.ref.WeakReference; +import java.lang.CloneNotSupportedException; + +import android.content.res.AssetFileDescriptor; +import android.os.Looper; +import android.os.Handler; +import android.os.Message; +import android.util.AndroidRuntimeException; +import android.util.Log; + +/** + * JetPlayer provides access to JET content playback and control. + * <p> + * Use <code>JetPlayer.getJetPlayer()</code> to get an instance of this class. + * + */ +public class JetPlayer +{ + //-------------------------------------------- + // Constants + //------------------------ + /** + * The maximum number of simultaneous tracks. Use __link #getMaxTracks()} to + * access this value. + */ + private static int MAXTRACKS = 32; + + // to keep in sync with the JetPlayer class constants + // defined in frameworks/base/include/media/JetPlayer.h + private static final int JET_EVENT = 1; + private static final int JET_USERID_UPDATE = 2; + private static final int JET_NUMQUEUEDSEGMENT_UPDATE = 3; + private static final int JET_PAUSE_UPDATE = 4; + + // to keep in sync with external/sonivox/arm-wt-22k/lib_src/jet_data.h + // Encoding of event information on 32 bits + private static final int JET_EVENT_VAL_MASK = 0x0000007f; // mask for value + private static final int JET_EVENT_CTRL_MASK = 0x00003f80; // mask for controller + private static final int JET_EVENT_CHAN_MASK = 0x0003c000; // mask for channel + private static final int JET_EVENT_TRACK_MASK = 0x00fc0000; // mask for track number + private static final int JET_EVENT_SEG_MASK = 0xff000000; // mask for segment ID + private static final int JET_EVENT_CTRL_SHIFT = 7; // shift to get controller number to bit 0 + private static final int JET_EVENT_CHAN_SHIFT = 14; // shift to get MIDI channel to bit 0 + private static final int JET_EVENT_TRACK_SHIFT = 18; // shift to get track ID to bit 0 + private static final int JET_EVENT_SEG_SHIFT = 24; // shift to get segment ID to bit 0 + + + //-------------------------------------------- + // Member variables + //------------------------ + private EventHandler mNativeEventHandler = null; + + /** + * Lock to protect status listener updates against status change notifications + */ + private final Object mStatusListenerLock = new Object(); + + /** + * Lock to protect the event listener updates against event notifications + */ + private final Object mEventListenerLock = new Object(); + + private JetStatusUpdateListener mJetStatusUpdateListener = null; + + private JetEventListener mJetEventListener = null; + + private static JetPlayer singletonRef; + + + //-------------------------------- + // Used exclusively by native code + //-------------------- + /** + * Accessed by native methods: provides access to C++ JetPlayer object + */ + @SuppressWarnings("unused") + private int mNativePlayerInJavaObj; + + + //-------------------------------------------- + // Constructor, finalize + //------------------------ + public static JetPlayer getJetPlayer() { + if (singletonRef == null) + singletonRef = new JetPlayer(); + return singletonRef; + } + + + public Object clone() throws CloneNotSupportedException { + // JetPlayer is a singleton class, + // so you can't clone a JetPlayer instance + throw new CloneNotSupportedException(); + } + + + private JetPlayer() { + + native_setup(new WeakReference<JetPlayer>(this), + JetPlayer.getMaxTracks(), + 1200); //TODO parametrize this (?) + } + + + protected void finalize() { + native_finalize(); + } + + + public void release() { + native_release(); + } + + + private void createNativeEventHandler() { + Looper looper; + if ((looper = Looper.myLooper()) != null) { + mNativeEventHandler = new EventHandler(this, looper); + } else if ((looper = Looper.getMainLooper()) != null) { + mNativeEventHandler = new EventHandler(this, looper); + } else { + mNativeEventHandler = null; + } + } + + + //-------------------------------------------- + // Getters + //------------------------ + /** + * Returns the maximum number of simultaneous MIDI tracks supported by the Jet player + */ + public static int getMaxTracks() { + return JetPlayer.MAXTRACKS; + } + + + //-------------------------------------------- + // Jet functionality + //------------------------ + public boolean loadJetFile(String path) { + return native_loadJetFromFile(path); + } + + + public boolean loadJetFile(AssetFileDescriptor afd) { + long len = afd.getLength(); + if (len < 0) { + throw new AndroidRuntimeException("no length for fd"); + } + return native_loadJetFromFileD( + afd.getFileDescriptor(), afd.getStartOffset(), len); + } + + + public boolean closeJetFile() { + return native_closeJetFile(); + } + + + public boolean play() { + return native_playJet(); + } + + + public boolean pause() { + return native_pauseJet(); + } + + + public boolean queueJetSegment(int segmentNum, int libNum, int repeatCount, + int transpose, int muteFlags, byte userID) { + return native_queueJetSegment(segmentNum, libNum, repeatCount, + transpose, muteFlags, userID); + } + + + public boolean queueJetSegmentMuteArray(int segmentNum, int libNum, int repeatCount, + int transpose, boolean[] muteArray, byte userID) { + if (muteArray.length != JetPlayer.getMaxTracks()) { + return false; + } + return native_queueJetSegmentMuteArray(segmentNum, libNum, repeatCount, + transpose, muteArray, userID); + } + + + public boolean setMuteFlags(int muteFlags, boolean sync) { + return native_setMuteFlags(muteFlags, sync); + } + + + public boolean setMuteArray(boolean[] muteArray, boolean sync) { + if(muteArray.length != JetPlayer.getMaxTracks()) + return false; + return native_setMuteArray(muteArray, sync); + } + + + public boolean setMuteFlag(int trackId, boolean muteFlag, boolean sync) { + return native_setMuteFlag(trackId, muteFlag, sync); + } + + + public boolean triggerClip(int clipId) { + return native_triggerClip(clipId); + } + + + public boolean clearQueue() { + return native_clearQueue(); + } + + + //--------------------------------------------------------- + // Internal class to handle events posted from native code + //------------------------ + private class EventHandler extends Handler + { + private JetPlayer mJet; + + public EventHandler(JetPlayer jet, Looper looper) { + super(looper); + mJet = jet; + } + + @Override + public void handleMessage(Message msg) { + switch(msg.what) { + case JET_EVENT: + synchronized (mEventListenerLock) { + if (mJetEventListener != null) { + // call the appropriate listener after decoding the event parameters + // encoded in msg.arg1 + mJetEventListener.onJetEvent( + mJet, + (short)((msg.arg1 & JET_EVENT_SEG_MASK) >> JET_EVENT_SEG_SHIFT), + (byte) ((msg.arg1 & JET_EVENT_TRACK_MASK) >> JET_EVENT_TRACK_SHIFT), + // JETCreator channel numbers start at 1, but the index starts at 0 + // in the .jet files + (byte)(((msg.arg1 & JET_EVENT_CHAN_MASK) >> JET_EVENT_CHAN_SHIFT) + 1), + (byte) ((msg.arg1 & JET_EVENT_CTRL_MASK) >> JET_EVENT_CTRL_SHIFT), + (byte) (msg.arg1 & JET_EVENT_VAL_MASK) ); + } + } + return; + case JET_USERID_UPDATE: + synchronized (mStatusListenerLock) { + if (mJetStatusUpdateListener != null) { + mJetStatusUpdateListener.onJetUserIdUpdate(mJet, msg.arg1, msg.arg2); + } + } + return; + case JET_NUMQUEUEDSEGMENT_UPDATE: + synchronized (mStatusListenerLock) { + if (mJetStatusUpdateListener != null) { + mJetStatusUpdateListener.onJetNumQueuedSegmentUpdate(mJet, msg.arg1); + } + } + return; + case JET_PAUSE_UPDATE: + synchronized (mStatusListenerLock) { + if (mJetStatusUpdateListener != null) + mJetStatusUpdateListener.onJetPauseUpdate(mJet, msg.arg1); + } + return; + + default: + loge("Unknown message type " + msg.what); + return; + } + } + } + + + //-------------------------------------------- + // Jet status update listener + //------------------------ + public void setStatusUpdateListener(JetStatusUpdateListener listener) { + synchronized(mStatusListenerLock) { + mJetStatusUpdateListener = listener; + } + + if ((listener != null) && (mNativeEventHandler == null)) { + createNativeEventHandler(); + } + } + + /** + * Handles the notification when the JET status is updated. + */ + public interface JetStatusUpdateListener { + /** + * Callback for when JET's currently playing segment userID is updated. + * + * @param player the JET player the status update is coming from + * @param userId the ID of the currently playing segment + * @param repeatCount the repetition count for the segment (0 means it plays once) + */ + void onJetUserIdUpdate(JetPlayer player, int userId, int repeatCount); + + /** + * Callback for when JET's number of queued segments is updated. + * + * @param player the JET player the status update is coming from + * @param nbSegments the number of segments in the JET queue + */ + void onJetNumQueuedSegmentUpdate(JetPlayer player, int nbSegments); + + /** + * Callback for when JET pause state is updated. + * + * @param player the JET player the status update is coming from + * @param paused indicates whether JET is paused or not + */ + void onJetPauseUpdate(JetPlayer player, int paused); + } + + + //-------------------------------------------- + // Jet event listener + //------------------------ + public void setEventListener(JetEventListener listener) { + synchronized(mEventListenerLock) { + mJetEventListener = listener; + } + + if ((listener != null) && (mNativeEventHandler == null)) { + createNativeEventHandler(); + } + } + + /** + * Handles the notification when the JET engine generates an event. + */ + public interface JetEventListener { + /** + * Callback for when the JET engine generates a new event. + * + * @param player the JET player the event is coming from + * @param segment 8 bit unsigned value + * @param track 6 bit unsigned value + * @param channel 4 bit unsigned value + * @param controller 7 bit unsigned value + * @param value 7 bit unsigned value + */ + void onJetEvent(JetPlayer player, + short segment, byte track, byte channel, byte controller, byte value); + } + + + //-------------------------------------------- + // Native methods + //------------------------ + private native final boolean native_setup(Object Jet_this, + int maxTracks, int trackBufferSize); + private native final void native_finalize(); + private native final void native_release(); + private native final boolean native_loadJetFromFile(String pathToJetFile); + private native final boolean native_loadJetFromFileD(FileDescriptor fd, long offset, long len); + private native final boolean native_closeJetFile(); + private native final boolean native_playJet(); + private native final boolean native_pauseJet(); + private native final boolean native_queueJetSegment(int segmentNum, int libNum, + int repeatCount, int transpose, int muteFlags, byte userID); + private native final boolean native_queueJetSegmentMuteArray(int segmentNum, int libNum, + int repeatCount, int transpose, boolean[] muteArray, byte userID); + private native final boolean native_setMuteFlags(int muteFlags, boolean sync); + private native final boolean native_setMuteArray(boolean[]muteArray, boolean sync); + private native final boolean native_setMuteFlag(int trackId, boolean muteFlag, boolean sync); + private native final boolean native_triggerClip(int clipId); + private native final boolean native_clearQueue(); + + //--------------------------------------------------------- + // Called exclusively by native code + //-------------------- + @SuppressWarnings("unused") + private static void postEventFromNative(Object jetplayer_ref, + int what, int arg1, int arg2) { + + JetPlayer jet = (JetPlayer)((WeakReference)jetplayer_ref).get(); + + if( (jet!=null) && (jet.mNativeEventHandler!=null) ){ + Message m = jet.mNativeEventHandler.obtainMessage(what, arg1, arg2, null); + jet.mNativeEventHandler.sendMessage(m); + } + } + + + //--------------------------------------------------------- + // Utils + //-------------------- + private final static String TAG = "JetPlayer-J"; + + private static void logd(String msg) { + Log.d(TAG, "[ android.media.JetPlayer ] " + msg); + } + + private static void loge(String msg) { + Log.e(TAG, "[ android.media.JetPlayer ] " + msg); + } + +} diff --git a/media/java/android/media/MediaFile.java b/media/java/android/media/MediaFile.java new file mode 100644 index 0000000..f05842d --- /dev/null +++ b/media/java/android/media/MediaFile.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2007 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 android.content.ContentValues; +import android.provider.MediaStore.Audio; +import android.provider.MediaStore.Images; +import android.provider.MediaStore.Video; + +import java.util.HashMap; +import java.util.Iterator; + +/** + * MediaScanner helper class. + * + * {@hide} + */ +public class MediaFile { + // comma separated list of all file extensions supported by the media scanner + public static String sFileExtensions; + + // Audio file types + public static final int FILE_TYPE_MP3 = 1; + public static final int FILE_TYPE_M4A = 2; + public static final int FILE_TYPE_WAV = 3; + public static final int FILE_TYPE_AMR = 4; + public static final int FILE_TYPE_AWB = 5; + public static final int FILE_TYPE_WMA = 6; + public static final int FILE_TYPE_OGG = 7; + private static final int FIRST_AUDIO_FILE_TYPE = FILE_TYPE_MP3; + private static final int LAST_AUDIO_FILE_TYPE = FILE_TYPE_OGG; + + // MIDI file types + public static final int FILE_TYPE_MID = 11; + public static final int FILE_TYPE_SMF = 12; + public static final int FILE_TYPE_IMY = 13; + private static final int FIRST_MIDI_FILE_TYPE = FILE_TYPE_MID; + private static final int LAST_MIDI_FILE_TYPE = FILE_TYPE_IMY; + + // Video file types + public static final int FILE_TYPE_MP4 = 21; + public static final int FILE_TYPE_M4V = 22; + public static final int FILE_TYPE_3GPP = 23; + public static final int FILE_TYPE_3GPP2 = 24; + public static final int FILE_TYPE_WMV = 25; + private static final int FIRST_VIDEO_FILE_TYPE = FILE_TYPE_MP4; + private static final int LAST_VIDEO_FILE_TYPE = FILE_TYPE_WMV; + + // Image file types + public static final int FILE_TYPE_JPEG = 31; + public static final int FILE_TYPE_GIF = 32; + public static final int FILE_TYPE_PNG = 33; + public static final int FILE_TYPE_BMP = 34; + public static final int FILE_TYPE_WBMP = 35; + private static final int FIRST_IMAGE_FILE_TYPE = FILE_TYPE_JPEG; + private static final int LAST_IMAGE_FILE_TYPE = FILE_TYPE_WBMP; + + // Playlist file types + public static final int FILE_TYPE_M3U = 41; + public static final int FILE_TYPE_PLS = 42; + public static final int FILE_TYPE_WPL = 43; + private static final int FIRST_PLAYLIST_FILE_TYPE = FILE_TYPE_M3U; + private static final int LAST_PLAYLIST_FILE_TYPE = FILE_TYPE_WPL; + + static class MediaFileType { + + int fileType; + String mimeType; + + MediaFileType(int fileType, String mimeType) { + this.fileType = fileType; + this.mimeType = mimeType; + } + } + + private static HashMap<String, MediaFileType> sFileTypeMap + = new HashMap<String, MediaFileType>(); + private static HashMap<String, Integer> sMimeTypeMap + = new HashMap<String, Integer>(); + static void addFileType(String extension, int fileType, String mimeType) { + sFileTypeMap.put(extension, new MediaFileType(fileType, mimeType)); + sMimeTypeMap.put(mimeType, new Integer(fileType)); + } + static { + addFileType("MP3", FILE_TYPE_MP3, "audio/mpeg"); + addFileType("M4A", FILE_TYPE_M4A, "audio/mp4"); + addFileType("WAV", FILE_TYPE_WAV, "audio/x-wav"); + addFileType("AMR", FILE_TYPE_AMR, "audio/amr"); + addFileType("AWB", FILE_TYPE_AWB, "audio/amr-wb"); + addFileType("WMA", FILE_TYPE_WMA, "audio/x-ms-wma"); + addFileType("OGG", FILE_TYPE_OGG, "application/ogg"); + addFileType("OGA", FILE_TYPE_OGG, "application/ogg"); + + addFileType("MID", FILE_TYPE_MID, "audio/midi"); + addFileType("MIDI", FILE_TYPE_MID, "audio/midi"); + addFileType("XMF", FILE_TYPE_MID, "audio/midi"); + addFileType("RTTTL", FILE_TYPE_MID, "audio/midi"); + addFileType("SMF", FILE_TYPE_SMF, "audio/sp-midi"); + addFileType("IMY", FILE_TYPE_IMY, "audio/imelody"); + addFileType("RTX", FILE_TYPE_MID, "audio/midi"); + addFileType("OTA", FILE_TYPE_MID, "audio/midi"); + + addFileType("MP4", FILE_TYPE_MP4, "video/mp4"); + addFileType("M4V", FILE_TYPE_M4V, "video/mp4"); + addFileType("3GP", FILE_TYPE_3GPP, "video/3gpp"); + addFileType("3GPP", FILE_TYPE_3GPP, "video/3gpp"); + addFileType("3G2", FILE_TYPE_3GPP2, "video/3gpp2"); + addFileType("3GPP2", FILE_TYPE_3GPP2, "video/3gpp2"); + addFileType("WMV", FILE_TYPE_WMV, "video/x-ms-wmv"); + + addFileType("JPG", FILE_TYPE_JPEG, "image/jpeg"); + addFileType("JPEG", FILE_TYPE_JPEG, "image/jpeg"); + addFileType("GIF", FILE_TYPE_GIF, "image/gif"); + addFileType("PNG", FILE_TYPE_PNG, "image/png"); + addFileType("BMP", FILE_TYPE_BMP, "image/x-ms-bmp"); + addFileType("WBMP", FILE_TYPE_WBMP, "image/vnd.wap.wbmp"); + + addFileType("M3U", FILE_TYPE_M3U, "audio/x-mpegurl"); + addFileType("PLS", FILE_TYPE_PLS, "audio/x-scpls"); + addFileType("WPL", FILE_TYPE_WPL, "application/vnd.ms-wpl"); + + // compute file extensions list for native Media Scanner + StringBuilder builder = new StringBuilder(); + Iterator<String> iterator = sFileTypeMap.keySet().iterator(); + + while (iterator.hasNext()) { + if (builder.length() > 0) { + builder.append(','); + } + builder.append(iterator.next()); + } + sFileExtensions = builder.toString(); + } + + public static final String UNKNOWN_STRING = "<unknown>"; + + public static boolean isAudioFileType(int fileType) { + return ((fileType >= FIRST_AUDIO_FILE_TYPE && + fileType <= LAST_AUDIO_FILE_TYPE) || + (fileType >= FIRST_MIDI_FILE_TYPE && + fileType <= LAST_MIDI_FILE_TYPE)); + } + + public static boolean isVideoFileType(int fileType) { + return (fileType >= FIRST_VIDEO_FILE_TYPE && + fileType <= LAST_VIDEO_FILE_TYPE); + } + + public static boolean isImageFileType(int fileType) { + return (fileType >= FIRST_IMAGE_FILE_TYPE && + fileType <= LAST_IMAGE_FILE_TYPE); + } + + public static boolean isPlayListFileType(int fileType) { + return (fileType >= FIRST_PLAYLIST_FILE_TYPE && + fileType <= LAST_PLAYLIST_FILE_TYPE); + } + + public static MediaFileType getFileType(String path) { + int lastDot = path.lastIndexOf("."); + if (lastDot < 0) + return null; + return sFileTypeMap.get(path.substring(lastDot + 1).toUpperCase()); + } + + public static int getFileTypeForMimeType(String mimeType) { + Integer value = sMimeTypeMap.get(mimeType); + return (value == null ? 0 : value.intValue()); + } + +} diff --git a/media/java/android/media/MediaMetadataRetriever.java b/media/java/android/media/MediaMetadataRetriever.java new file mode 100644 index 0000000..3a49a5f --- /dev/null +++ b/media/java/android/media/MediaMetadataRetriever.java @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2008 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 android.content.ContentResolver; +import android.content.Context; +import android.content.res.AssetFileDescriptor; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.FileNotFoundException; + +/** + * MediaMetadataRetriever class provides a unified interface for retrieving + * frame and meta data from an input media file. + * {@hide} + */ +public class MediaMetadataRetriever +{ + static { + System.loadLibrary("media_jni"); + } + + // The field below is accessed by native methods + @SuppressWarnings("unused") + private int mNativeContext; + + public MediaMetadataRetriever() { + native_setup(); + } + + /** + * Call this method before setDataSource() so that the mode becomes + * effective for subsequent operations. This method can be called only once + * at the beginning if the intended mode of operation for a + * MediaMetadataRetriever object remains the same for its whole lifetime, + * and thus it is unnecessary to call this method each time setDataSource() + * is called. If this is not never called (which is allowed), by default the + * intended mode of operation is to both capture frame and retrieve meta + * data (i.e., MODE_GET_METADATA_ONLY | MODE_CAPTURE_FRAME_ONLY). + * Often, this may not be what one wants, since doing this has negative + * performance impact on execution time of a call to setDataSource(), since + * both types of operations may be time consuming. + * + * @param mode The intended mode of operation. Can be any combination of + * MODE_GET_METADATA_ONLY and MODE_CAPTURE_FRAME_ONLY: + * 1. MODE_GET_METADATA_ONLY & MODE_CAPTURE_FRAME_ONLY: + * For neither frame capture nor meta data retrieval + * 2. MODE_GET_METADATA_ONLY: For meta data retrieval only + * 3. MODE_CAPTURE_FRAME_ONLY: For frame capture only + * 4. MODE_GET_METADATA_ONLY | MODE_CAPTURE_FRAME_ONLY: + * For both frame capture and meta data retrieval + */ + public native void setMode(int mode); + + /** + * @return the current mode of operation. A negative return value indicates + * some runtime error has occurred. + */ + public native int getMode(); + + /** + * Sets the data source (file pathname) to use. Call this + * method before the rest of the methods in this class. This method may be + * time-consuming. + * + * @param path The path of the input media file. + * @throws IllegalArgumentException If the path is invalid. + */ + public native void setDataSource(String path) throws IllegalArgumentException; + + /** + * Sets the data source (FileDescriptor) to use. It is the caller's + * responsibility to close the file descriptor. It is safe to do so as soon + * as this call returns. Call this method before the rest of the methods in + * this class. This method may be time-consuming. + * + * @param fd the FileDescriptor for the file you want to play + * @param offset the offset into the file where the data to be played starts, + * in bytes. It must be non-negative + * @param length the length in bytes of the data to be played. It must be + * non-negative. + * @throws IllegalArgumentException if the arguments are invalid + */ + public native void setDataSource(FileDescriptor fd, long offset, long length) + throws IllegalArgumentException; + + /** + * Sets the data source (FileDescriptor) to use. It is the caller's + * responsibility to close the file descriptor. It is safe to do so as soon + * as this call returns. Call this method before the rest of the methods in + * this class. This method may be time-consuming. + * + * @param fd the FileDescriptor for the file you want to play + * @throws IllegalArgumentException if the FileDescriptor is invalid + */ + public void setDataSource(FileDescriptor fd) + throws IllegalArgumentException { + // intentionally less than LONG_MAX + setDataSource(fd, 0, 0x7ffffffffffffffL); + } + + /** + * Sets the data source as a content Uri. Call this method before + * the rest of the methods in this class. This method may be time-consuming. + * + * @param context the Context to use when resolving the Uri + * @param uri the Content URI of the data you want to play + * @throws IllegalArgumentException if the Uri is invalid + * @throws SecurityException if the Uri cannot be used due to lack of + * permission. + */ + public void setDataSource(Context context, Uri uri) + throws IllegalArgumentException, SecurityException { + if (uri == null) { + throw new IllegalArgumentException(); + } + + String scheme = uri.getScheme(); + if(scheme == null || scheme.equals("file")) { + setDataSource(uri.getPath()); + return; + } + + AssetFileDescriptor fd = null; + try { + ContentResolver resolver = context.getContentResolver(); + try { + fd = resolver.openAssetFileDescriptor(uri, "r"); + } catch(FileNotFoundException e) { + throw new IllegalArgumentException(); + } + if (fd == null) { + throw new IllegalArgumentException(); + } + FileDescriptor descriptor = fd.getFileDescriptor(); + if (!descriptor.valid()) { + throw new IllegalArgumentException(); + } + // Note: using getDeclaredLength so that our behavior is the same + // as previous versions when the content provider is returning + // a full file. + if (fd.getDeclaredLength() < 0) { + setDataSource(descriptor); + } else { + setDataSource(descriptor, fd.getStartOffset(), fd.getDeclaredLength()); + } + return; + } catch (SecurityException ex) { + } finally { + try { + if (fd != null) { + fd.close(); + } + } catch(IOException ioEx) { + } + } + setDataSource(uri.toString()); + } + + /** + * Call this method after setDataSource(). This method retrieves the + * meta data value associated with the keyCode. + * + * The keyCode currently supported is listed below as METADATA_XXX + * constants. With any other value, it returns a null pointer. + * + * @param keyCode One of the constants listed below at the end of the class. + * @return The meta data value associate with the given keyCode on success; + * null on failure. + */ + public native String extractMetadata(int keyCode); + + /** + * Call this method after setDataSource(). This method finds a + * representative frame if successful and returns it as a bitmap. This is + * useful for generating a thumbnail for an input media source. + * + * @return A Bitmap containing a representative video frame, which + * can be null, if such a frame cannot be retrieved. + */ + public native Bitmap captureFrame(); + + /** + * Call this method after setDataSource(). This method finds the optional + * graphic or album art associated (embedded or external url linked) the + * related data source. + * + * @return null if no such graphic is found. + */ + public native byte[] extractAlbumArt(); + + /** + * Call it when one is done with the object. This method releases the memory + * allocated internally. + */ + public native void release(); + private native void native_setup(); + + private native final void native_finalize(); + + @Override + protected void finalize() throws Throwable { + try { + native_finalize(); + } finally { + super.finalize(); + } + } + + public static final int MODE_GET_METADATA_ONLY = 0x01; + public static final int MODE_CAPTURE_FRAME_ONLY = 0x02; + + /* + * Do not change these values without updating their counterparts + * in include/media/mediametadataretriever.h! + */ + public static final int METADATA_KEY_CD_TRACK_NUMBER = 0; + public static final int METADATA_KEY_ALBUM = 1; + public static final int METADATA_KEY_ARTIST = 2; + public static final int METADATA_KEY_AUTHOR = 3; + public static final int METADATA_KEY_COMPOSER = 4; + public static final int METADATA_KEY_DATE = 5; + public static final int METADATA_KEY_GENRE = 6; + public static final int METADATA_KEY_TITLE = 7; + public static final int METADATA_KEY_YEAR = 8; + public static final int METADATA_KEY_DURATION = 9; + public static final int METADATA_KEY_NUM_TRACKS = 10; + public static final int METADATA_KEY_IS_DRM_CRIPPLED = 11; + public static final int METADATA_KEY_CODEC = 12; + public static final int METADATA_KEY_RATING = 13; + public static final int METADATA_KEY_COMMENT = 14; + public static final int METADATA_KEY_COPYRIGHT = 15; + public static final int METADATA_KEY_BIT_RATE = 16; + public static final int METADATA_KEY_FRAME_RATE = 17; + public static final int METADATA_KEY_VIDEO_FORMAT = 18; + public static final int METADATA_KEY_VIDEO_HEIGHT = 19; + public static final int METADATA_KEY_VIDEO_WIDTH = 20; + // Add more here... +} diff --git a/media/java/android/media/MediaPlayer.java b/media/java/android/media/MediaPlayer.java new file mode 100644 index 0000000..369af3b --- /dev/null +++ b/media/java/android/media/MediaPlayer.java @@ -0,0 +1,1211 @@ +/* + * Copyright (C) 2006 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 android.content.ContentResolver; +import android.content.Context; +import android.content.res.AssetFileDescriptor; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.ParcelFileDescriptor; +import android.os.PowerManager; +import android.util.Log; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.graphics.Bitmap; +import android.media.AudioManager; + +import java.io.FileDescriptor; +import java.io.IOException; + +import java.lang.ref.WeakReference; + +/** + * MediaPlayer class can be used to control playback + * of audio/video files and streams. An example on how to use the methods in + * this class can be found in {@link android.widget.VideoView}. + * Please see <a href="{@docRoot}guide/topics/media/index.html">Audio and Video</a> + * for additional help using MediaPlayer. + * + * <p>Topics covered here are: + * <ol> + * <li><a href="#StateDiagram">State Diagram</a> + * <li><a href="#Valid_and_Invalid_States">Valid and Invalid States</a> + * <li><a href="#Permissions">Permissions</a> + * </ol> + * + * <a name="StateDiagram"></a> + * <h3>State Diagram</h3> + * + * <p>Playback control of audio/video files and streams is managed as a state + * machine. The following diagram shows the life cycle and the states of a + * MediaPlayer object driven by the supported playback control operations. + * The ovals represent the states a MediaPlayer object may reside + * in. The arcs represent the playback control operations that drive the object + * state transition. There are two types of arcs. The arcs with a single arrow + * head represent synchronous method calls, while those with + * a double arrow head represent asynchronous method calls.</p> + * + * <p><img src="../../../images/mediaplayer_state_diagram.gif" + * alt="MediaPlayer State diagram" + * border="0" /></p> + * + * <p>From this state diagram, one can see that a MediaPlayer object has the + * following states:</p> + * <ul> + * <li>When a MediaPlayer object is just created using <code>new</code> or + * after {@link #reset()} is called, it is in the <em>Idle</em> state; and after + * {@link #release()} is called, it is in the <em>End</em> state. Between these + * two states is the life cycle of the MediaPlayer object. + * <ul> + * <li>There is a subtle but important difference between a newly constructed + * MediaPlayer object and the MediaPlayer object after {@link #reset()} + * is called. It is a programming error to invoke methods such + * as {@link #getCurrentPosition()}, + * {@link #getDuration()}, {@link #getVideoHeight()}, + * {@link #getVideoWidth()}, {@link #setAudioStreamType(int)}, + * {@link #setLooping(boolean)}, + * {@link #setVolume(float, float)}, {@link #pause()}, {@link #start()}, + * {@link #stop()}, {@link #seekTo(int)}, {@link #prepare()} or + * {@link #prepareAsync()} in the <em>Idle</em> state for both cases. If any of these + * methods is called right after a MediaPlayer object is constructed, + * the user supplied callback method OnErrorListener.onError() won't be + * called by the internal player engine and the object state remains + * unchanged; but if these methods are called right after {@link #reset()}, + * the user supplied callback method OnErrorListener.onError() will be + * invoked by the internal player engine and the object will be + * transfered to the <em>Error</em> state. </li> + * <li>It is also recommended that once + * a MediaPlayer object is no longer being used, call {@link #release()} immediately + * so that resources used by the internal player engine associated with the + * MediaPlayer object can be released immediately. Resource may include + * singleton resources such as hardware acceleration components and + * failure to call {@link #release()} may cause subsequent instances of + * MediaPlayer objects to fallback to software implementations or fail + * altogether. Once the MediaPlayer + * object is in the <em>End</em> state, it can no longer be used and + * there is no way to bring it back to any other state. </li> + * <li>Furthermore, + * the MediaPlayer objects created using <code>new</code> is in the + * <em>Idle</em> state, while those created with one + * of the overloaded convenient <code>create</code> methods are <em>NOT</em> + * in the <em>Idle</em> state. In fact, the objects are in the <em>Prepared</em> + * state if the creation using <code>create</code> method is successful. + * </li> + * </ul> + * </li> + * <li>In general, some playback control operation may fail due to various + * reasons, such as unsupported audio/video format, poorly interleaved + * audio/video, resolution too high, streaming timeout, and the like. + * Thus, error reporting and recovery is an important concern under + * these circumstances. Sometimes, due to programming errors, invoking a playback + * control operation in an invalid state may also occur. Under all these + * error conditions, the internal player engine invokes a user supplied + * OnErrorListener.onError() method if an OnErrorListener has been + * registered beforehand via + * {@link #setOnErrorListener(android.media.MediaPlayer.OnErrorListener)}. + * <ul> + * <li>It is important to note that once an error occurs, the + * MediaPlayer object enters the <em>Error</em> state (except as noted + * above), even if an error listener has not been registered by the application.</li> + * <li>In order to reuse a MediaPlayer object that is in the <em> + * Error</em> state and recover from the error, + * {@link #reset()} can be called to restore the object to its <em>Idle</em> + * state.</li> + * <li>It is good programming practice to have your application + * register a OnErrorListener to look out for error notifications from + * the internal player engine.</li> + * <li>IlleglStateException is + * thrown to prevent programming errors such as calling {@link #prepare()}, + * {@link #prepareAsync()}, or one of the overloaded <code>setDataSource + * </code> methods in an invalid state. </li> + * </ul> + * </li> + * <li>Calling + * {@link #setDataSource(FileDescriptor)}, or + * {@link #setDataSource(String)}, or + * {@link #setDataSource(Context, Uri)}, or + * {@link #setDataSource(FileDescriptor, long, long)} transfers a + * MediaPlayer object in the <em>Idle</em> state to the + * <em>Initialized</em> state. + * <ul> + * <li>An IllegalStateException is thrown if + * setDataSource() is called in any other state.</li> + * <li>It is good programming + * practice to always look out for <code>IllegalArgumentException</code> + * and <code>IOException</code> that may be thrown from the overloaded + * <code>setDataSource</code> methods.</li> + * </ul> + * </li> + * <li>A MediaPlayer object must first enter the <em>Prepared</em> state + * before playback can be started. + * <ul> + * <li>There are two ways (synchronous vs. + * asynchronous) that the <em>Prepared</em> state can be reached: + * either a call to {@link #prepare()} (synchronous) which + * transfers the object to the <em>Prepared</em> state once the method call + * returns, or a call to {@link #prepareAsync()} (asynchronous) which + * first transfers the object to the <em>Preparing</em> state after the + * call returns (which occurs almost right way) while the internal + * player engine continues working on the rest of preparation work + * until the preparation work completes. When the preparation completes or when {@link #prepare()} call returns, + * the internal player engine then calls a user supplied callback method, + * onPrepared() of the OnPreparedListener interface, if an + * OnPreparedListener is registered beforehand via {@link + * #setOnPreparedListener(android.media.MediaPlayer.OnPreparedListener)}.</li> + * <li>It is important to note that + * the <em>Preparing</em> state is a transient state, and the behavior + * of calling any method with side effect while a MediaPlayer object is + * in the <em>Preparing</em> state is undefined.</li> + * <li>An IllegalStateException is + * thrown if {@link #prepare()} or {@link #prepareAsync()} is called in + * any other state.</li> + * <li>While in the <em>Prepared</em> state, properties + * such as audio/sound volume, screenOnWhilePlaying, looping can be + * adjusted by invoking the corresponding set methods.</li> + * </ul> + * </li> + * <li>To start the playback, {@link #start()} must be called. After + * {@link #start()} returns successfully, the MediaPlayer object is in the + * <em>Started</em> state. {@link #isPlaying()} can be called to test + * whether the MediaPlayer object is in the <em>Started</em> state. + * <ul> + * <li>While in the <em>Started</em> state, the internal player engine calls + * a user supplied OnBufferingUpdateListener.onBufferingUpdate() callback + * method if a OnBufferingUpdateListener has been registered beforehand + * via {@link #setOnBufferingUpdateListener(OnBufferingUpdateListener)}. + * This callback allows applications to keep track of the buffering status + * while streaming audio/video.</li> + * <li>Calling {@link #start()} has not effect + * on a MediaPlayer object that is already in the <em>Started</em> state.</li> + * </ul> + * </li> + * <li>Playback can be paused and stopped, and the current playback position + * can be adjusted. Playback can be paused via {@link #pause()}. When the call to + * {@link #pause()} returns, the MediaPlayer object enters the + * <em>Paused</em> state. Note that the transition from the <em>Started</em> + * state to the <em>Paused</em> state and vice versa happens + * asynchronously in the player engine. It may take some time before + * the state is updated in calls to {@link #isPlaying()}, and it can be + * a number of seconds in the case of streamed content. + * <ul> + * <li>Calling {@link #start()} to resume playback for a paused + * MediaPlayer object, and the resumed playback + * position is the same as where it was paused. When the call to + * {@link #start()} returns, the paused MediaPlayer object goes back to + * the <em>Started</em> state.</li> + * <li>Calling {@link #pause()} has no effect on + * a MediaPlayer object that is already in the <em>Paused</em> state.</li> + * </ul> + * </li> + * <li>Calling {@link #stop()} stops playback and causes a + * MediaPlayer in the <em>Started</em>, <em>Paused</em>, <em>Prepared + * </em> or <em>PlaybackCompleted</em> state to enter the + * <em>Stopped</em> state. + * <ul> + * <li>Once in the <em>Stopped</em> state, playback cannot be started + * until {@link #prepare()} or {@link #prepareAsync()} are called to set + * the MediaPlayer object to the <em>Prepared</em> state again.</li> + * <li>Calling {@link #stop()} has no effect on a MediaPlayer + * object that is already in the <em>Stopped</em> state.</li> + * </ul> + * </li> + * <li>The playback position can be adjusted with a call to + * {@link #seekTo(int)}. + * <ul> + * <li>Although the asynchronuous {@link #seekTo(int)} + * call returns right way, the actual seek operation may take a while to + * finish, especially for audio/video being streamed. When the actual + * seek operation completes, the internal player engine calls a user + * supplied OnSeekComplete.onSeekComplete() if an OnSeekCompleteListener + * has been registered beforehand via + * {@link #setOnSeekCompleteListener(OnSeekCompleteListener)}.</li> + * <li>Please + * note that {@link #seekTo(int)} can also be called in the other states, + * such as <em>Prepared</em>, <em>Paused</em> and <em>PlaybackCompleted + * </em> state.</li> + * <li>Furthermore, the actual current playback position + * can be retrieved with a call to {@link #getCurrentPosition()}, which + * is helpful for applications such as a Music player that need to keep + * track of the playback progress.</li> + * </ul> + * </li> + * <li>When the playback reaches the end of stream, the playback completes. + * <ul> + * <li>If the looping mode was being set to <var>true</var>with + * {@link #setLooping(boolean)}, the MediaPlayer object shall remain in + * the <em>Started</em> state.</li> + * <li>If the looping mode was set to <var>false + * </var>, the player engine calls a user supplied callback method, + * OnCompletion.onCompletion(), if a OnCompletionListener is registered + * beforehand via {@link #setOnCompletionListener(OnCompletionListener)}. + * The invoke of the callback signals that the object is now in the <em> + * PlaybackCompleted</em> state.</li> + * <li>While in the <em>PlaybackCompleted</em> + * state, calling {@link #start()} can restart the playback from the + * beginning of the audio/video source.</li> + * </ul> + * + * + * <a name="Valid_and_Invalid_States"></a> + * <h3>Valid and invalid states</h3> + * + * <table border="0" cellspacing="0" cellpadding="0"> + * <tr><td>Method Name </p></td> + * <td>Valid Sates </p></td> + * <td>Invalid States </p></td> + * <td>Comments </p></td></tr> + * <tr><td>getCurrentPosition </p></td> + * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped, + * PlaybackCompleted} </p></td> + * <td>{Error}</p></td> + * <td>Successful invoke of this method in a valid state does not change the + * state. Calling this method in an invalid state transfers the object + * to the <em>Error</em> state. </p></td></tr> + * <tr><td>getDuration </p></td> + * <td>{Prepared, Started, Paused, Stopped, PlaybackCompleted} </p></td> + * <td>{Idle, Initialized, Error} </p></td> + * <td>Successful invoke of this method in a valid state does not change the + * state. Calling this method in an invalid state transfers the object + * to the <em>Error</em> state. </p></td></tr> + * <tr><td>getVideoHeight </p></td> + * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped, + * PlaybackCompleted}</p></td> + * <td>{Error}</p></td> + * <td>Successful invoke of this method in a valid state does not change the + * state. Calling this method in an invalid state transfers the object + * to the <em>Error</em> state. </p></td></tr> + * <tr><td>getVideoWidth </p></td> + * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped, + * PlaybackCompleted}</p></td> + * <td>{Error}</p></td> + * <td>Successful invoke of this method in a valid state does not change + * the state. Calling this method in an invalid state transfers the + * object to the <em>Error</em> state. </p></td></tr> + * <tr><td>isPlaying </p></td> + * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped, + * PlaybackCompleted}</p></td> + * <td>{Error}</p></td> + * <td>Successful invoke of this method in a valid state does not change + * the state. Calling this method in an invalid state transfers the + * object to the <em>Error</em> state. </p></td></tr> + * <tr><td>pause </p></td> + * <td>{Started, Paused}</p></td> + * <td>{Idle, Initialized, Prepared, Stopped, PlaybackCompleted, Error}</p></td> + * <td>Successful invoke of this method in a valid state transfers the + * object to the <em>Paused</em> state. Calling this method in an + * invalid state transfers the object to the <em>Error</em> state.</p></td></tr> + * <tr><td>prepare </p></td> + * <td>{Initialized, Stopped} </p></td> + * <td>{Idle, Prepared, Started, Paused, PlaybackCompleted, Error} </p></td> + * <td>Successful invoke of this method in a valid state transfers the + * object to the <em>Prepared</em> state. Calling this method in an + * invalid state throws an IllegalStateException.</p></td></tr> + * <tr><td>prepareAsync </p></td> + * <td>{Initialized, Stopped} </p></td> + * <td>{Idle, Prepared, Started, Paused, PlaybackCompleted, Error} </p></td> + * <td>Successful invoke of this method in a valid state transfers the + * object to the <em>Preparing</em> state. Calling this method in an + * invalid state throws an IllegalStateException.</p></td></tr> + * <tr><td>release </p></td> + * <td>any </p></td> + * <td>{} </p></td> + * <td>After {@link #release()}, the object is no longer available. </p></td></tr> + * <tr><td>reset </p></td> + * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped, + * PlaybackCompleted, Error}</p></td> + * <td>{}</p></td> + * <td>After {@link #reset()}, the object is like being just created.</p></td></tr> + * <tr><td>seekTo </p></td> + * <td>{Prepared, Started, Paused, PlaybackCompleted} </p></td> + * <td>{Idle, Initialized, Stopped, Error}</p></td> + * <td>Successful invoke of this method in a valid state does not change + * the state. Calling this method in an invalid state transfers the + * object to the <em>Error</em> state. </p></td></tr> + * <tr><td>setAudioStreamType </p></td> + * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused, + * PlaybackCompleted}</p></td> + * <td>{Error}</p></td> + * <td>Successful invoke of this method does not change the state.</p></td></tr> + * <tr><td>setDataSource </p></td> + * <td>{Idle} </p></td> + * <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted, + * Error} </p></td> + * <td>Successful invoke of this method in a valid state transfers the + * object to the <em>Initialized</em> state. Calling this method in an + * invalid state throws an IllegalStateException.</p></td></tr> + * <tr><td>setDisplay </p></td> + * <td>any </p></td> + * <td>{} </p></td> + * <td>This method can be called in any state and calling it does not change + * the object state. </p></td></tr> + * <tr><td>setLooping </p></td> + * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused, + * PlaybackCompleted}</p></td> + * <td>{Error}</p></td> + * <td>Successful invoke of this method in a valid state does not change + * the state. Calling this method in an + * invalid state transfers the object to the <em>Error</em> state.</p></td></tr> + * <tr><td>isLooping </p></td> + * <td>any </p></td> + * <td>{} </p></td> + * <td>This method can be called in any state and calling it does not change + * the object state. </p></td></tr> + * <tr><td>setOnBufferingUpdateListener </p></td> + * <td>any </p></td> + * <td>{} </p></td> + * <td>This method can be called in any state and calling it does not change + * the object state. </p></td></tr> + * <tr><td>setOnCompletionListener </p></td> + * <td>any </p></td> + * <td>{} </p></td> + * <td>This method can be called in any state and calling it does not change + * the object state. </p></td></tr> + * <tr><td>setOnErrorListener </p></td> + * <td>any </p></td> + * <td>{} </p></td> + * <td>This method can be called in any state and calling it does not change + * the object state. </p></td></tr> + * <tr><td>setOnPreparedListener </p></td> + * <td>any </p></td> + * <td>{} </p></td> + * <td>This method can be called in any state and calling it does not change + * the object state. </p></td></tr> + * <tr><td>setOnSeekCompleteListener </p></td> + * <td>any </p></td> + * <td>{} </p></td> + * <td>This method can be called in any state and calling it does not change + * the object state. </p></td></tr> + * <tr><td>setScreenOnWhilePlaying</></td> + * <td>any </p></td> + * <td>{} </p></td> + * <td>This method can be called in any state and calling it does not change + * the object state. </p></td></tr> + * <tr><td>setVolume </p></td> + * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused, + * PlaybackCompleted}</p></td> + * <td>{Error}</p></td> + * <td>Successful invoke of this method does not change the state. + * <tr><td>setWakeMode </p></td> + * <td>any </p></td> + * <td>{} </p></td> + * <td>This method can be called in any state and calling it does not change + * the object state.</p></td></tr> + * <tr><td>start </p></td> + * <td>{Prepared, Started, Paused, PlaybackCompleted}</p></td> + * <td>{Idle, Initialized, Stopped, Error}</p></td> + * <td>Successful invoke of this method in a valid state transfers the + * object to the <em>Started</em> state. Calling this method in an + * invalid state transfers the object to the <em>Error</em> state.</p></td></tr> + * <tr><td>stop </p></td> + * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td> + * <td>{Idle, Initialized, Error}</p></td> + * <td>Successful invoke of this method in a valid state transfers the + * object to the <em>Stopped</em> state. Calling this method in an + * invalid state transfers the object to the <em>Error</em> state.</p></td></tr> + * </table> + * + * <a name="Permissions"></a> + * <h3>Permissions</h3> + * <p>One may need to declare a corresponding WAKE_LOCK permission {@link + * android.R.styleable#AndroidManifestUsesPermission <uses-permission>} + * element. + * + */ +public class MediaPlayer +{ + static { + System.loadLibrary("media_jni"); + } + + private final static String TAG = "MediaPlayer"; + + private int mNativeContext; // accessed by native methods + private int mListenerContext; // accessed by native methods + private Surface mSurface; // accessed by native methods + private SurfaceHolder mSurfaceHolder; + private EventHandler mEventHandler; + private PowerManager.WakeLock mWakeLock = null; + private boolean mScreenOnWhilePlaying; + private boolean mStayAwake; + + /** + * Default constructor. Consider using one of the create() methods for + * synchronously instantiating a MediaPlayer from a Uri or resource. + * <p>When done with the MediaPlayer, you should call {@link #release()}, + * to free the resources. If not released, too many MediaPlayer instances may + * result in an exception.</p> + */ + public MediaPlayer() { + + Looper looper; + if ((looper = Looper.myLooper()) != null) { + mEventHandler = new EventHandler(this, looper); + } else if ((looper = Looper.getMainLooper()) != null) { + mEventHandler = new EventHandler(this, looper); + } else { + mEventHandler = null; + } + + /* Native setup requires a weak reference to our object. + * It's easier to create it here than in C++. + */ + native_setup(new WeakReference<MediaPlayer>(this)); + } + + /** + * Sets the SurfaceHolder to use for displaying the video portion of the media. + * This call is optional. Not calling it when playing back a video will + * result in only the audio track being played. + * + * @param sh the SurfaceHolder to use for video display + */ + public void setDisplay(SurfaceHolder sh) { + mSurfaceHolder = sh; + mSurface = sh.getSurface(); + updateSurfaceScreenOn(); + } + + /** + * Convenience method to create a MediaPlayer for a given Uri. + * On success, {@link #prepare()} will already have been called and must not be called again. + * <p>When done with the MediaPlayer, you should call {@link #release()}, + * to free the resources. If not released, too many MediaPlayer instances will + * result in an exception.</p> + * + * @param context the Context to use + * @param uri the Uri from which to get the datasource + * @return a MediaPlayer object, or null if creation failed + */ + public static MediaPlayer create(Context context, Uri uri) { + return create (context, uri, null); + } + + /** + * Convenience method to create a MediaPlayer for a given Uri. + * On success, {@link #prepare()} will already have been called and must not be called again. + * <p>When done with the MediaPlayer, you should call {@link #release()}, + * to free the resources. If not released, too many MediaPlayer instances will + * result in an exception.</p> + * + * @param context the Context to use + * @param uri the Uri from which to get the datasource + * @param holder the SurfaceHolder to use for displaying the video + * @return a MediaPlayer object, or null if creation failed + */ + public static MediaPlayer create(Context context, Uri uri, SurfaceHolder holder) { + + try { + MediaPlayer mp = new MediaPlayer(); + mp.setDataSource(context, uri); + if (holder != null) { + mp.setDisplay(holder); + } + mp.prepare(); + return mp; + } catch (IOException ex) { + Log.d(TAG, "create failed:", ex); + // fall through + } catch (IllegalArgumentException ex) { + Log.d(TAG, "create failed:", ex); + // fall through + } catch (SecurityException ex) { + Log.d(TAG, "create failed:", ex); + // fall through + } + + return null; + } + + /** + * Convenience method to create a MediaPlayer for a given resource id. + * On success, {@link #prepare()} will already have been called and must not be called again. + * <p>When done with the MediaPlayer, you should call {@link #release()}, + * to free the resources. If not released, too many MediaPlayer instances will + * result in an exception.</p> + * + * @param context the Context to use + * @param resid the raw resource id (<var>R.raw.<something></var>) for + * the resource to use as the datasource + * @return a MediaPlayer object, or null if creation failed + */ + public static MediaPlayer create(Context context, int resid) { + try { + AssetFileDescriptor afd = context.getResources().openRawResourceFd(resid); + if (afd == null) return null; + + MediaPlayer mp = new MediaPlayer(); + mp.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); + afd.close(); + mp.prepare(); + return mp; + } catch (IOException ex) { + Log.d(TAG, "create failed:", ex); + // fall through + } catch (IllegalArgumentException ex) { + Log.d(TAG, "create failed:", ex); + // fall through + } catch (SecurityException ex) { + Log.d(TAG, "create failed:", ex); + // fall through + } + return null; + } + + /** + * Sets the data source as a content Uri. + * + * @param context the Context to use when resolving the Uri + * @param uri the Content URI of the data you want to play + * @throws IllegalStateException if it is called in an invalid state + */ + public void setDataSource(Context context, Uri uri) + throws IOException, IllegalArgumentException, SecurityException, IllegalStateException { + + String scheme = uri.getScheme(); + if(scheme == null || scheme.equals("file")) { + setDataSource(uri.getPath()); + return; + } + + AssetFileDescriptor fd = null; + try { + ContentResolver resolver = context.getContentResolver(); + fd = resolver.openAssetFileDescriptor(uri, "r"); + if (fd == null) { + return; + } + // Note: using getDeclaredLength so that our behavior is the same + // as previous versions when the content provider is returning + // a full file. + if (fd.getDeclaredLength() < 0) { + setDataSource(fd.getFileDescriptor()); + } else { + setDataSource(fd.getFileDescriptor(), fd.getStartOffset(), fd.getDeclaredLength()); + } + return; + } catch (SecurityException ex) { + } catch (IOException ex) { + } finally { + if (fd != null) { + fd.close(); + } + } + setDataSource(uri.toString()); + return; + } + + /** + * Sets the data source (file-path or http/rtsp URL) to use. + * + * @param path the path of the file, or the http/rtsp URL of the stream you want to play + * @throws IllegalStateException if it is called in an invalid state + */ + public native void setDataSource(String path) throws IOException, IllegalArgumentException, IllegalStateException; + + /** + * Sets the data source (FileDescriptor) to use. It is the caller's responsibility + * to close the file descriptor. It is safe to do so as soon as this call returns. + * + * @param fd the FileDescriptor for the file you want to play + * @throws IllegalStateException if it is called in an invalid state + */ + public void setDataSource(FileDescriptor fd) + throws IOException, IllegalArgumentException, IllegalStateException { + // intentionally less than LONG_MAX + setDataSource(fd, 0, 0x7ffffffffffffffL); + } + + /** + * Sets the data source (FileDescriptor) to use. It is the caller's responsibility + * to close the file descriptor. It is safe to do so as soon as this call returns. + * + * @param fd the FileDescriptor for the file you want to play + * @param offset the offset into the file where the data to be played starts, in bytes + * @param length the length in bytes of the data to be played + * @throws IllegalStateException if it is called in an invalid state + */ + public native void setDataSource(FileDescriptor fd, long offset, long length) + throws IOException, IllegalArgumentException, IllegalStateException; + + /** + * Prepares the player for playback, synchronously. + * + * After setting the datasource and the display surface, you need to either + * call prepare() or prepareAsync(). For files, it is OK to call prepare(), + * which blocks until MediaPlayer is ready for playback. + * + * @throws IllegalStateException if it is called in an invalid state + */ + public native void prepare() throws IOException, IllegalStateException; + + /** + * Prepares the player for playback, asynchronously. + * + * After setting the datasource and the display surface, you need to either + * call prepare() or prepareAsync(). For streams, you should call prepareAsync(), + * which returns immediately, rather than blocking until enough data has been + * buffered. + * + * @throws IllegalStateException if it is called in an invalid state + */ + public native void prepareAsync() throws IllegalStateException; + + /** + * Starts or resumes playback. If playback had previously been paused, + * playback will continue from where it was paused. If playback had + * been stopped, or never started before, playback will start at the + * beginning. + * + * @throws IllegalStateException if it is called in an invalid state + */ + public void start() throws IllegalStateException { + stayAwake(true); + _start(); + } + + private native void _start() throws IllegalStateException; + + /** + * Stops playback after playback has been stopped or paused. + * + * @throws IllegalStateException if the internal player engine has not been + * initialized. + */ + public void stop() throws IllegalStateException { + stayAwake(false); + _stop(); + } + + private native void _stop() throws IllegalStateException; + + /** + * Pauses playback. Call start() to resume. + * + * @throws IllegalStateException if the internal player engine has not been + * initialized. + */ + public void pause() throws IllegalStateException { + stayAwake(false); + _pause(); + } + + private native void _pause() throws IllegalStateException; + + /** + * Set the low-level power management behavior for this MediaPlayer. This + * can be used when the MediaPlayer is not playing through a SurfaceHolder + * set with {@link #setDisplay(SurfaceHolder)} and thus can use the + * high-level {@link #setScreenOnWhilePlaying(boolean)} feature. + * + * <p>This function has the MediaPlayer access the low-level power manager + * service to control the device's power usage while playing is occurring. + * The parameter is a combination of {@link android.os.PowerManager} wake flags. + * Use of this method requires {@link android.Manifest.permission#WAKE_LOCK} + * permission. + * By default, no attempt is made to keep the device awake during playback. + * + * @param context the Context to use + * @param mode the power/wake mode to set + * @see android.os.PowerManager + */ + public void setWakeMode(Context context, int mode) { + boolean washeld = false; + if (mWakeLock != null) { + if (mWakeLock.isHeld()) { + washeld = true; + mWakeLock.release(); + } + mWakeLock = null; + } + + PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE); + mWakeLock = pm.newWakeLock(mode|PowerManager.ON_AFTER_RELEASE, MediaPlayer.class.getName()); + mWakeLock.setReferenceCounted(false); + if (washeld) { + mWakeLock.acquire(); + } + } + + /** + * Control whether we should use the attached SurfaceHolder to keep the + * screen on while video playback is occurring. This is the preferred + * method over {@link #setWakeMode} where possible, since it doesn't + * require that the application have permission for low-level wake lock + * access. + * + * @param screenOn Supply true to keep the screen on, false to allow it + * to turn off. + */ + public void setScreenOnWhilePlaying(boolean screenOn) { + if (mScreenOnWhilePlaying != screenOn) { + mScreenOnWhilePlaying = screenOn; + updateSurfaceScreenOn(); + } + } + + private void stayAwake(boolean awake) { + if (mWakeLock != null) { + if (awake && !mWakeLock.isHeld()) { + mWakeLock.acquire(); + } else if (!awake && mWakeLock.isHeld()) { + mWakeLock.release(); + } + } + mStayAwake = awake; + updateSurfaceScreenOn(); + } + + private void updateSurfaceScreenOn() { + if (mSurfaceHolder != null) { + mSurfaceHolder.setKeepScreenOn(mScreenOnWhilePlaying && mStayAwake); + } + } + + /** + * Returns the width of the video. + * + * @return the width of the video, or 0 if there is no video, + * no display surface was set, or prepare()/prepareAsync() + * have not completed yet + */ + public native int getVideoWidth(); + + /** + * Returns the height of the video. + * + * @return the height of the video, or 0 if there is no video, + * no display surface was set, or prepare()/prepareAsync() + * have not completed yet + */ + public native int getVideoHeight(); + + /** + * Checks whether the MediaPlayer is playing. + * + * @return true if currently playing, false otherwise + */ + public native boolean isPlaying(); + + /** + * Seeks to specified time position. + * + * @param msec the offset in milliseconds from the start to seek to + * @throws IllegalStateException if the internal player engine has not been + * initialized + */ + public native void seekTo(int msec) throws IllegalStateException; + + /** + * Gets the current playback position. + * + * @return the current position in milliseconds + */ + public native int getCurrentPosition(); + + /** + * Gets the duration of the file. + * + * @return the duration in milliseconds + */ + public native int getDuration(); + + /** + * Releases resources associated with this MediaPlayer object. + * It is considered good practice to call this method when you're + * done using the MediaPlayer. + */ + public void release() { + stayAwake(false); + updateSurfaceScreenOn(); + mOnPreparedListener = null; + mOnBufferingUpdateListener = null; + mOnCompletionListener = null; + mOnSeekCompleteListener = null; + mOnErrorListener = null; + mOnVideoSizeChangedListener = null; + _release(); + } + + private native void _release(); + + /** + * Resets the MediaPlayer to its uninitialized state. After calling + * this method, you will have to initialize it again by setting the + * data source and calling prepare(). + */ + public void reset() { + stayAwake(false); + _reset(); + // make sure none of the listeners get called anymore + mEventHandler.removeCallbacksAndMessages(null); + } + + private native void _reset(); + + /** + * Sets the audio stream type for this MediaPlayer. See {@link AudioManager} + * for a list of stream types. + * + * @param streamtype the audio stream type + * @see android.media.AudioManager + */ + public native void setAudioStreamType(int streamtype); + + /** + * Sets the player to be looping or non-looping. + * + * @param looping whether to loop or not + */ + public native void setLooping(boolean looping); + + /** + * Checks whether the MediaPlayer is looping or non-looping. + * + * @return true if the MediaPlayer is currently looping, false otherwise + */ + public native boolean isLooping(); + + /** + * Sets the volume on this player. + * This API is recommended for balancing the output of audio streams + * within an application. Unless you are writing an application to + * control user settings, this API should be used in preference to + * {@link AudioManager#setStreamVolume(int, int, int)} which sets the volume of ALL streams of + * a particular type. Note that the passed volume values are raw scalars. + * UI controls should be scaled logarithmically. + * + * @param leftVolume left volume scalar + * @param rightVolume right volume scalar + */ + public native void setVolume(float leftVolume, float rightVolume); + + /** + * Currently not implemented, returns null. + * @deprecated + * @hide + */ + public native Bitmap getFrameAt(int msec) throws IllegalStateException; + + private native final void native_setup(Object mediaplayer_this); + private native final void native_finalize(); + @Override + protected void finalize() { native_finalize(); } + + /* Do not change these values without updating their counterparts + * in include/media/mediaplayer.h! + */ + private static final int MEDIA_NOP = 0; // interface test message + private static final int MEDIA_PREPARED = 1; + private static final int MEDIA_PLAYBACK_COMPLETE = 2; + private static final int MEDIA_BUFFERING_UPDATE = 3; + private static final int MEDIA_SEEK_COMPLETE = 4; + private static final int MEDIA_SET_VIDEO_SIZE = 5; + private static final int MEDIA_ERROR = 100; + + // error codes from framework that indicate content issues + // contained in arg1 of error message + + // Seek not supported - live stream + private static final int ERROR_SEEK_NOT_SUPPORTED = 42; + + // A/V interleave exceeds the progressive streaming buffer + private static final int ERROR_CONTENT_IS_POORLY_INTERLEAVED = 43; + + // video decoder is falling behind - content is too complex + private static final int ERROR_VIDEO_TRACK_IS_FALLING_BEHIND = 44; + + private class EventHandler extends Handler + { + private MediaPlayer mMediaPlayer; + + public EventHandler(MediaPlayer mp, Looper looper) { + super(looper); + mMediaPlayer = mp; + } + + @Override + public void handleMessage(Message msg) { + if (mMediaPlayer.mNativeContext == 0) { + Log.w(TAG, "mediaplayer went away with unhandled events"); + return; + } + switch(msg.what) { + case MEDIA_PREPARED: + if (mOnPreparedListener != null) + mOnPreparedListener.onPrepared(mMediaPlayer); + return; + + case MEDIA_PLAYBACK_COMPLETE: + if (mOnCompletionListener != null) + mOnCompletionListener.onCompletion(mMediaPlayer); + stayAwake(false); + return; + + case MEDIA_BUFFERING_UPDATE: + if (mOnBufferingUpdateListener != null) + mOnBufferingUpdateListener.onBufferingUpdate(mMediaPlayer, msg.arg1); + return; + + case MEDIA_SEEK_COMPLETE: + if (mOnSeekCompleteListener != null) + mOnSeekCompleteListener.onSeekComplete(mMediaPlayer); + return; + + case MEDIA_SET_VIDEO_SIZE: + if (mOnVideoSizeChangedListener != null) + mOnVideoSizeChangedListener.onVideoSizeChanged(mMediaPlayer, msg.arg1, msg.arg2); + return; + + case MEDIA_ERROR: + Log.e(TAG, "Error (" + msg.arg1 + "," + msg.arg2 + ")"); + boolean error_was_handled = false; + if (mOnErrorListener != null) { + error_was_handled = mOnErrorListener.onError(mMediaPlayer, msg.arg1, msg.arg2); + } + if (mOnCompletionListener != null && ! error_was_handled) { + mOnCompletionListener.onCompletion(mMediaPlayer); + } + stayAwake(false); + return; + case MEDIA_NOP: // interface test message - ignore + break; + + default: + Log.e(TAG, "Unknown message type " + msg.what); + return; + } + } + } + + /** + * Called from native code when an interesting event happens. This method + * just uses the EventHandler system to post the event back to the main app thread. + * We use a weak reference to the original MediaPlayer object so that the native + * code is safe from the object disappearing from underneath it. (This is + * the cookie passed to native_setup().) + */ + private static void postEventFromNative(Object mediaplayer_ref, + int what, int arg1, int arg2, Object obj) + { + MediaPlayer mp = (MediaPlayer)((WeakReference)mediaplayer_ref).get(); + if (mp == null) { + return; + } + + if (mp.mEventHandler != null) { + Message m = mp.mEventHandler.obtainMessage(what, arg1, arg2, obj); + mp.mEventHandler.sendMessage(m); + } + } + + /** + * Interface definition for a callback to be invoked when the media + * source is ready for playback. + */ + public interface OnPreparedListener + { + /** + * Called when the media file is ready for playback. + * + * @param mp the MediaPlayer that is ready for playback + */ + void onPrepared(MediaPlayer mp); + } + + /** + * Register a callback to be invoked when the media source is ready + * for playback. + * + * @param l the callback that will be run + */ + public void setOnPreparedListener(OnPreparedListener l) + { + mOnPreparedListener = l; + } + + private OnPreparedListener mOnPreparedListener; + + /** + * Interface definition for a callback to be invoked when playback of + * a media source has completed. + */ + public interface OnCompletionListener + { + /** + * Called when the end of a media source is reached during playback. + * + * @param mp the MediaPlayer that reached the end of the file + */ + void onCompletion(MediaPlayer mp); + } + + /** + * Register a callback to be invoked when the end of a media source + * has been reached during playback. + * + * @param l the callback that will be run + */ + public void setOnCompletionListener(OnCompletionListener l) + { + mOnCompletionListener = l; + } + + private OnCompletionListener mOnCompletionListener; + + /** + * Interface definition of a callback to be invoked indicating buffering + * status of a media resource being streamed over the network. + */ + public interface OnBufferingUpdateListener + { + /** + * Called to update status in buffering a media stream. + * + * @param mp the MediaPlayer the update pertains to + * @param percent the percentage (0-100) of the buffer + * that has been filled thus far + */ + void onBufferingUpdate(MediaPlayer mp, int percent); + } + + /** + * Register a callback to be invoked when the status of a network + * stream's buffer has changed. + * + * @param l the callback that will be run + */ + public void setOnBufferingUpdateListener(OnBufferingUpdateListener l) + { + mOnBufferingUpdateListener = l; + } + + private OnBufferingUpdateListener mOnBufferingUpdateListener; + + /** + * Interface definition of a callback to be invoked indicating + * the completion of a seek operation. + */ + public interface OnSeekCompleteListener + { + /** + * Called to indicate the completion of a seek operation. + * + * @param mp the MediaPlayer that issued the seek operation + */ + public void onSeekComplete(MediaPlayer mp); + } + + /** + * Register a callback to be invoked when a seek operation has been + * completed. + * + * @param l the callback that will be run + */ + public void setOnSeekCompleteListener(OnSeekCompleteListener l) + { + mOnSeekCompleteListener = l; + } + + private OnSeekCompleteListener mOnSeekCompleteListener; + + /** + * Interface definition of a callback to be invoked when the + * video size is first known or updated + * FIXME: Unhide this API after approval + * @hide + */ + public interface OnVideoSizeChangedListener + { + /** + * Called to indicate the video size + * + * @param mp the MediaPlayer associated with this callback + * @param width the width of the video + * @param height the height of the video + * @hide + */ + public void onVideoSizeChanged(MediaPlayer mp, int width, int height); + } + + /** + * Register a callback to be invoked when the video size is + * known or updated. + * + * @param l the callback that will be run + * @hide + */ + public void setOnVideoSizeChangedListener(OnVideoSizeChangedListener l) + { + mOnVideoSizeChangedListener = l; + } + + private OnVideoSizeChangedListener mOnVideoSizeChangedListener; + + /* Do not change these values without updating their counterparts + * in include/media/mediaplayer.h! + */ + /** Unspecified media player error. + * @see android.media.MediaPlayer.OnErrorListener + */ + public static final int MEDIA_ERROR_UNKNOWN = 1; + /** Media server died. In this case, the application must release the + * MediaPlayer object and instantiate a new one. + * @see android.media.MediaPlayer.OnErrorListener + */ + public static final int MEDIA_ERROR_SERVER_DIED = 100; + + + /** + * Interface definition of a callback to be invoked when there + * has been an error during an asynchronous operation (other errors + * will throw exceptions at method call time). + */ + public interface OnErrorListener + { + /** + * Called to indicate an error. + * + * @param mp the MediaPlayer the error pertains to + * @param what the type of error that has occurred: + * <ul> + * <li>{@link #MEDIA_ERROR_UNKNOWN} + * <li>{@link #MEDIA_ERROR_SERVER_DIED} + * </ul> + * @param extra an extra code, specific to the error type + * @return True if the method handled the error, false if it didn't. + * Returning false, or not having an OnErrorListener at all, will + * cause the OnCompletionListener to be called. + */ + boolean onError(MediaPlayer mp, int what, int extra); + } + + /** + * Register a callback to be invoked when an error has happened + * during an asynchronous operation. + * + * @param l the callback that will be run + */ + public void setOnErrorListener(OnErrorListener l) + { + mOnErrorListener = l; + } + + private OnErrorListener mOnErrorListener; +} diff --git a/media/java/android/media/MediaRecorder.java b/media/java/android/media/MediaRecorder.java new file mode 100644 index 0000000..4906cbb --- /dev/null +++ b/media/java/android/media/MediaRecorder.java @@ -0,0 +1,491 @@ +/* + * Copyright (C) 2007 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 android.hardware.Camera; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Log; +import android.view.Surface; +import java.io.IOException; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileDescriptor; +import java.lang.ref.WeakReference; + +/** + * Used to record audio and video. The recording control is based on a + * simple state machine (see below). + * + * <p><img src="{@docRoot}images/mediarecorder_state_diagram.gif" border="0" /> + * </p> + * + * <p>A common case of using MediaRecorder to record audio works as follows: + * + * <pre>MediaRecorder recorder = new MediaRecorder(); + * recorder.setAudioSource(MediaRecorder.AudioSource.MIC); + * recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); + * recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); + * recorder.setOutputFile(PATH_NAME); + * recorder.prepare(); + * recorder.start(); // Recording is now started + * ... + * recorder.stop(); + * recorder.reset(); // You can reuse the object by going back to setAudioSource() step + * recorder.release(); // Now the object cannot be reused + * </pre> + * + * <p>See the <a href="{@docRoot}guide/topics/media/index.html">Audio and Video</a> + * documentation for additional help with using MediaRecorder. + */ +public class MediaRecorder +{ + static { + System.loadLibrary("media_jni"); + } + private final static String TAG = "MediaRecorder"; + + // The two fields below are accessed by native methods + @SuppressWarnings("unused") + private int mNativeContext; + + @SuppressWarnings("unused") + private Surface mSurface; + + private String mPath; + private FileDescriptor mFd; + private EventHandler mEventHandler; + private OnErrorListener mOnErrorListener; + + /** + * Default constructor. + */ + public MediaRecorder() { + + Looper looper; + if ((looper = Looper.myLooper()) != null) { + mEventHandler = new EventHandler(this, looper); + } else if ((looper = Looper.getMainLooper()) != null) { + mEventHandler = new EventHandler(this, looper); + } else { + mEventHandler = null; + } + + /* Native setup requires a weak reference to our object. + * It's easier to create it here than in C++. + */ + native_setup(new WeakReference<MediaRecorder>(this)); + } + + /** + * Sets a Camera to use for recording. Use this function to switch + * quickly between preview and capture mode without a teardown of + * the camera object. Must call before prepare(). + * + * @param c the Camera to use for recording + */ + public native void setCamera(Camera c); + + /** + * Sets a Surface to show a preview of recorded media (video). Calls this + * before prepare() to make sure that the desirable preview display is + * set. + * + * @param sv the Surface to use for the preview + */ + public void setPreviewDisplay(Surface sv) { + mSurface = sv; + } + + /** + * Defines the audio source. These constants are used with + * {@link MediaRecorder#setAudioSource(int)}. + */ + public final class AudioSource { + /* Do not change these values without updating their counterparts + * in include/media/mediarecorder.h! + */ + private AudioSource() {} + public static final int DEFAULT = 0; + /** Microphone audio source */ + public static final int MIC = 1; + } + + /** + * Defines the video source. These constants are used with + * {@link MediaRecorder#setVideoSource(int)}. + */ + public final class VideoSource { + /* Do not change these values without updating their counterparts + * in include/media/mediarecorder.h! + */ + private VideoSource() {} + public static final int DEFAULT = 0; + /** Camera video source */ + public static final int CAMERA = 1; + } + + /** + * Defines the output format. These constants are used with + * {@link MediaRecorder#setOutputFormat(int)}. + */ + public final class OutputFormat { + /* Do not change these values without updating their counterparts + * in include/media/mediarecorder.h! + */ + private OutputFormat() {} + public static final int DEFAULT = 0; + /** 3GPP media file format*/ + public static final int THREE_GPP = 1; + /** MPEG4 media file format*/ + public static final int MPEG_4 = 2; + /** Raw AMR file format */ + public static final int RAW_AMR = 3; + }; + + /** + * Defines the audio encoding. These constants are used with + * {@link MediaRecorder#setAudioEncoder(int)}. + */ + public final class AudioEncoder { + /* Do not change these values without updating their counterparts + * in include/media/mediarecorder.h! + */ + private AudioEncoder() {} + public static final int DEFAULT = 0; + /** AMR (Narrowband) audio codec */ + public static final int AMR_NB = 1; + //public static final AAC = 2; currently unsupported + } + + /** + * Defines the video encoding. These constants are used with + * {@link MediaRecorder#setVideoEncoder(int)}. + */ + public final class VideoEncoder { + /* Do not change these values without updating their counterparts + * in include/media/mediarecorder.h! + */ + private VideoEncoder() {} + public static final int DEFAULT = 0; + public static final int H263 = 1; + public static final int H264 = 2; + public static final int MPEG_4_SP = 3; + } + + /** + * Sets the audio source to be used for recording. If this method is not + * called, the output file will not contain an audio track. The source needs + * to be specified before setting recording-parameters or encoders. Call + * this only before setOutputFormat(). + * + * @param audio_source the audio source to use + * @throws IllegalStateException if it is called after setOutputFormat() + * @see android.media.MediaRecorder.AudioSource + */ + public native void setAudioSource(int audio_source) + throws IllegalStateException; + + /** + * Sets the video source to be used for recording. If this method is not + * called, the output file will not contain an video track. The source needs + * to be specified before setting recording-parameters or encoders. Call + * this only before setOutputFormat(). + * + * @param video_source the video source to use + * @throws IllegalStateException if it is called after setOutputFormat() + * @see android.media.MediaRecorder.VideoSource + */ + public native void setVideoSource(int video_source) + throws IllegalStateException; + + /** + * Sets the format of the output file produced during recording. Call this + * after setAudioSource()/setVideoSource() but before prepare(). + * + * @param output_format the output format to use. The output format + * needs to be specified before setting recording-parameters or encoders. + * @throws IllegalStateException if it is called after prepare() or before + * setAudioSource()/setVideoSource(). + * @see android.media.MediaRecorder.OutputFormat + */ + public native void setOutputFormat(int output_format) + throws IllegalStateException; + + /** + * Sets the width and height of the video to be captured. Must be called + * after setVideoSource(). Call this after setOutFormat() but before + * prepare(). + * + * @param width the width of the video to be captured + * @param height the height of the video to be captured + * @throws IllegalStateException if it is called after + * prepare() or before setOutputFormat() + */ + public native void setVideoSize(int width, int height) + throws IllegalStateException; + + /** + * Sets the frame rate of the video to be captured. Must be called + * after setVideoSource(). Call this after setOutFormat() but before + * prepare(). + * + * @param rate the number of frames per second of video to capture + * @throws IllegalStateException if it is called after + * prepare() or before setOutputFormat(). + * + * NOTE: On some devices that have auto-frame rate, this sets the + * maximum frame rate, not a constant frame rate. Actual frame rate + * will vary according to lighting conditions. + */ + public native void setVideoFrameRate(int rate) throws IllegalStateException; + + /** + * Sets the audio encoder to be used for recording. If this method is not + * called, the output file will not contain an audio track. Call this after + * setOutputFormat() but before prepare(). + * + * @param audio_encoder the audio encoder to use. + * @throws IllegalStateException if it is called before + * setOutputFormat() or after prepare(). + * @see android.media.MediaRecorder.AudioEncoder + */ + public native void setAudioEncoder(int audio_encoder) + throws IllegalStateException; + + /** + * Sets the video encoder to be used for recording. If this method is not + * called, the output file will not contain an video track. Call this after + * setOutputFormat() and before prepare(). + * + * @param video_encoder the video encoder to use. + * @throws IllegalStateException if it is called before + * setOutputFormat() or after prepare() + * @see android.media.MediaRecorder.VideoEncoder + */ + public native void setVideoEncoder(int video_encoder) + throws IllegalStateException; + + /** + * Pass in the file descriptor of the file to be written. Call this after + * setOutputFormat() but before prepare(). + * + * @param fd an open file descriptor to be written into. + * @throws IllegalStateException if it is called before + * setOutputFormat() or after prepare() + */ + public void setOutputFile(FileDescriptor fd) throws IllegalStateException + { + mPath = null; + mFd = fd; + } + + /** + * Sets the path of the output file to be produced. Call this after + * setOutputFormat() but before prepare(). + * + * @param path The pathname to use. + * @throws IllegalStateException if it is called before + * setOutputFormat() or after prepare() + */ + public void setOutputFile(String path) throws IllegalStateException + { + mFd = null; + mPath = path; + } + + // native implementation + private native void _setOutputFile(FileDescriptor fd, long offset, long length) + throws IllegalStateException, IOException; + private native void _prepare() throws IllegalStateException, IOException; + + /** + * Prepares the recorder to begin capturing and encoding data. This method + * must be called after setting up the desired audio and video sources, + * encoders, file format, etc., but before start(). + * + * @throws IllegalStateException if it is called after + * start() or before setOutputFormat(). + * @throws IOException if prepare fails otherwise. + */ + public void prepare() throws IllegalStateException, IOException + { + if (mPath != null) { + FileOutputStream fos = new FileOutputStream(mPath); + try { + _setOutputFile(fos.getFD(), 0, 0); + } finally { + fos.close(); + } + } else if (mFd != null) { + _setOutputFile(mFd, 0, 0); + } else { + throw new IOException("No valid output file"); + } + _prepare(); + } + + /** + * Begins capturing and encoding data to the file specified with + * setOutputFile(). Call this after prepare(). + * + * @throws IllegalStateException if it is called before + * prepare(). + */ + public native void start() throws IllegalStateException; + + /** + * Stops recording. Call this after start(). Once recording is stopped, + * you will have to configure it again as if it has just been constructed. + * + * @throws IllegalStateException if it is called before start() + */ + public native void stop() throws IllegalStateException; + + /** + * Restarts the MediaRecorder to its idle state. After calling + * this method, you will have to configure it again as if it had just been + * constructed. + */ + public void reset() { + native_reset(); + + // make sure none of the listeners get called anymore + mEventHandler.removeCallbacksAndMessages(null); + } + + private native void native_reset(); + + /** + * Returns the maximum absolute amplitude that was sampled since the last + * call to this method. Call this only after the setAudioSource(). + * + * @return the maximum absolute amplitude measured since the last call, or + * 0 when called for the first time + * @throws IllegalStateException if it is called before + * the audio source has been set. + */ + public native int getMaxAmplitude() throws IllegalStateException; + + /* Do not change this value without updating its counterpart + * in include/media/mediarecorder.h! + */ + /** Unspecified media recorder error. + * @see android.media.MediaRecorder.OnErrorListener + */ + public static final int MEDIA_RECORDER_ERROR_UNKNOWN = 1; + + /** + * Interface definition for a callback to be invoked when an error + * occurs while recording. + */ + public interface OnErrorListener + { + /** + * Called when an error occurs while recording. + * + * @param mr the MediaRecorder that encountered the error + * @param what the type of error that has occurred: + * <ul> + * <li>{@link #MEDIA_RECORDER_ERROR_UNKNOWN} + * </ul> + * @param extra an extra code, specific to the error type + */ + void onError(MediaRecorder mr, int what, int extra); + } + + /** + * Register a callback to be invoked when an error occurs while + * recording. + * + * @param l the callback that will be run + */ + public void setOnErrorListener(OnErrorListener l) + { + mOnErrorListener = l; + } + + private class EventHandler extends Handler + { + private MediaRecorder mMediaRecorder; + + public EventHandler(MediaRecorder mr, Looper looper) { + super(looper); + mMediaRecorder = mr; + } + + /* Do not change this value without updating its counterpart + * in include/media/mediarecorder.h! + */ + private static final int MEDIA_RECORDER_EVENT_ERROR = 1; + + @Override + public void handleMessage(Message msg) { + if (mMediaRecorder.mNativeContext == 0) { + Log.w(TAG, "mediarecorder went away with unhandled events"); + return; + } + switch(msg.what) { + case MEDIA_RECORDER_EVENT_ERROR: + if (mOnErrorListener != null) + mOnErrorListener.onError(mMediaRecorder, msg.arg1, msg.arg2); + + return; + + default: + Log.e(TAG, "Unknown message type " + msg.what); + return; + } + } + } + + /** + * Called from native code when an interesting event happens. This method + * just uses the EventHandler system to post the event back to the main app thread. + * We use a weak reference to the original MediaRecorder object so that the native + * code is safe from the object disappearing from underneath it. (This is + * the cookie passed to native_setup().) + */ + private static void postEventFromNative(Object mediarecorder_ref, + int what, int arg1, int arg2, Object obj) + { + MediaRecorder mr = (MediaRecorder)((WeakReference)mediarecorder_ref).get(); + if (mr == null) { + return; + } + + if (mr.mEventHandler != null) { + Message m = mr.mEventHandler.obtainMessage(what, arg1, arg2, obj); + mr.mEventHandler.sendMessage(m); + } + } + + /** + * Releases resources associated with this MediaRecorder object. + * It is good practice to call this method when you're done + * using the MediaRecorder. + */ + public native void release(); + + private native final void native_setup(Object mediarecorder_this) throws IllegalStateException; + + private native final void native_finalize(); + + @Override + protected void finalize() { native_finalize(); } +} diff --git a/media/java/android/media/MediaScanner.java b/media/java/android/media/MediaScanner.java new file mode 100644 index 0000000..fc8476d --- /dev/null +++ b/media/java/android/media/MediaScanner.java @@ -0,0 +1,1352 @@ +/* + * Copyright (C) 2007 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 android.content.ContentValues; +import android.content.Context; +import android.content.IContentProvider; +import android.content.ContentUris; +import android.database.Cursor; +import android.database.SQLException; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.Process; +import android.os.RemoteException; +import android.os.SystemProperties; +import android.provider.MediaStore; +import android.provider.Settings; +import android.provider.MediaStore.Audio; +import android.provider.MediaStore.Images; +import android.provider.MediaStore.Video; +import android.provider.MediaStore.Audio.Genres; +import android.provider.MediaStore.Audio.Playlists; +import android.sax.Element; +import android.sax.ElementListener; +import android.sax.RootElement; +import android.text.TextUtils; +import android.util.Config; +import android.util.Log; +import android.util.Xml; + +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +import java.io.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; + +/** + * Internal service that no-one should use directly. + * + * {@hide} + */ +public class MediaScanner +{ + static { + System.loadLibrary("media_jni"); + } + + private final static String TAG = "MediaScanner"; + + private static final String[] AUDIO_PROJECTION = new String[] { + Audio.Media._ID, // 0 + Audio.Media.DATA, // 1 + Audio.Media.DATE_MODIFIED, // 2 + }; + + private static final int ID_AUDIO_COLUMN_INDEX = 0; + private static final int PATH_AUDIO_COLUMN_INDEX = 1; + private static final int DATE_MODIFIED_AUDIO_COLUMN_INDEX = 2; + + private static final String[] VIDEO_PROJECTION = new String[] { + Video.Media._ID, // 0 + Video.Media.DATA, // 1 + Video.Media.DATE_MODIFIED, // 2 + }; + + private static final int ID_VIDEO_COLUMN_INDEX = 0; + private static final int PATH_VIDEO_COLUMN_INDEX = 1; + private static final int DATE_MODIFIED_VIDEO_COLUMN_INDEX = 2; + + private static final String[] IMAGES_PROJECTION = new String[] { + Images.Media._ID, // 0 + Images.Media.DATA, // 1 + Images.Media.DATE_MODIFIED, // 2 + }; + + private static final int ID_IMAGES_COLUMN_INDEX = 0; + private static final int PATH_IMAGES_COLUMN_INDEX = 1; + private static final int DATE_MODIFIED_IMAGES_COLUMN_INDEX = 2; + + private static final String[] PLAYLISTS_PROJECTION = new String[] { + Audio.Playlists._ID, // 0 + Audio.Playlists.DATA, // 1 + Audio.Playlists.DATE_MODIFIED, // 2 + }; + + private static final String[] PLAYLIST_MEMBERS_PROJECTION = new String[] { + Audio.Playlists.Members.PLAYLIST_ID, // 0 + }; + + private static final int ID_PLAYLISTS_COLUMN_INDEX = 0; + private static final int PATH_PLAYLISTS_COLUMN_INDEX = 1; + private static final int DATE_MODIFIED_PLAYLISTS_COLUMN_INDEX = 2; + + private static final String[] GENRE_LOOKUP_PROJECTION = new String[] { + Audio.Genres._ID, // 0 + Audio.Genres.NAME, // 1 + }; + + private static final String RINGTONES_DIR = "/ringtones/"; + private static final String NOTIFICATIONS_DIR = "/notifications/"; + private static final String ALARMS_DIR = "/alarms/"; + private static final String MUSIC_DIR = "/music/"; + private static final String PODCAST_DIR = "/podcasts/"; + + private static final String[] ID3_GENRES = { + // ID3v1 Genres + "Blues", + "Classic Rock", + "Country", + "Dance", + "Disco", + "Funk", + "Grunge", + "Hip-Hop", + "Jazz", + "Metal", + "New Age", + "Oldies", + "Other", + "Pop", + "R&B", + "Rap", + "Reggae", + "Rock", + "Techno", + "Industrial", + "Alternative", + "Ska", + "Death Metal", + "Pranks", + "Soundtrack", + "Euro-Techno", + "Ambient", + "Trip-Hop", + "Vocal", + "Jazz+Funk", + "Fusion", + "Trance", + "Classical", + "Instrumental", + "Acid", + "House", + "Game", + "Sound Clip", + "Gospel", + "Noise", + "AlternRock", + "Bass", + "Soul", + "Punk", + "Space", + "Meditative", + "Instrumental Pop", + "Instrumental Rock", + "Ethnic", + "Gothic", + "Darkwave", + "Techno-Industrial", + "Electronic", + "Pop-Folk", + "Eurodance", + "Dream", + "Southern Rock", + "Comedy", + "Cult", + "Gangsta", + "Top 40", + "Christian Rap", + "Pop/Funk", + "Jungle", + "Native American", + "Cabaret", + "New Wave", + "Psychadelic", + "Rave", + "Showtunes", + "Trailer", + "Lo-Fi", + "Tribal", + "Acid Punk", + "Acid Jazz", + "Polka", + "Retro", + "Musical", + "Rock & Roll", + "Hard Rock", + // The following genres are Winamp extensions + "Folk", + "Folk-Rock", + "National Folk", + "Swing", + "Fast Fusion", + "Bebob", + "Latin", + "Revival", + "Celtic", + "Bluegrass", + "Avantgarde", + "Gothic Rock", + "Progressive Rock", + "Psychedelic Rock", + "Symphonic Rock", + "Slow Rock", + "Big Band", + "Chorus", + "Easy Listening", + "Acoustic", + "Humour", + "Speech", + "Chanson", + "Opera", + "Chamber Music", + "Sonata", + "Symphony", + "Booty Bass", + "Primus", + "Porn Groove", + "Satire", + "Slow Jam", + "Club", + "Tango", + "Samba", + "Folklore", + "Ballad", + "Power Ballad", + "Rhythmic Soul", + "Freestyle", + "Duet", + "Punk Rock", + "Drum Solo", + "A capella", + "Euro-House", + "Dance Hall" + }; + + private int mNativeContext; + private Context mContext; + private IContentProvider mMediaProvider; + private Uri mAudioUri; + private Uri mVideoUri; + private Uri mImagesUri; + private Uri mThumbsUri; + private Uri mGenresUri; + private Uri mPlaylistsUri; + private boolean mProcessPlaylists, mProcessGenres; + + // used when scanning the image database so we know whether we have to prune + // old thumbnail files + private int mOriginalCount; + /** Whether the scanner has set a default sound for the ringer ringtone. */ + private boolean mDefaultRingtoneSet; + /** Whether the scanner has set a default sound for the notification ringtone. */ + private boolean mDefaultNotificationSet; + /** The filename for the default sound for the ringer ringtone. */ + private String mDefaultRingtoneFilename; + /** The filename for the default sound for the notification ringtone. */ + private String mDefaultNotificationFilename; + /** + * The prefix for system properties that define the default sound for + * ringtones. Concatenate the name of the setting from Settings + * to get the full system property. + */ + private static final String DEFAULT_RINGTONE_PROPERTY_PREFIX = "ro.config."; + + // set to true if file path comparisons should be case insensitive. + // this should be set when scanning files on a case insensitive file system. + private boolean mCaseInsensitivePaths; + + private BitmapFactory.Options mBitmapOptions = new BitmapFactory.Options(); + + private static class FileCacheEntry { + Uri mTableUri; + long mRowId; + String mPath; + long mLastModified; + boolean mSeenInFileSystem; + boolean mLastModifiedChanged; + + FileCacheEntry(Uri tableUri, long rowId, String path, long lastModified) { + mTableUri = tableUri; + mRowId = rowId; + mPath = path; + mLastModified = lastModified; + mSeenInFileSystem = false; + mLastModifiedChanged = false; + } + + @Override + public String toString() { + return mPath; + } + } + + // hashes file path to FileCacheEntry. + // path should be lower case if mCaseInsensitivePaths is true + private HashMap<String, FileCacheEntry> mFileCache; + + private ArrayList<FileCacheEntry> mPlayLists; + private HashMap<String, Uri> mGenreCache; + + + public MediaScanner(Context c) { + native_setup(); + mContext = c; + mBitmapOptions.inSampleSize = 1; + mBitmapOptions.inJustDecodeBounds = true; + + setDefaultRingtoneFileNames(); + } + + private void setDefaultRingtoneFileNames() { + mDefaultRingtoneFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX + + Settings.System.RINGTONE); + mDefaultNotificationFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX + + Settings.System.NOTIFICATION_SOUND); + } + + private MyMediaScannerClient mClient = new MyMediaScannerClient(); + + private class MyMediaScannerClient implements MediaScannerClient { + + private String mArtist; + private String mAlbumArtist; // use this if mArtist is missing + private String mAlbum; + private String mTitle; + private String mComposer; + private String mGenre; + private String mMimeType; + private int mFileType; + private int mTrack; + private int mYear; + private int mDuration; + private String mPath; + private long mLastModified; + private long mFileSize; + + public FileCacheEntry beginFile(String path, String mimeType, long lastModified, long fileSize) { + + // special case certain file names + // I use regionMatches() instead of substring() below + // to avoid memory allocation + int lastSlash = path.lastIndexOf('/'); + if (lastSlash >= 0 && lastSlash + 2 < path.length()) { + // ignore those ._* files created by MacOS + if (path.regionMatches(lastSlash + 1, "._", 0, 2)) { + return null; + } + + // ignore album art files created by Windows Media Player: + // Folder.jpg, AlbumArtSmall.jpg, AlbumArt_{...}_Large.jpg and AlbumArt_{...}_Small.jpg + if (path.regionMatches(true, path.length() - 4, ".jpg", 0, 4)) { + if (path.regionMatches(true, lastSlash + 1, "AlbumArt_{", 0, 10) || + path.regionMatches(true, lastSlash + 1, "AlbumArt.", 0, 9)) { + return null; + } + int length = path.length() - lastSlash - 1; + if ((length == 17 && path.regionMatches(true, lastSlash + 1, "AlbumArtSmall", 0, 13)) || + (length == 10 && path.regionMatches(true, lastSlash + 1, "Folder", 0, 6))) { + return null; + } + } + } + + mMimeType = null; + // try mimeType first, if it is specified + if (mimeType != null) { + mFileType = MediaFile.getFileTypeForMimeType(mimeType); + if (mFileType != 0) { + mMimeType = mimeType; + } + } + mFileSize = fileSize; + + // if mimeType was not specified, compute file type based on file extension. + if (mMimeType == null) { + MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); + if (mediaFileType != null) { + mFileType = mediaFileType.fileType; + mMimeType = mediaFileType.mimeType; + } + } + + String key = path; + if (mCaseInsensitivePaths) { + key = path.toLowerCase(); + } + FileCacheEntry entry = mFileCache.get(key); + if (entry == null) { + entry = new FileCacheEntry(null, 0, path, 0); + mFileCache.put(key, entry); + } + entry.mSeenInFileSystem = true; + + // add some slack to avoid a rounding error + long delta = lastModified - entry.mLastModified; + if (delta > 1 || delta < -1) { + entry.mLastModified = lastModified; + entry.mLastModifiedChanged = true; + } + + if (mProcessPlaylists && MediaFile.isPlayListFileType(mFileType)) { + mPlayLists.add(entry); + // we don't process playlists in the main scan, so return null + return null; + } + + // clear all the metadata + mArtist = null; + mAlbumArtist = null; + mAlbum = null; + mTitle = null; + mComposer = null; + mGenre = null; + mTrack = 0; + mYear = 0; + mDuration = 0; + mPath = path; + mLastModified = lastModified; + + return entry; + } + + public void scanFile(String path, long lastModified, long fileSize) { + doScanFile(path, null, lastModified, fileSize, false); + } + + public void scanFile(String path, String mimeType, long lastModified, long fileSize) { + doScanFile(path, mimeType, lastModified, fileSize, false); + } + + public Uri doScanFile(String path, String mimeType, long lastModified, long fileSize, boolean scanAlways) { + Uri result = null; +// long t1 = System.currentTimeMillis(); + try { + FileCacheEntry entry = beginFile(path, mimeType, lastModified, fileSize); + // rescan for metadata if file was modified since last scan + if (entry != null && (entry.mLastModifiedChanged || scanAlways)) { + boolean ringtones = (path.indexOf(RINGTONES_DIR) > 0); + boolean notifications = (path.indexOf(NOTIFICATIONS_DIR) > 0); + boolean alarms = (path.indexOf(ALARMS_DIR) > 0); + boolean podcasts = (path.indexOf(PODCAST_DIR) > 0); + boolean music = (path.indexOf(MUSIC_DIR) > 0) || + (!ringtones && !notifications && !alarms && !podcasts); + + if (mFileType == MediaFile.FILE_TYPE_MP3 || + mFileType == MediaFile.FILE_TYPE_MP4 || + mFileType == MediaFile.FILE_TYPE_M4A || + mFileType == MediaFile.FILE_TYPE_3GPP || + mFileType == MediaFile.FILE_TYPE_3GPP2 || + mFileType == MediaFile.FILE_TYPE_OGG || + mFileType == MediaFile.FILE_TYPE_MID || + mFileType == MediaFile.FILE_TYPE_WMA) { + // we only extract metadata from MP3, M4A, OGG, MID and WMA files. + // check MP4 files, to determine if they contain only audio. + processFile(path, mimeType, this); + } else if (MediaFile.isImageFileType(mFileType)) { + // we used to compute the width and height but it's not worth it + } + + result = endFile(entry, ringtones, notifications, alarms, music, podcasts); + } + } catch (RemoteException e) { + Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); + } +// long t2 = System.currentTimeMillis(); +// Log.v(TAG, "scanFile: " + path + " took " + (t2-t1)); + return result; + } + + private int parseSubstring(String s, int start, int defaultValue) { + int length = s.length(); + if (start == length) return defaultValue; + + char ch = s.charAt(start++); + // return defaultValue if we have no integer at all + if (ch < '0' || ch > '9') return defaultValue; + + int result = ch - '0'; + while (start < length) { + ch = s.charAt(start++); + if (ch < '0' || ch > '9') return result; + result = result * 10 + (ch - '0'); + } + + return result; + } + + public void handleStringTag(String name, String value) { + if (name.equalsIgnoreCase("title") || name.startsWith("title;")) { + mTitle = value.trim(); + } else if (name.equalsIgnoreCase("artist") || name.startsWith("artist;")) { + mArtist = value.trim(); + } else if (name.equalsIgnoreCase("albumartist") || name.startsWith("albumartist;")) { + mAlbumArtist = value.trim(); + } else if (name.equalsIgnoreCase("album") || name.startsWith("album;")) { + mAlbum = value.trim(); + } else if (name.equalsIgnoreCase("composer") || name.startsWith("composer;")) { + mComposer = value.trim(); + } else if (name.equalsIgnoreCase("genre") || name.startsWith("genre;")) { + // handle numeric genres, which PV sometimes encodes like "(20)" + if (value.length() > 0) { + int genreCode = -1; + char ch = value.charAt(0); + if (ch == '(') { + genreCode = parseSubstring(value, 1, -1); + } else if (ch >= '0' && ch <= '9') { + genreCode = parseSubstring(value, 0, -1); + } + if (genreCode >= 0 && genreCode < ID3_GENRES.length) { + value = ID3_GENRES[genreCode]; + } + } + mGenre = value; + } else if (name.equalsIgnoreCase("year") || name.startsWith("year;")) { + mYear = parseSubstring(value, 0, 0); + } else if (name.equalsIgnoreCase("tracknumber") || name.startsWith("tracknumber;")) { + // track number might be of the form "2/12" + // we just read the number before the slash + int num = parseSubstring(value, 0, 0); + mTrack = (mTrack / 1000) * 1000 + num; + } else if (name.equalsIgnoreCase("discnumber") || + name.equals("set") || name.startsWith("set;")) { + // set number might be of the form "1/3" + // we just read the number before the slash + int num = parseSubstring(value, 0, 0); + mTrack = (num * 1000) + (mTrack % 1000); + } else if (name.equalsIgnoreCase("duration")) { + mDuration = parseSubstring(value, 0, 0); + } + } + + public void setMimeType(String mimeType) { + mMimeType = mimeType; + mFileType = MediaFile.getFileTypeForMimeType(mimeType); + } + + /** + * Formats the data into a values array suitable for use with the Media + * Content Provider. + * + * @return a map of values + */ + private ContentValues toValues() { + ContentValues map = new ContentValues(); + + map.put(MediaStore.MediaColumns.DATA, mPath); + map.put(MediaStore.MediaColumns.TITLE, mTitle); + map.put(MediaStore.MediaColumns.DATE_MODIFIED, mLastModified); + map.put(MediaStore.MediaColumns.SIZE, mFileSize); + map.put(MediaStore.MediaColumns.MIME_TYPE, mMimeType); + + if (MediaFile.isVideoFileType(mFileType)) { + map.put(Video.Media.ARTIST, (mArtist != null && mArtist.length() > 0 ? mArtist : MediaFile.UNKNOWN_STRING)); + map.put(Video.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0 ? mAlbum : MediaFile.UNKNOWN_STRING)); + map.put(Video.Media.DURATION, mDuration); + // FIXME - add RESOLUTION + } else if (MediaFile.isImageFileType(mFileType)) { + // FIXME - add DESCRIPTION + // map.put(field, value); + } else if (MediaFile.isAudioFileType(mFileType)) { + map.put(Audio.Media.ARTIST, (mArtist != null && mArtist.length() > 0 ? mArtist : MediaFile.UNKNOWN_STRING)); + map.put(Audio.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0 ? mAlbum : MediaFile.UNKNOWN_STRING)); + map.put(Audio.Media.COMPOSER, mComposer); + if (mYear != 0) { + map.put(Audio.Media.YEAR, mYear); + } + map.put(Audio.Media.TRACK, mTrack); + map.put(Audio.Media.DURATION, mDuration); + } + return map; + } + + private Uri endFile(FileCacheEntry entry, boolean ringtones, boolean notifications, + boolean alarms, boolean music, boolean podcasts) + throws RemoteException { + // update database + Uri tableUri; + boolean isAudio = MediaFile.isAudioFileType(mFileType); + boolean isVideo = MediaFile.isVideoFileType(mFileType); + boolean isImage = MediaFile.isImageFileType(mFileType); + if (isVideo) { + tableUri = mVideoUri; + } else if (isImage) { + tableUri = mImagesUri; + } else if (isAudio) { + tableUri = mAudioUri; + } else { + // don't add file to database if not audio, video or image + return null; + } + entry.mTableUri = tableUri; + + // use album artist if artist is missing + if (mArtist == null || mArtist.length() == 0) { + mArtist = mAlbumArtist; + } + + ContentValues values = toValues(); + String title = values.getAsString(MediaStore.MediaColumns.TITLE); + if (TextUtils.isEmpty(title)) { + title = values.getAsString(MediaStore.MediaColumns.DATA); + // extract file name after last slash + int lastSlash = title.lastIndexOf('/'); + if (lastSlash >= 0) { + lastSlash++; + if (lastSlash < title.length()) { + title = title.substring(lastSlash); + } + } + // truncate the file extension (if any) + int lastDot = title.lastIndexOf('.'); + if (lastDot > 0) { + title = title.substring(0, lastDot); + } + values.put(MediaStore.MediaColumns.TITLE, title); + } + if (isAudio) { + values.put(Audio.Media.IS_RINGTONE, ringtones); + values.put(Audio.Media.IS_NOTIFICATION, notifications); + values.put(Audio.Media.IS_ALARM, alarms); + values.put(Audio.Media.IS_MUSIC, music); + values.put(Audio.Media.IS_PODCAST, podcasts); + } else if (isImage) { + // nothing right now + } + + Uri result = null; + long rowId = entry.mRowId; + if (rowId == 0) { + // new file, insert it + result = mMediaProvider.insert(tableUri, values); + if (result != null) { + rowId = ContentUris.parseId(result); + entry.mRowId = rowId; + } + } else { + // updated file + result = ContentUris.withAppendedId(tableUri, rowId); + mMediaProvider.update(result, values, null, null); + } + if (mProcessGenres && mGenre != null) { + String genre = mGenre; + Uri uri = mGenreCache.get(genre); + if (uri == null) { + Cursor cursor = null; + try { + // see if the genre already exists + cursor = mMediaProvider.query( + mGenresUri, + GENRE_LOOKUP_PROJECTION, MediaStore.Audio.Genres.NAME + "=?", + new String[] { genre }, null); + if (cursor == null || cursor.getCount() == 0) { + // genre does not exist, so create the genre in the genre table + values.clear(); + values.put(MediaStore.Audio.Genres.NAME, genre); + uri = mMediaProvider.insert(mGenresUri, values); + } else { + // genre already exists, so compute its Uri + cursor.moveToNext(); + uri = ContentUris.withAppendedId(mGenresUri, cursor.getLong(0)); + } + if (uri != null) { + uri = Uri.withAppendedPath(uri, Genres.Members.CONTENT_DIRECTORY); + mGenreCache.put(genre, uri); + } + } finally { + // release the cursor if it exists + if (cursor != null) { + cursor.close(); + } + } + } + + if (uri != null) { + // add entry to audio_genre_map + values.clear(); + values.put(MediaStore.Audio.Genres.Members.AUDIO_ID, Long.valueOf(rowId)); + mMediaProvider.insert(uri, values); + } + } + + if (notifications && !mDefaultNotificationSet) { + if (TextUtils.isEmpty(mDefaultNotificationFilename) || + doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename)) { + setSettingIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId); + mDefaultNotificationSet = true; + } + } else if (ringtones && !mDefaultRingtoneSet) { + if (TextUtils.isEmpty(mDefaultRingtoneFilename) || + doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename)) { + setSettingIfNotSet(Settings.System.RINGTONE, tableUri, rowId); + mDefaultRingtoneSet = true; + } + } + + return result; + } + + private boolean doesPathHaveFilename(String path, String filename) { + int pathFilenameStart = path.lastIndexOf(File.separatorChar) + 1; + int filenameLength = filename.length(); + return path.regionMatches(pathFilenameStart, filename, 0, filenameLength) && + pathFilenameStart + filenameLength == path.length(); + } + + private void setSettingIfNotSet(String settingName, Uri uri, long rowId) { + + String existingSettingValue = Settings.System.getString(mContext.getContentResolver(), + settingName); + + if (TextUtils.isEmpty(existingSettingValue)) { + // Set the setting to the given URI + Settings.System.putString(mContext.getContentResolver(), settingName, + ContentUris.withAppendedId(uri, rowId).toString()); + } + } + + }; // end of anonymous MediaScannerClient instance + + private void prescan(String filePath) throws RemoteException { + Cursor c = null; + String where = null; + String[] selectionArgs = null; + + if (mFileCache == null) { + mFileCache = new HashMap<String, FileCacheEntry>(); + } else { + mFileCache.clear(); + } + if (mPlayLists == null) { + mPlayLists = new ArrayList<FileCacheEntry>(); + } else { + mPlayLists.clear(); + } + + // Build the list of files from the content provider + try { + // Read existing files from the audio table + if (filePath != null) { + where = MediaStore.Audio.Media.DATA + "=?"; + selectionArgs = new String[] { filePath }; + } + c = mMediaProvider.query(mAudioUri, AUDIO_PROJECTION, where, selectionArgs, null); + + if (c != null) { + try { + while (c.moveToNext()) { + long rowId = c.getLong(ID_AUDIO_COLUMN_INDEX); + String path = c.getString(PATH_AUDIO_COLUMN_INDEX); + long lastModified = c.getLong(DATE_MODIFIED_AUDIO_COLUMN_INDEX); + + String key = path; + if (mCaseInsensitivePaths) { + key = path.toLowerCase(); + } + mFileCache.put(key, new FileCacheEntry(mAudioUri, rowId, path, + lastModified)); + } + } finally { + c.close(); + c = null; + } + } + + // Read existing files from the video table + if (filePath != null) { + where = MediaStore.Video.Media.DATA + "=?"; + } else { + where = null; + } + c = mMediaProvider.query(mVideoUri, VIDEO_PROJECTION, where, selectionArgs, null); + + if (c != null) { + try { + while (c.moveToNext()) { + long rowId = c.getLong(ID_VIDEO_COLUMN_INDEX); + String path = c.getString(PATH_VIDEO_COLUMN_INDEX); + long lastModified = c.getLong(DATE_MODIFIED_VIDEO_COLUMN_INDEX); + + String key = path; + if (mCaseInsensitivePaths) { + key = path.toLowerCase(); + } + mFileCache.put(key, new FileCacheEntry(mVideoUri, rowId, path, + lastModified)); + } + } finally { + c.close(); + c = null; + } + } + + // Read existing files from the images table + if (filePath != null) { + where = MediaStore.Images.Media.DATA + "=?"; + } else { + where = null; + } + mOriginalCount = 0; + c = mMediaProvider.query(mImagesUri, IMAGES_PROJECTION, where, selectionArgs, null); + + if (c != null) { + try { + mOriginalCount = c.getCount(); + while (c.moveToNext()) { + long rowId = c.getLong(ID_IMAGES_COLUMN_INDEX); + String path = c.getString(PATH_IMAGES_COLUMN_INDEX); + long lastModified = c.getLong(DATE_MODIFIED_IMAGES_COLUMN_INDEX); + + String key = path; + if (mCaseInsensitivePaths) { + key = path.toLowerCase(); + } + mFileCache.put(key, new FileCacheEntry(mImagesUri, rowId, path, + lastModified)); + } + } finally { + c.close(); + c = null; + } + } + + if (mProcessPlaylists) { + // Read existing files from the playlists table + if (filePath != null) { + where = MediaStore.Audio.Playlists.DATA + "=?"; + } else { + where = null; + } + c = mMediaProvider.query(mPlaylistsUri, PLAYLISTS_PROJECTION, where, selectionArgs, null); + + if (c != null) { + try { + while (c.moveToNext()) { + String path = c.getString(PATH_IMAGES_COLUMN_INDEX); + + if (path != null && path.length() > 0) { + long rowId = c.getLong(ID_PLAYLISTS_COLUMN_INDEX); + long lastModified = c.getLong(DATE_MODIFIED_PLAYLISTS_COLUMN_INDEX); + + String key = path; + if (mCaseInsensitivePaths) { + key = path.toLowerCase(); + } + mFileCache.put(key, new FileCacheEntry(mPlaylistsUri, rowId, path, + lastModified)); + } + } + } finally { + c.close(); + c = null; + } + } + } + } + finally { + if (c != null) { + c.close(); + } + } + } + + private boolean inScanDirectory(String path, String[] directories) { + for (int i = 0; i < directories.length; i++) { + if (path.startsWith(directories[i])) { + return true; + } + } + return false; + } + + private void pruneDeadThumbnailFiles() { + HashSet<String> existingFiles = new HashSet<String>(); + String directory = "/sdcard/DCIM/.thumbnails"; + String [] files = (new File(directory)).list(); + if (files == null) + files = new String[0]; + + for (int i = 0; i < files.length; i++) { + String fullPathString = directory + "/" + files[i]; + existingFiles.add(fullPathString); + } + + try { + Cursor c = mMediaProvider.query( + mThumbsUri, + new String [] { "_data" }, + null, + null, + null); + Log.v(TAG, "pruneDeadThumbnailFiles... " + c); + if (c != null && c.moveToFirst()) { + do { + String fullPathString = c.getString(0); + existingFiles.remove(fullPathString); + } while (c.moveToNext()); + } + + for (String fileToDelete : existingFiles) { + if (Config.LOGV) + Log.v(TAG, "fileToDelete is " + fileToDelete); + try { + (new File(fileToDelete)).delete(); + } catch (SecurityException ex) { + } + } + + Log.v(TAG, "/pruneDeadThumbnailFiles... " + c); + if (c != null) { + c.close(); + } + } catch (RemoteException e) { + // We will soon be killed... + } + } + + private void postscan(String[] directories) throws RemoteException { + Iterator<FileCacheEntry> iterator = mFileCache.values().iterator(); + + while (iterator.hasNext()) { + FileCacheEntry entry = iterator.next(); + String path = entry.mPath; + + // remove database entries for files that no longer exist. + boolean fileMissing = false; + + if (!entry.mSeenInFileSystem) { + if (inScanDirectory(path, directories)) { + // we didn't see this file in the scan directory. + fileMissing = true; + } else { + // the file is outside of our scan directory, + // so we need to check for file existence here. + File testFile = new File(path); + if (!testFile.exists()) { + fileMissing = true; + } + } + } + + if (fileMissing) { + // do not delete missing playlists, since they may have been modified by the user. + // the user can delete them in the media player instead. + // instead, clear the path and lastModified fields in the row + MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); + int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); + + if (MediaFile.isPlayListFileType(fileType)) { + ContentValues values = new ContentValues(); + values.put(MediaStore.Audio.Playlists.DATA, ""); + values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, 0); + mMediaProvider.update(ContentUris.withAppendedId(mPlaylistsUri, entry.mRowId), values, null, null); + } else { + mMediaProvider.delete(ContentUris.withAppendedId(entry.mTableUri, entry.mRowId), null, null); + iterator.remove(); + } + } + } + + // handle playlists last, after we know what media files are on the storage. + if (mProcessPlaylists) { + processPlayLists(); + } + + if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external"))) + pruneDeadThumbnailFiles(); + + // allow GC to clean up + mGenreCache = null; + mPlayLists = null; + mFileCache = null; + mMediaProvider = null; + } + + private void initialize(String volumeName) { + mMediaProvider = mContext.getContentResolver().acquireProvider("media"); + + mAudioUri = Audio.Media.getContentUri(volumeName); + mVideoUri = Video.Media.getContentUri(volumeName); + mImagesUri = Images.Media.getContentUri(volumeName); + mThumbsUri = Images.Thumbnails.getContentUri(volumeName); + + if (!volumeName.equals("internal")) { + // we only support playlists on external media + mProcessPlaylists = true; + mProcessGenres = true; + mGenreCache = new HashMap<String, Uri>(); + mGenresUri = Genres.getContentUri(volumeName); + mPlaylistsUri = Playlists.getContentUri(volumeName); + // assuming external storage is FAT (case insensitive), except on the simulator. + if ( Process.supportsProcesses()) { + mCaseInsensitivePaths = true; + } + } + } + + public void scanDirectories(String[] directories, String volumeName) { + try { + long start = System.currentTimeMillis(); + initialize(volumeName); + prescan(null); + long prescan = System.currentTimeMillis(); + + for (int i = 0; i < directories.length; i++) { + processDirectory(directories[i], MediaFile.sFileExtensions, mClient); + } + long scan = System.currentTimeMillis(); + postscan(directories); + long end = System.currentTimeMillis(); + + if (Config.LOGD) { + Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n"); + Log.d(TAG, " scan time: " + (scan - prescan) + "ms\n"); + Log.d(TAG, "postscan time: " + (end - scan) + "ms\n"); + Log.d(TAG, " total time: " + (end - start) + "ms\n"); + } + } catch (SQLException e) { + // this might happen if the SD card is removed while the media scanner is running + Log.e(TAG, "SQLException in MediaScanner.scan()", e); + } catch (UnsupportedOperationException e) { + // this might happen if the SD card is removed while the media scanner is running + Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException in MediaScanner.scan()", e); + } + } + + // this function is used to scan a single file + public Uri scanSingleFile(String path, String volumeName, String mimeType) { + try { + initialize(volumeName); + prescan(path); + + File file = new File(path); + // always scan the file, so we can return the content://media Uri for existing files + return mClient.doScanFile(path, mimeType, file.lastModified(), file.length(), true); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); + return null; + } + } + + // returns the number of matching file/directory names, starting from the right + private int matchPaths(String path1, String path2) { + int result = 0; + int end1 = path1.length(); + int end2 = path2.length(); + + while (end1 > 0 && end2 > 0) { + int slash1 = path1.lastIndexOf('/', end1 - 1); + int slash2 = path2.lastIndexOf('/', end2 - 1); + int backSlash1 = path1.lastIndexOf('\\', end1 - 1); + int backSlash2 = path2.lastIndexOf('\\', end2 - 1); + int start1 = (slash1 > backSlash1 ? slash1 : backSlash1); + int start2 = (slash2 > backSlash2 ? slash2 : backSlash2); + if (start1 < 0) start1 = 0; else start1++; + if (start2 < 0) start2 = 0; else start2++; + int length = end1 - start1; + if (end2 - start2 != length) break; + if (path1.regionMatches(true, start1, path2, start2, length)) { + result++; + end1 = start1 - 1; + end2 = start2 - 1; + } else break; + } + + return result; + } + + private boolean addPlayListEntry(String entry, String playListDirectory, + Uri uri, ContentValues values, int index) { + + // watch for trailing whitespace + int entryLength = entry.length(); + while (entryLength > 0 && Character.isWhitespace(entry.charAt(entryLength - 1))) entryLength--; + // path should be longer than 3 characters. + // avoid index out of bounds errors below by returning here. + if (entryLength < 3) return false; + if (entryLength < entry.length()) entry = entry.substring(0, entryLength); + + // does entry appear to be an absolute path? + // look for Unix or DOS absolute paths + char ch1 = entry.charAt(0); + boolean fullPath = (ch1 == '/' || + (Character.isLetter(ch1) && entry.charAt(1) == ':' && entry.charAt(2) == '\\')); + // if we have a relative path, combine entry with playListDirectory + if (!fullPath) + entry = playListDirectory + entry; + + //FIXME - should we look for "../" within the path? + + // best matching MediaFile for the play list entry + FileCacheEntry bestMatch = null; + + // number of rightmost file/directory names for bestMatch + int bestMatchLength = 0; + + Iterator<FileCacheEntry> iterator = mFileCache.values().iterator(); + while (iterator.hasNext()) { + FileCacheEntry cacheEntry = iterator.next(); + String path = cacheEntry.mPath; + + if (path.equalsIgnoreCase(entry)) { + bestMatch = cacheEntry; + break; // don't bother continuing search + } + + int matchLength = matchPaths(path, entry); + if (matchLength > bestMatchLength) { + bestMatch = cacheEntry; + bestMatchLength = matchLength; + } + } + + if (bestMatch == null) { + return false; + } + + try { + // OK, now we need to add this to the database + values.clear(); + values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index)); + values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(bestMatch.mRowId)); + mMediaProvider.insert(uri, values); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException in MediaScanner.addPlayListEntry()", e); + return false; + } + + return true; + } + + private void processM3uPlayList(String path, String playListDirectory, Uri uri, ContentValues values) { + BufferedReader reader = null; + try { + File f = new File(path); + if (f.exists()) { + reader = new BufferedReader( + new InputStreamReader(new FileInputStream(f)), 8192); + String line = reader.readLine(); + int index = 0; + while (line != null) { + // ignore comment lines, which begin with '#' + if (line.length() > 0 && line.charAt(0) != '#') { + values.clear(); + if (addPlayListEntry(line, playListDirectory, uri, values, index)) + index++; + } + line = reader.readLine(); + } + } + } catch (IOException e) { + Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); + } finally { + try { + if (reader != null) + reader.close(); + } catch (IOException e) { + Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); + } + } + } + + private void processPlsPlayList(String path, String playListDirectory, Uri uri, ContentValues values) { + BufferedReader reader = null; + try { + File f = new File(path); + if (f.exists()) { + reader = new BufferedReader( + new InputStreamReader(new FileInputStream(f)), 8192); + String line = reader.readLine(); + int index = 0; + while (line != null) { + // ignore comment lines, which begin with '#' + if (line.startsWith("File")) { + int equals = line.indexOf('='); + if (equals > 0) { + values.clear(); + if (addPlayListEntry(line.substring(equals + 1), playListDirectory, uri, values, index)) + index++; + } + } + line = reader.readLine(); + } + } + } catch (IOException e) { + Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); + } finally { + try { + if (reader != null) + reader.close(); + } catch (IOException e) { + Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); + } + } + } + + class WplHandler implements ElementListener { + + final ContentHandler handler; + String playListDirectory; + Uri uri; + ContentValues values = new ContentValues(); + int index = 0; + + public WplHandler(String playListDirectory, Uri uri) { + this.playListDirectory = playListDirectory; + this.uri = uri; + + RootElement root = new RootElement("smil"); + Element body = root.getChild("body"); + Element seq = body.getChild("seq"); + Element media = seq.getChild("media"); + media.setElementListener(this); + + this.handler = root.getContentHandler(); + } + + public void start(Attributes attributes) { + String path = attributes.getValue("", "src"); + if (path != null) { + values.clear(); + if (addPlayListEntry(path, playListDirectory, uri, values, index)) { + index++; + } + } + } + + public void end() { + } + + ContentHandler getContentHandler() { + return handler; + } + } + + private void processWplPlayList(String path, String playListDirectory, Uri uri) { + FileInputStream fis = null; + try { + File f = new File(path); + if (f.exists()) { + fis = new FileInputStream(f); + + Xml.parse(fis, Xml.findEncodingByName("UTF-8"), new WplHandler(playListDirectory, uri).getContentHandler()); + } + } catch (SAXException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (fis != null) + fis.close(); + } catch (IOException e) { + Log.e(TAG, "IOException in MediaScanner.processWplPlayList()", e); + } + } + } + + private void processPlayLists() throws RemoteException { + Iterator<FileCacheEntry> iterator = mPlayLists.iterator(); + while (iterator.hasNext()) { + FileCacheEntry entry = iterator.next(); + String path = entry.mPath; + + // only process playlist files if they are new or have been modified since the last scan + if (entry.mLastModifiedChanged) { + ContentValues values = new ContentValues(); + int lastSlash = path.lastIndexOf('/'); + if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path); + Uri uri, membersUri; + long rowId = entry.mRowId; + if (rowId == 0) { + // Create a new playlist + + int lastDot = path.lastIndexOf('.'); + String name = (lastDot < 0 ? path.substring(lastSlash + 1) : path.substring(lastSlash + 1, lastDot)); + values.put(MediaStore.Audio.Playlists.NAME, name); + values.put(MediaStore.Audio.Playlists.DATA, path); + values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified); + uri = mMediaProvider.insert(mPlaylistsUri, values); + rowId = ContentUris.parseId(uri); + membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); + } else { + uri = ContentUris.withAppendedId(mPlaylistsUri, rowId); + + // update lastModified value of existing playlist + values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified); + mMediaProvider.update(uri, values, null, null); + + // delete members of existing playlist + membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); + mMediaProvider.delete(membersUri, null, null); + } + + String playListDirectory = path.substring(0, lastSlash + 1); + MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); + int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); + + if (fileType == MediaFile.FILE_TYPE_M3U) + processM3uPlayList(path, playListDirectory, membersUri, values); + else if (fileType == MediaFile.FILE_TYPE_PLS) + processPlsPlayList(path, playListDirectory, membersUri, values); + else if (fileType == MediaFile.FILE_TYPE_WPL) + processWplPlayList(path, playListDirectory, membersUri); + + Cursor cursor = mMediaProvider.query(membersUri, PLAYLIST_MEMBERS_PROJECTION, null, + null, null); + try { + if (cursor == null || cursor.getCount() == 0) { + Log.d(TAG, "playlist is empty - deleting"); + mMediaProvider.delete(uri, null, null); + } + } finally { + if (cursor != null) cursor.close(); + } + } + } + } + + private native void processDirectory(String path, String extensions, MediaScannerClient client); + private native void processFile(String path, String mimeType, MediaScannerClient client); + public native void setLocale(String locale); + + public native byte[] extractAlbumArt(FileDescriptor fd); + + private native final void native_setup(); + private native final void native_finalize(); + @Override + protected void finalize() { + mContext.getContentResolver().releaseProvider(mMediaProvider); + native_finalize(); + } +} diff --git a/media/java/android/media/MediaScannerClient.java b/media/java/android/media/MediaScannerClient.java new file mode 100644 index 0000000..cf1a8da --- /dev/null +++ b/media/java/android/media/MediaScannerClient.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2007 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; + +/** + * {@hide} + */ +public interface MediaScannerClient +{ + public void scanFile(String path, long lastModified, long fileSize); + + public void scanFile(String path, String mimeType, long lastModified, long fileSize); + + /** + * Called by native code to return metadata extracted from media files. + */ + public void handleStringTag(String name, String value); + + /** + * Called by native code to return mime type extracted from DRM content. + */ + public void setMimeType(String mimeType); +} diff --git a/media/java/android/media/MediaScannerConnection.java b/media/java/android/media/MediaScannerConnection.java new file mode 100644 index 0000000..d2596b8 --- /dev/null +++ b/media/java/android/media/MediaScannerConnection.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2008 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 android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.media.IMediaScannerListener; +import android.media.IMediaScannerService; +import android.net.Uri; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Config; +import android.util.Log; + + +/** + * MediaScannerConnection provides a way for applications to pass a + * newly created or downloaded media file to the media scanner service. + * The media scanner service will read metadata from the file and add + * the file to the media content provider. + * The MediaScannerConnectionClient provides an interface for the + * media scanner service to return the Uri for a newly scanned file + * to the client of the MediaScannerConnection class. + */ +public class MediaScannerConnection implements ServiceConnection { + + private static final String TAG = "MediaScannerConnection"; + + private Context mContext; + private MediaScannerConnectionClient mClient; + private IMediaScannerService mService; + private boolean mConnected; // true if connect() has been called since last disconnect() + + private IMediaScannerListener.Stub mListener = new IMediaScannerListener.Stub() { + public void scanCompleted(String path, Uri uri) { + MediaScannerConnectionClient client = mClient; + if (client != null) { + client.onScanCompleted(path, uri); + } + } + }; + + /** + * An interface for notifying clients of MediaScannerConnection + * when a connection to the MediaScanner service has been established + * and when the scanning of a file has completed. + */ + public interface MediaScannerConnectionClient { + /** + * Called to notify the client when a connection to the + * MediaScanner service has been established. + */ + public void onMediaScannerConnected(); + + /** + * Called to notify the client when the media scanner has finished + * scanning a file. + * @param path the path to the file that has been scanned. + * @param uri the Uri for the file if the scanning operation succeeded + * and the file was added to the media database, or null if scanning failed. + */ + public void onScanCompleted(String path, Uri uri); + } + + /** + * Constructs a new MediaScannerConnection object. + * @param context the Context object, required for establishing a connection to + * the media scanner service. + * @param client an optional object implementing the MediaScannerConnectionClient + * interface, for receiving notifications from the media scanner. + */ + public MediaScannerConnection(Context context, MediaScannerConnectionClient client) { + mContext = context; + mClient = client; + } + + /** + * Initiates a connection to the media scanner service. + * {@link MediaScannerConnectionClient#onMediaScannerConnected()} + * will be called when the connection is established. + */ + public void connect() { + synchronized (this) { + if (!mConnected) { + Intent intent = new Intent(IMediaScannerService.class.getName()); + mContext.bindService(intent, this, Context.BIND_AUTO_CREATE); + mConnected = true; + } + } + } + + /** + * Releases the connection to the media scanner service. + */ + public void disconnect() { + synchronized (this) { + if (mConnected) { + if (Config.LOGV) { + Log.v(TAG, "Disconnecting from Media Scanner"); + } + try { + mContext.unbindService(this); + } catch (IllegalArgumentException ex) { + if (Config.LOGV) { + Log.v(TAG, "disconnect failed: " + ex); + } + } + mConnected = false; + } + } + } + + /** + * Returns whether we are connected to the media scanner service + * @return true if we are connected, false otherwise + */ + public synchronized boolean isConnected() { + return (mService != null && mConnected); + } + + /** + * Requests the media scanner to scan a file. + * @param path the path to the file to be scanned. + * @param mimeType an optional mimeType for the file. + * If mimeType is null, then the mimeType will be inferred from the file extension. + * Success or failure of the scanning operation cannot be determined until + * {@link MediaScannerConnectionClient#onScanCompleted(String, Uri)} is called. + */ + public void scanFile(String path, String mimeType) { + synchronized (this) { + if (mService == null || !mConnected) { + throw new IllegalStateException("not connected to MediaScannerService"); + } + try { + if (Config.LOGV) { + Log.v(TAG, "Scanning file " + path); + } + mService.requestScanFile(path, mimeType, mListener); + } catch (RemoteException e) { + if (Config.LOGD) { + Log.d(TAG, "Failed to scan file " + path); + } + } + } + } + + /** + * Part of the ServiceConnection interface. Do not call. + */ + public void onServiceConnected(ComponentName className, IBinder service) { + if (Config.LOGV) { + Log.v(TAG, "Connected to Media Scanner"); + } + synchronized (this) { + mService = IMediaScannerService.Stub.asInterface(service); + if (mService != null && mClient != null) { + mClient.onMediaScannerConnected(); + } + } + } + + /** + * Part of the ServiceConnection interface. Do not call. + */ + public void onServiceDisconnected(ComponentName className) { + if (Config.LOGV) { + Log.v(TAG, "Disconnected from Media Scanner"); + } + synchronized (this) { + mService = null; + } + } +} diff --git a/media/java/android/media/ResampleInputStream.java b/media/java/android/media/ResampleInputStream.java new file mode 100644 index 0000000..e26eae5 --- /dev/null +++ b/media/java/android/media/ResampleInputStream.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2008 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 android.util.Config; +import android.util.Log; + +import java.io.InputStream; +import java.io.IOException; + + +/** + * ResampleInputStream + * @hide + */ +public final class ResampleInputStream extends InputStream +{ + static { + System.loadLibrary("media_jni"); + } + + private final static String TAG = "ResampleInputStream"; + + // pcm input stream + private InputStream mInputStream; + + // sample rates, assumed to be normalized + private final int mRateIn; + private final int mRateOut; + + // input pcm data + private byte[] mBuf; + private int mBufCount; + + // length of 2:1 fir + private static final int mFirLength = 29; + + // helper for bytewise read() + private final byte[] mOneByte = new byte[1]; + + /** + * Create a new ResampleInputStream, which converts the sample rate + * @param inputStream InputStream containing 16 bit PCM. + * @param rateIn the input sample rate. + * @param rateOut the output sample rate. + * This only handles rateIn == rateOut / 2 for the moment. + */ + public ResampleInputStream(InputStream inputStream, int rateIn, int rateOut) { + // only support 2:1 at the moment + if (rateIn != 2 * rateOut) throw new IllegalArgumentException("only support 2:1 at the moment"); + rateIn = 2; + rateOut = 1; + + mInputStream = inputStream; + mRateIn = rateIn; + mRateOut = rateOut; + } + + @Override + public int read() throws IOException { + int rtn = read(mOneByte, 0, 1); + return rtn == 1 ? (0xff & mOneByte[0]) : -1; + } + + @Override + public int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } + + @Override + public int read(byte[] b, int offset, int length) throws IOException { + if (mInputStream == null) throw new IllegalStateException("not open"); + + // ensure that mBuf is big enough to cover requested 'length' + int nIn = ((length / 2) * mRateIn / mRateOut + mFirLength) * 2; + if (mBuf == null) { + mBuf = new byte[nIn]; + } else if (nIn > mBuf.length) { + byte[] bf = new byte[nIn]; + System.arraycopy(mBuf, 0, bf, 0, mBufCount); + mBuf = bf; + } + + // read until we have enough data for at least one output sample + while (true) { + int len = ((mBufCount / 2 - mFirLength) * mRateOut / mRateIn) * 2; + if (len > 0) { + length = len < length ? len : (length / 2) * 2; + break; + } + // TODO: should mBuf.length below be nIn instead? + int n = mInputStream.read(mBuf, mBufCount, mBuf.length - mBufCount); + if (n == -1) return -1; + mBufCount += n; + } + + // resample input data + fir21(mBuf, 0, b, offset, length / 2); + + // move any unused bytes to front of mBuf + int nFwd = length * mRateIn / mRateOut; + mBufCount -= nFwd; + if (mBufCount > 0) System.arraycopy(mBuf, nFwd, mBuf, 0, mBufCount); + + return length; + } + +/* + @Override + public int available() throws IOException { + int nsamples = (mIn - mOut + mInputStream.available()) / 2; + return ((nsamples - mFirLength) * mRateOut / mRateIn) * 2; + } +*/ + + @Override + public void close() throws IOException { + try { + if (mInputStream != null) mInputStream.close(); + } finally { + mInputStream = null; + } + } + + @Override + protected void finalize() throws Throwable { + if (mInputStream != null) { + close(); + throw new IllegalStateException("someone forgot to close ResampleInputStream"); + } + } + + // + // fir filter code JNI interface + // + private static native void fir21(byte[] in, int inOffset, + byte[] out, int outOffset, int npoints); + +} diff --git a/media/java/android/media/Ringtone.java b/media/java/android/media/Ringtone.java new file mode 100644 index 0000000..e80d8aa --- /dev/null +++ b/media/java/android/media/Ringtone.java @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2006 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 android.content.ContentResolver; +import android.content.Context; +import android.content.res.AssetFileDescriptor; +import android.database.Cursor; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.net.Uri; +import android.provider.DrmStore; +import android.provider.MediaStore; +import android.provider.Settings; +import android.util.Log; + +import java.io.FileDescriptor; +import java.io.IOException; + +/** + * Ringtone provides a quick method for playing a ringtone, notification, or + * other similar types of sounds. + * <p> + * For ways of retrieving {@link Ringtone} objects or to show a ringtone + * picker, see {@link RingtoneManager}. + * + * @see RingtoneManager + */ +public class Ringtone { + private static String TAG = "Ringtone"; + + private static final String[] MEDIA_COLUMNS = new String[] { + MediaStore.Audio.Media._ID, + MediaStore.Audio.Media.DATA, + MediaStore.Audio.Media.TITLE + }; + + private static final String[] DRM_COLUMNS = new String[] { + DrmStore.Audio._ID, + DrmStore.Audio.DATA, + DrmStore.Audio.TITLE + }; + + private MediaPlayer mAudio; + + private Uri mUri; + private String mTitle; + private FileDescriptor mFileDescriptor; + private AssetFileDescriptor mAssetFileDescriptor; + + private int mStreamType = AudioManager.STREAM_RING; + + private Context mContext; + + Ringtone(Context context) { + mContext = context; + } + + /** + * Sets the stream type where this ringtone will be played. + * + * @param streamType The stream, see {@link AudioManager}. + */ + public void setStreamType(int streamType) { + mStreamType = streamType; + + if (mAudio != null) { + /* + * The stream type has to be set before the media player is + * prepared. Re-initialize it. + */ + try { + openMediaPlayer(); + } catch (IOException e) { + Log.w(TAG, "Couldn't set the stream type", e); + } + } + } + + /** + * Gets the stream type where this ringtone will be played. + * + * @return The stream type, see {@link AudioManager}. + */ + public int getStreamType() { + return mStreamType; + } + + /** + * Returns a human-presentable title for ringtone. Looks in media and DRM + * content providers. If not in either, uses the filename + * + * @param context A context used for querying. + */ + public String getTitle(Context context) { + if (mTitle != null) return mTitle; + return mTitle = getTitle(context, mUri, true); + } + + private static String getTitle(Context context, Uri uri, boolean followSettingsUri) { + Cursor cursor = null; + ContentResolver res = context.getContentResolver(); + + String title = null; + + if (uri != null) { + String authority = uri.getAuthority(); + + if (Settings.AUTHORITY.equals(authority)) { + if (followSettingsUri) { + Uri actualUri = RingtoneManager.getActualDefaultRingtoneUri(context, + RingtoneManager.getDefaultType(uri)); + String actualTitle = getTitle(context, actualUri, false); + title = context + .getString(com.android.internal.R.string.ringtone_default_with_actual, + actualTitle); + } + } else { + + if (DrmStore.AUTHORITY.equals(authority)) { + cursor = res.query(uri, DRM_COLUMNS, null, null, null); + } else if (MediaStore.AUTHORITY.equals(authority)) { + cursor = res.query(uri, MEDIA_COLUMNS, null, null, null); + } + + if (cursor != null && cursor.getCount() == 1) { + cursor.moveToFirst(); + return cursor.getString(2); + } else { + title = uri.getLastPathSegment(); + } + } + } + + if (title == null) { + title = context.getString(com.android.internal.R.string.ringtone_unknown); + + if (title == null) { + title = ""; + } + } + + return title; + } + + private void openMediaPlayer() throws IOException { + mAudio = new MediaPlayer(); + if (mUri != null) { + mAudio.setDataSource(mContext, mUri); + } else if (mFileDescriptor != null) { + mAudio.setDataSource(mFileDescriptor); + } else if (mAssetFileDescriptor != null) { + // Note: using getDeclaredLength so that our behavior is the same + // as previous versions when the content provider is returning + // a full file. + if (mAssetFileDescriptor.getDeclaredLength() < 0) { + mAudio.setDataSource(mAssetFileDescriptor.getFileDescriptor()); + } else { + mAudio.setDataSource(mAssetFileDescriptor.getFileDescriptor(), + mAssetFileDescriptor.getStartOffset(), + mAssetFileDescriptor.getDeclaredLength()); + } + } else { + throw new IOException("No data source set."); + } + mAudio.setAudioStreamType(mStreamType); + mAudio.prepare(); + } + + void open(FileDescriptor fd) throws IOException { + mFileDescriptor = fd; + openMediaPlayer(); + } + + void open(AssetFileDescriptor fd) throws IOException { + mAssetFileDescriptor = fd; + openMediaPlayer(); + } + + void open(Uri uri) throws IOException { + mUri = uri; + openMediaPlayer(); + } + + /** + * Plays the ringtone. + */ + public void play() { + if (mAudio == null) { + try { + openMediaPlayer(); + } catch (Exception ex) { + Log.e(TAG, "play() caught ", ex); + mAudio = null; + } + } + if (mAudio != null) { + mAudio.start(); + } + } + + /** + * Stops a playing ringtone. + */ + public void stop() { + if (mAudio != null) { + mAudio.reset(); + mAudio.release(); + mAudio = null; + } + } + + /** + * Whether this ringtone is currently playing. + * + * @return True if playing, false otherwise. + */ + public boolean isPlaying() { + return mAudio != null && mAudio.isPlaying(); + } + + void setTitle(String title) { + mTitle = title; + } +} diff --git a/media/java/android/media/RingtoneManager.java b/media/java/android/media/RingtoneManager.java new file mode 100644 index 0000000..2f0007f --- /dev/null +++ b/media/java/android/media/RingtoneManager.java @@ -0,0 +1,713 @@ +/* + * Copyright (C) 2007 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 com.android.internal.database.SortCursor; + +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.app.Activity; +import android.content.ContentUris; +import android.content.Context; +import android.content.res.AssetFileDescriptor; +import android.database.Cursor; +import android.net.Uri; +import android.os.Environment; +import android.provider.DrmStore; +import android.provider.MediaStore; +import android.provider.Settings; +import android.provider.Settings.System; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; + +/** + * RingtoneManager provides access to ringtones, notification, and other types + * of sounds. It manages querying the different media providers and combines the + * results into a single cursor. It also provides a {@link Ringtone} for each + * ringtone. We generically call these sounds ringtones, however the + * {@link #TYPE_RINGTONE} refers to the type of sounds that are suitable for the + * phone ringer. + * <p> + * To show a ringtone picker to the user, use the + * {@link #ACTION_RINGTONE_PICKER} intent to launch the picker as a subactivity. + * + * @see Ringtone + */ +public class RingtoneManager { + + private static final String TAG = "RingtoneManager"; + + // Make sure these are in sync with attrs.xml: + // <attr name="ringtoneType"> + + /** + * Type that refers to sounds that are used for the phone ringer. + */ + public static final int TYPE_RINGTONE = 1; + + /** + * Type that refers to sounds that are used for notifications. + */ + public static final int TYPE_NOTIFICATION = 2; + + /** + * Type that refers to sounds that are used for the alarm. + */ + public static final int TYPE_ALARM = 4; + + /** + * All types of sounds. + */ + public static final int TYPE_ALL = TYPE_RINGTONE | TYPE_NOTIFICATION | TYPE_ALARM; + + // </attr> + + /** + * Activity Action: Shows a ringtone picker. + * <p> + * Input: {@link #EXTRA_RINGTONE_EXISTING_URI}, + * {@link #EXTRA_RINGTONE_SHOW_DEFAULT}, + * {@link #EXTRA_RINGTONE_SHOW_SILENT}, {@link #EXTRA_RINGTONE_TYPE}, + * {@link #EXTRA_RINGTONE_DEFAULT_URI}, {@link #EXTRA_RINGTONE_TITLE}, + * {@link #EXTRA_RINGTONE_INCLUDE_DRM}. + * <p> + * Output: {@link #EXTRA_RINGTONE_PICKED_URI}. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_RINGTONE_PICKER = "android.intent.action.RINGTONE_PICKER"; + + /** + * Given to the ringtone picker as a boolean. Whether to show an item for + * "Default". + * + * @see #ACTION_RINGTONE_PICKER + */ + public static final String EXTRA_RINGTONE_SHOW_DEFAULT = + "android.intent.extra.ringtone.SHOW_DEFAULT"; + + /** + * Given to the ringtone picker as a boolean. Whether to show an item for + * "Silent". If the "Silent" item is picked, + * {@link #EXTRA_RINGTONE_PICKED_URI} will be null. + * + * @see #ACTION_RINGTONE_PICKER + */ + public static final String EXTRA_RINGTONE_SHOW_SILENT = + "android.intent.extra.ringtone.SHOW_SILENT"; + + /** + * Given to the ringtone picker as a boolean. Whether to include DRM ringtones. + */ + public static final String EXTRA_RINGTONE_INCLUDE_DRM = + "android.intent.extra.ringtone.INCLUDE_DRM"; + + /** + * Given to the ringtone picker as a {@link Uri}. The {@link Uri} of the + * current ringtone, which will be used to show a checkmark next to the item + * for this {@link Uri}. If showing an item for "Default" (@see + * {@link #EXTRA_RINGTONE_SHOW_DEFAULT}), this can also be one of + * {@link System#DEFAULT_RINGTONE_URI} or + * {@link System#DEFAULT_NOTIFICATION_URI} to have the "Default" item + * checked. + * + * @see #ACTION_RINGTONE_PICKER + */ + public static final String EXTRA_RINGTONE_EXISTING_URI = + "android.intent.extra.ringtone.EXISTING_URI"; + + /** + * Given to the ringtone picker as a {@link Uri}. The {@link Uri} of the + * ringtone to play when the user attempts to preview the "Default" + * ringtone. This can be one of {@link System#DEFAULT_RINGTONE_URI} or + * {@link System#DEFAULT_NOTIFICATION_URI} to have the "Default" point to + * the current sound for the given default sound type. If you are showing a + * ringtone picker for some other type of sound, you are free to provide any + * {@link Uri} here. + */ + public static final String EXTRA_RINGTONE_DEFAULT_URI = + "android.intent.extra.ringtone.DEFAULT_URI"; + + /** + * Given to the ringtone picker as an int. Specifies which ringtone type(s) should be + * shown in the picker. One or more of {@link #TYPE_RINGTONE}, + * {@link #TYPE_NOTIFICATION}, {@link #TYPE_ALARM}, or {@link #TYPE_ALL} + * (bitwise-ored together). + */ + public static final String EXTRA_RINGTONE_TYPE = "android.intent.extra.ringtone.TYPE"; + + /** + * Given to the ringtone picker as a {@link CharSequence}. The title to + * show for the ringtone picker. This has a default value that is suitable + * in most cases. + */ + public static final String EXTRA_RINGTONE_TITLE = "android.intent.extra.ringtone.TITLE"; + + /** + * Returned from the ringtone picker as a {@link Uri}. + * <p> + * It will be one of: + * <li> the picked ringtone, + * <li> a {@link Uri} that equals {@link System#DEFAULT_RINGTONE_URI} or + * {@link System#DEFAULT_NOTIFICATION_URI} if the default was chosen, + * <li> null if the "Silent" item was picked. + * + * @see #ACTION_RINGTONE_PICKER + */ + public static final String EXTRA_RINGTONE_PICKED_URI = + "android.intent.extra.ringtone.PICKED_URI"; + + // Make sure the column ordering and then ..._COLUMN_INDEX are in sync + + private static final String[] INTERNAL_COLUMNS = new String[] { + MediaStore.Audio.Media._ID, MediaStore.Audio.Media.TITLE, + "\"" + MediaStore.Audio.Media.INTERNAL_CONTENT_URI + "\"" + }; + + private static final String[] DRM_COLUMNS = new String[] { + DrmStore.Audio._ID, DrmStore.Audio.TITLE, + "\"" + DrmStore.Audio.CONTENT_URI + "\"" + }; + + private static final String[] MEDIA_COLUMNS = new String[] { + MediaStore.Audio.Media._ID, MediaStore.Audio.Media.TITLE, + "\"" + MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "\"" + }; + + /** + * The column index (in the cursor returned by {@link #getCursor()} for the + * row ID. + */ + public static final int ID_COLUMN_INDEX = 0; + + /** + * The column index (in the cursor returned by {@link #getCursor()} for the + * title. + */ + public static final int TITLE_COLUMN_INDEX = 1; + + /** + * The column index (in the cursor returned by {@link #getCursor()} for the + * media provider's URI. + */ + public static final int URI_COLUMN_INDEX = 2; + + private Activity mActivity; + private Context mContext; + + private Cursor mCursor; + + private int mType = TYPE_RINGTONE; + + /** + * If a column (item from this list) exists in the Cursor, its value must + * be true (value of 1) for the row to be returned. + */ + private List<String> mFilterColumns = new ArrayList<String>(); + + private boolean mStopPreviousRingtone = true; + private Ringtone mPreviousRingtone; + + private boolean mIncludeDrm; + + /** + * Constructs a RingtoneManager. This constructor is recommended as its + * constructed instance manages cursor(s). + * + * @param activity The activity used to get a managed cursor. + */ + public RingtoneManager(Activity activity) { + mContext = mActivity = activity; + setType(mType); + } + + /** + * Constructs a RingtoneManager. The instance constructed by this + * constructor will not manage the cursor(s), so the client should handle + * this itself. + * + * @param context The context to used to get a cursor. + */ + public RingtoneManager(Context context) { + mContext = context; + setType(mType); + } + + /** + * Sets which type(s) of ringtones will be listed by this. + * + * @param type The type(s), one or more of {@link #TYPE_RINGTONE}, + * {@link #TYPE_NOTIFICATION}, {@link #TYPE_ALARM}, + * {@link #TYPE_ALL}. + * @see #EXTRA_RINGTONE_TYPE + */ + public void setType(int type) { + + if (mCursor != null) { + throw new IllegalStateException( + "Setting filter columns should be done before querying for ringtones."); + } + + mType = type; + setFilterColumnsList(type); + } + + /** + * Infers the playback stream type based on what type of ringtones this + * manager is returning. + * + * @return The stream type. + * @hide Pending API Council approval + */ + public int inferStreamType() { + switch (mType) { + + case TYPE_ALARM: + return AudioManager.STREAM_ALARM; + + case TYPE_NOTIFICATION: + return AudioManager.STREAM_NOTIFICATION; + + default: + return AudioManager.STREAM_RING; + } + } + + /** + * Whether retrieving another {@link Ringtone} will stop playing the + * previously retrieved {@link Ringtone}. + * <p> + * If this is false, make sure to {@link Ringtone#stop()} any previous + * ringtones to free resources. + * + * @param stopPreviousRingtone If true, the previously retrieved + * {@link Ringtone} will be stopped. + */ + public void setStopPreviousRingtone(boolean stopPreviousRingtone) { + mStopPreviousRingtone = stopPreviousRingtone; + } + + /** + * @see #setStopPreviousRingtone(boolean) + */ + public boolean getStopPreviousRingtone() { + return mStopPreviousRingtone; + } + + /** + * Stops playing the last {@link Ringtone} retrieved from this. + */ + public void stopPreviousRingtone() { + if (mPreviousRingtone != null) { + mPreviousRingtone.stop(); + } + } + + /** + * Returns whether DRM ringtones will be included. + * + * @return Whether DRM ringtones will be included. + * @see #setIncludeDrm(boolean) + */ + public boolean getIncludeDrm() { + return mIncludeDrm; + } + + /** + * Sets whether to include DRM ringtones. + * + * @param includeDrm Whether to include DRM ringtones. + */ + public void setIncludeDrm(boolean includeDrm) { + mIncludeDrm = includeDrm; + } + + /** + * Returns a {@link Cursor} of all the ringtones available. The returned + * cursor will be the same cursor returned each time this method is called, + * so do not {@link Cursor#close()} the cursor. The cursor can be + * {@link Cursor#deactivate()} safely. + * <p> + * If {@link RingtoneManager#RingtoneManager(Activity)} was not used, the + * caller should manage the returned cursor through its activity's life + * cycle to prevent leaking the cursor. + * + * @return A {@link Cursor} of all the ringtones available. + * @see #ID_COLUMN_INDEX + * @see #TITLE_COLUMN_INDEX + * @see #URI_COLUMN_INDEX + */ + public Cursor getCursor() { + if (mCursor != null && mCursor.requery()) { + return mCursor; + } + + final Cursor internalCursor = getInternalRingtones(); + final Cursor drmCursor = mIncludeDrm ? getDrmRingtones() : null; + final Cursor mediaCursor = getMediaRingtones(); + + return mCursor = new SortCursor(new Cursor[] { internalCursor, drmCursor, mediaCursor }, + MediaStore.MediaColumns.TITLE); + } + + /** + * Gets a {@link Ringtone} for the ringtone at the given position in the + * {@link Cursor}. + * + * @param position The position (in the {@link Cursor}) of the ringtone. + * @return A {@link Ringtone} pointing to the ringtone. + */ + public Ringtone getRingtone(int position) { + if (mStopPreviousRingtone && mPreviousRingtone != null) { + mPreviousRingtone.stop(); + } + + mPreviousRingtone = getRingtone(mContext, getRingtoneUri(position), inferStreamType()); + return mPreviousRingtone; + } + + /** + * Gets a {@link Uri} for the ringtone at the given position in the {@link Cursor}. + * + * @param position The position (in the {@link Cursor}) of the ringtone. + * @return A {@link Uri} pointing to the ringtone. + */ + public Uri getRingtoneUri(int position) { + final Cursor cursor = getCursor(); + + if (!cursor.moveToPosition(position)) { + return null; + } + + return getUriFromCursor(cursor); + } + + private static Uri getUriFromCursor(Cursor cursor) { + return ContentUris.withAppendedId(Uri.parse(cursor.getString(URI_COLUMN_INDEX)), cursor + .getLong(ID_COLUMN_INDEX)); + } + + /** + * Gets the position of a {@link Uri} within this {@link RingtoneManager}. + * + * @param ringtoneUri The {@link Uri} to retreive the position of. + * @return The position of the {@link Uri}, or -1 if it cannot be found. + */ + public int getRingtonePosition(Uri ringtoneUri) { + + if (ringtoneUri == null) return -1; + + final Cursor cursor = getCursor(); + final int cursorCount = cursor.getCount(); + + if (!cursor.moveToFirst()) { + return -1; + } + + // Only create Uri objects when the actual URI changes + Uri currentUri = null; + String previousUriString = null; + for (int i = 0; i < cursorCount; i++) { + String uriString = cursor.getString(URI_COLUMN_INDEX); + if (currentUri == null || !uriString.equals(previousUriString)) { + currentUri = Uri.parse(uriString); + } + + if (ringtoneUri.equals(ContentUris.withAppendedId(currentUri, cursor + .getLong(ID_COLUMN_INDEX)))) { + return i; + } + + cursor.move(1); + + previousUriString = uriString; + } + + return -1; + } + + /** + * Returns a valid ringtone URI. No guarantees on which it returns. If it + * cannot find one, returns null. + * + * @param context The context to use for querying. + * @return A ringtone URI, or null if one cannot be found. + */ + public static Uri getValidRingtoneUri(Context context) { + final RingtoneManager rm = new RingtoneManager(context); + + Uri uri = getValidRingtoneUriFromCursorAndClose(context, rm.getInternalRingtones()); + + if (uri == null) { + uri = getValidRingtoneUriFromCursorAndClose(context, rm.getMediaRingtones()); + } + + if (uri == null) { + uri = getValidRingtoneUriFromCursorAndClose(context, rm.getDrmRingtones()); + } + + return uri; + } + + private static Uri getValidRingtoneUriFromCursorAndClose(Context context, Cursor cursor) { + if (cursor != null) { + Uri uri = null; + + if (cursor.moveToFirst()) { + uri = getUriFromCursor(cursor); + } + cursor.close(); + + return uri; + } else { + return null; + } + } + + private Cursor getInternalRingtones() { + return query( + MediaStore.Audio.Media.INTERNAL_CONTENT_URI, INTERNAL_COLUMNS, + constructBooleanTrueWhereClause(mFilterColumns), + null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER); + } + + private Cursor getDrmRingtones() { + // DRM store does not have any columns to use for filtering + return query( + DrmStore.Audio.CONTENT_URI, DRM_COLUMNS, + null, null, DrmStore.Audio.TITLE); + } + + private Cursor getMediaRingtones() { + // Get the external media cursor. First check to see if it is mounted. + final String status = Environment.getExternalStorageState(); + + return (status.equals(Environment.MEDIA_MOUNTED) || + status.equals(Environment.MEDIA_MOUNTED_READ_ONLY)) + ? query( + MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MEDIA_COLUMNS, + constructBooleanTrueWhereClause(mFilterColumns), null, + MediaStore.Audio.Media.DEFAULT_SORT_ORDER) + : null; + } + + private void setFilterColumnsList(int type) { + List<String> columns = mFilterColumns; + columns.clear(); + + if ((type & TYPE_RINGTONE) != 0) { + columns.add(MediaStore.Audio.AudioColumns.IS_RINGTONE); + } + + if ((type & TYPE_NOTIFICATION) != 0) { + columns.add(MediaStore.Audio.AudioColumns.IS_NOTIFICATION); + } + + if ((type & TYPE_ALARM) != 0) { + columns.add(MediaStore.Audio.AudioColumns.IS_ALARM); + } + } + + /** + * Constructs a where clause that consists of at least one column being 1 + * (true). This is used to find all matching sounds for the given sound + * types (ringtone, notifications, etc.) + * + * @param columns The columns that must be true. + * @return The where clause. + */ + private static String constructBooleanTrueWhereClause(List<String> columns) { + + if (columns == null) return null; + + StringBuilder sb = new StringBuilder(); + for (int i = columns.size() - 1; i >= 0; i--) { + sb.append(columns.get(i)).append("=1 or "); + } + + if (columns.size() > 0) { + // Remove last ' or ' + sb.setLength(sb.length() - 4); + } + + return sb.toString(); + } + + private Cursor query(Uri uri, + String[] projection, + String selection, + String[] selectionArgs, + String sortOrder) { + if (mActivity != null) { + return mActivity.managedQuery(uri, projection, selection, selectionArgs, sortOrder); + } else { + return mContext.getContentResolver().query(uri, projection, selection, selectionArgs, + sortOrder); + } + } + + /** + * Returns a {@link Ringtone} for a given sound URI. + * <p> + * If the given URI cannot be opened for any reason, this method will + * attempt to fallback on another sound. If it cannot find any, it will + * return null. + * + * @param context A context used to query. + * @param ringtoneUri The {@link Uri} of a sound or ringtone. + * @return A {@link Ringtone} for the given URI, or null. + */ + public static Ringtone getRingtone(final Context context, Uri ringtoneUri) { + // Don't set the stream type + return getRingtone(context, ringtoneUri, -1); + } + + /** + * Returns a {@link Ringtone} for a given sound URI on the given stream + * type. Normally, if you change the stream type on the returned + * {@link Ringtone}, it will re-create the {@link MediaPlayer}. This is just + * an optimized route to avoid that. + * + * @param streamType The stream type for the ringtone, or -1 if it should + * not be set (and the default used instead). + * @see #getRingtone(Context, Uri) + */ + private static Ringtone getRingtone(final Context context, Uri ringtoneUri, int streamType) { + + try { + Ringtone r = new Ringtone(context); + if (streamType >= 0) { + r.setStreamType(streamType); + } + r.open(ringtoneUri); + return r; + } catch (Exception ex) { + Log.e(TAG, "Failed to open ringtone " + ringtoneUri); + } + + // Ringtone doesn't exist, use the fallback ringtone. + try { + AssetFileDescriptor afd = context.getResources().openRawResourceFd( + com.android.internal.R.raw.fallbackring); + if (afd != null) { + Ringtone r = new Ringtone(context); + r.open(afd); + afd.close(); + return r; + } + } catch (Exception ex) { + } + + // we should never get here + Log.e(TAG, "unable to find a usable ringtone"); + return null; + } + + /** + * Gets the current default sound's {@link Uri}. This will give the actual + * sound {@link Uri}, instead of using this, most clients can use + * {@link System#DEFAULT_RINGTONE_URI}. + * + * @param context A context used for querying. + * @param type The type whose default sound should be returned. One of + * {@link #TYPE_RINGTONE} or {@link #TYPE_NOTIFICATION}. + * @return A {@link Uri} pointing to the default sound for the sound type. + * @see #setActualDefaultRingtoneUri(Context, int, Uri) + */ + public static Uri getActualDefaultRingtoneUri(Context context, int type) { + String setting = getSettingForType(type); + if (setting == null) return null; + final String uriString = Settings.System.getString(context.getContentResolver(), setting); + return uriString != null ? Uri.parse(uriString) : getValidRingtoneUri(context); + } + + /** + * Sets the {@link Uri} of the default sound for a given sound type. + * + * @param context A context used for querying. + * @param type The type whose default sound should be set. One of + * {@link #TYPE_RINGTONE} or {@link #TYPE_NOTIFICATION}. + * @param ringtoneUri A {@link Uri} pointing to the default sound to set. + * @see #getActualDefaultRingtoneUri(Context, int) + */ + public static void setActualDefaultRingtoneUri(Context context, int type, Uri ringtoneUri) { + String setting = getSettingForType(type); + if (setting == null) return; + Settings.System.putString(context.getContentResolver(), setting, ringtoneUri.toString()); + } + + private static String getSettingForType(int type) { + if ((type & TYPE_RINGTONE) != 0) { + return Settings.System.RINGTONE; + } else if ((type & TYPE_NOTIFICATION) != 0) { + return Settings.System.NOTIFICATION_SOUND; + } else { + return null; + } + } + + /** + * Returns whether the given {@link Uri} is one of the default ringtones. + * + * @param ringtoneUri The ringtone {@link Uri} to be checked. + * @return Whether the {@link Uri} is a default. + */ + public static boolean isDefault(Uri ringtoneUri) { + return getDefaultType(ringtoneUri) != -1; + } + + /** + * Returns the type of a default {@link Uri}. + * + * @param defaultRingtoneUri The default {@link Uri}. For example, + * {@link System#DEFAULT_RINGTONE_URI} or + * {@link System#DEFAULT_NOTIFICATION_URI}. + * @return The type of the defaultRingtoneUri, or -1. + */ + public static int getDefaultType(Uri defaultRingtoneUri) { + if (defaultRingtoneUri == null) { + return -1; + } else if (defaultRingtoneUri.equals(Settings.System.DEFAULT_RINGTONE_URI)) { + return TYPE_RINGTONE; + } else if (defaultRingtoneUri.equals(Settings.System.DEFAULT_NOTIFICATION_URI)) { + return TYPE_NOTIFICATION; + } else { + return -1; + } + } + + /** + * Returns the {@link Uri} for the default ringtone of a particular type. + * Rather than returning the actual ringtone's sound {@link Uri}, this will + * return the symbolic {@link Uri} which will resolved to the actual sound + * when played. + * + * @param type The ringtone type whose default should be returned. + * @return The {@link Uri} of the default ringtone for the given type. + */ + public static Uri getDefaultUri(int type) { + if ((type & TYPE_RINGTONE) != 0) { + return Settings.System.DEFAULT_RINGTONE_URI; + } else if ((type & TYPE_NOTIFICATION) != 0) { + return Settings.System.DEFAULT_NOTIFICATION_URI; + } else { + return null; + } + } + +} diff --git a/media/java/android/media/SoundPool.java b/media/java/android/media/SoundPool.java new file mode 100644 index 0000000..000430f --- /dev/null +++ b/media/java/android/media/SoundPool.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2007 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 android.util.AndroidRuntimeException; +import android.util.Log; +import java.io.File; +import java.io.FileDescriptor; +import android.os.ParcelFileDescriptor; +import java.lang.ref.WeakReference; +import android.content.Context; +import android.content.res.AssetFileDescriptor; +import java.io.IOException; + +/* + * The SoundPool class manages and plays audio resources for applications. + */ +public class SoundPool +{ + static { System.loadLibrary("soundpool"); } + + private final static String TAG = "SoundPool"; + + private int mNativeContext; // accessed by native methods + + public SoundPool(int maxStreams, int streamType, int srcQuality) { + native_setup(new WeakReference<SoundPool>(this), maxStreams, streamType, srcQuality); + } + + public int load(String path, int priority) + { + // pass network streams to player + if (path.startsWith("http:")) + return _load(path, priority); + + // try local path + int id = 0; + try { + File f = new File(path); + if (f != null) { + ParcelFileDescriptor fd = ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY); + if (fd != null) { + id = _load(fd.getFileDescriptor(), 0, f.length(), priority); + //Log.v(TAG, "close fd"); + fd.close(); + } + } + } catch (java.io.IOException e) {} + return id; + } + + public int load(Context context, int resId, int priority) { + AssetFileDescriptor afd = context.getResources().openRawResourceFd(resId); + int id = 0; + if (afd != null) { + id = _load(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength(), priority); + try { + //Log.v(TAG, "close fd"); + afd.close(); + } catch (java.io.IOException ex) { + //Log.d(TAG, "close failed:", ex); + } + } + return id; + } + + public int load(AssetFileDescriptor afd, int priority) { + if (afd != null) { + long len = afd.getLength(); + if (len < 0) { + throw new AndroidRuntimeException("no length for fd"); + } + return _load(afd.getFileDescriptor(), afd.getStartOffset(), len, priority); + } else { + return 0; + } + } + + public int load(FileDescriptor fd, long offset, long length, int priority) { + return _load(fd, offset, length, priority); + } + + private native final int _load(String uri, int priority); + + private native final int _load(FileDescriptor fd, long offset, long length, int priority); + + public native final boolean unload(int soundID); + + public native final int play(int soundID, float leftVolume, float rightVolume, + int priority, int loop, float rate); + + public native final void pause(int streamID); + + public native final void resume(int streamID); + + public native final void stop(int streamID); + + public native final void setVolume(int streamID, + float leftVolume, float rightVolume); + + public native final void setPriority(int streamID, int priority); + + public native final void setLoop(int streamID, int loop); + + public native final void setRate(int streamID, float rate); + + public native final void release(); + + private native final void native_setup(Object mediaplayer_this, + int maxStreams, int streamType, int srcQuality); + + protected void finalize() { release(); } +} diff --git a/media/java/android/media/ToneGenerator.java b/media/java/android/media/ToneGenerator.java new file mode 100644 index 0000000..0901fbf --- /dev/null +++ b/media/java/android/media/ToneGenerator.java @@ -0,0 +1,286 @@ +/* + * Copyright (C) 2008 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; + + + +/** + * This class provides methods to play DTMF tones (ITU-T Recommendation Q.23), + * call supervisory tones (3GPP TS 22.001, CEPT) and proprietary tones (3GPP TS 31.111). + * Depending on call state and routing options, tones are mixed to the downlink audio + * or output to the speaker phone or headset. + * This API is not for generating tones over the uplink audio path. + */ +public class ToneGenerator +{ + + /* Values for toneType parameter of ToneGenerator() constructor */ + /* + * List of all available tones: These constants must be kept consistant with + * the enum in ToneGenerator C++ class. */ + + /** + * DTMF tone for key 0: 1336Hz, 941Hz, continuous</p> + * + * @see #ToneGenerator(int, int) + */ + public static final int TONE_DTMF_0 = 0; + /** + * DTMF tone for key 1: 1209Hz, 697Hz, continuous + * + * @see #ToneGenerator(int, int) + */ + public static final int TONE_DTMF_1 = 1; + /** + * DTMF tone for key 2: 1336Hz, 697Hz, continuous + * + * @see #ToneGenerator(int, int) + */ + public static final int TONE_DTMF_2 = 2; + /** + * DTMF tone for key 3: 1477Hz, 697Hz, continuous + * + * @see #ToneGenerator(int, int) + */ + public static final int TONE_DTMF_3 = 3; + /** + * DTMF tone for key 4: 1209Hz, 770Hz, continuous + * + * @see #ToneGenerator(int, int) + */ + public static final int TONE_DTMF_4 = 4; + /** + * DTMF tone for key 5: 1336Hz, 770Hz, continuous + * + * @see #ToneGenerator(int, int) + */ + public static final int TONE_DTMF_5 = 5; + /** + * DTMF tone for key 6: 1477Hz, 770Hz, continuous + * + * @see #ToneGenerator(int, int) + */ + public static final int TONE_DTMF_6 = 6; + /** + * DTMF tone for key 7: 1209Hz, 852Hz, continuous + * + * @see #ToneGenerator(int, int) + */ + public static final int TONE_DTMF_7 = 7; + /** + * DTMF tone for key 8: 1336Hz, 852Hz, continuous + * + * @see #ToneGenerator(int, int) + */ + public static final int TONE_DTMF_8 = 8; + /** + * DTMF tone for key 9: 1477Hz, 852Hz, continuous + * + * @see #ToneGenerator(int, int) + */ + public static final int TONE_DTMF_9 = 9; + /** + * DTMF tone for key *: 1209Hz, 941Hz, continuous + * + * @see #ToneGenerator(int, int) + */ + public static final int TONE_DTMF_S = 10; + /** + * DTMF tone for key #: 1477Hz, 941Hz, continuous + * + * @see #ToneGenerator(int, int) + */ + public static final int TONE_DTMF_P = 11; + /** + * DTMF tone for key A: 1633Hz, 697Hz, continuous + * + * @see #ToneGenerator(int, int) + */ + public static final int TONE_DTMF_A = 12; + /** + * DTMF tone for key B: 1633Hz, 770Hz, continuous + * + * @see #ToneGenerator(int, int) + */ + public static final int TONE_DTMF_B = 13; + /** + * DTMF tone for key C: 1633Hz, 852Hz, continuous + * + * @see #ToneGenerator(int, int) + */ + public static final int TONE_DTMF_C = 14; + /** + * DTMF tone for key D: 1633Hz, 941Hz, continuous + * + * @see #ToneGenerator(int, int) + */ + public static final int TONE_DTMF_D = 15; + /** + * Call supervisory tone, Dial tone: 425Hz, continuous + * + * @see #ToneGenerator(int, int) + */ + public static final int TONE_SUP_DIAL = 16; + /** + * Call supervisory tone, Busy: 425Hz, 500ms ON, 500ms OFF... + * + * @see #ToneGenerator(int, int) + */ + public static final int TONE_SUP_BUSY = 17; + /** + * Call supervisory tone, Congestion: 425Hz, 200ms ON, 200ms OFF... + * + * @see #ToneGenerator(int, int) + */ + public static final int TONE_SUP_CONGESTION = 18; + /** + * Call supervisory tone, Radio path acknowlegment : 425Hz, 200ms ON + * + * @see #ToneGenerator(int, int) + */ + public static final int TONE_SUP_RADIO_ACK = 19; + /** + * Call supervisory tone, Radio path not available: 425Hz, 200ms ON, 200 OFF 3 bursts + * + * @see #ToneGenerator(int, int) + */ + public static final int TONE_SUP_RADIO_NOTAVAIL = 20; + /** + * Call supervisory tone, Error/Special info: 950Hz+1400Hz+1800Hz, 330ms ON, 1s OFF... + * + * @see #ToneGenerator(int, int) + */ + public static final int TONE_SUP_ERROR = 21; + /** + * Call supervisory tone, Call Waiting: 425Hz, 200ms ON, 600ms OFF, 200ms ON, 3s OFF... + * + * @see #ToneGenerator(int, int) + */ + public static final int TONE_SUP_CALL_WAITING = 22; + /** + * Call supervisory tone, Ring Tone: 425Hz, 1s ON, 4s OFF... + * + * @see #ToneGenerator(int, int) + */ + public static final int TONE_SUP_RINGTONE = 23; + /** + * Proprietary tone, general beep: 400Hz+1200Hz, 35ms ON + * + * @see #ToneGenerator(int, int) + */ + public static final int TONE_PROP_BEEP = 24; + /** + * Proprietary tone, positive acknowlegement: 1200Hz, 100ms ON, 100ms OFF 2 bursts + * + * @see #ToneGenerator(int, int) + */ + public static final int TONE_PROP_ACK = 25; + /** + * Proprietary tone, negative acknowlegement: 300Hz+400Hz+500Hz, 400ms ON + * + * @see #ToneGenerator(int, int) + */ + public static final int TONE_PROP_NACK = 26; + /** + * Proprietary tone, prompt tone: 400Hz+1200Hz, 200ms ON + * + * @see #ToneGenerator(int, int) + */ + public static final int TONE_PROP_PROMPT = 27; + /** + * Proprietary tone, general double beep: twice 400Hz+1200Hz, 35ms ON, 200ms OFF, 35ms ON + * + * @see #ToneGenerator(int, int) + */ + public static final int TONE_PROP_BEEP2 = 28; + + /** Maximum volume, for use with {@link #ToneGenerator(int,int)} */ + public static final int MAX_VOLUME = AudioSystem.MAX_VOLUME; + /** Minimum volume setting, for use with {@link #ToneGenerator(int,int)} */ + public static final int MIN_VOLUME = AudioSystem.MIN_VOLUME; + + + /** + * ToneGenerator class contructor specifying output stream type and volume. + * + * @param streamType The streame type used for tone playback (e.g. STREAM_MUSIC). + * @param volume The volume of the tone, given in percentage of maximum volume (from 0-100). + * + */ + public ToneGenerator(int streamType, int volume) { + native_setup(streamType, volume); + } + + /** + * This method starts the playback of a tone of the specified type. + * only one tone can play at a time: if a tone is playing while this method is called, + * this tone is stopped and replaced by the one requested. + * @param toneType The type of tone generate chosen from the following list: + * <ul> + * <li>{@link #TONE_DTMF_0} + * <li>{@link #TONE_DTMF_1} + * <li>{@link #TONE_DTMF_2} + * <li>{@link #TONE_DTMF_3} + * <li>{@link #TONE_DTMF_4} + * <li>{@link #TONE_DTMF_5} + * <li>{@link #TONE_DTMF_6} + * <li>{@link #TONE_DTMF_7} + * <li>{@link #TONE_DTMF_8} + * <li>{@link #TONE_DTMF_9} + * <li>{@link #TONE_DTMF_A} + * <li>{@link #TONE_DTMF_B} + * <li>{@link #TONE_DTMF_C} + * <li>{@link #TONE_DTMF_D} + * <li>{@link #TONE_SUP_DIAL} + * <li>{@link #TONE_SUP_BUSY} + * <li>{@link #TONE_SUP_CONGESTION} + * <li>{@link #TONE_SUP_RADIO_ACK} + * <li>{@link #TONE_SUP_RADIO_NOTAVAIL} + * <li>{@link #TONE_SUP_ERROR} + * <li>{@link #TONE_SUP_CALL_WAITING} + * <li>{@link #TONE_SUP_RINGTONE} + * <li>{@link #TONE_PROP_BEEP} + * <li>{@link #TONE_PROP_ACK} + * <li>{@link #TONE_PROP_NACK} + * <li>{@link #TONE_PROP_PROMPT} + * <li>{@link #TONE_PROP_BEEP2} + * </ul> + * @see #ToneGenerator(int, int) + */ + public native boolean startTone(int toneType); + + /** + * This method stops the tone currently playing playback. + * @see #ToneGenerator(int, int) + */ + public native void stopTone(); + + /** + * Releases resources associated with this ToneGenerator object. It is good + * practice to call this method when you're done using the ToneGenerator. + */ + public native void release(); + + private native final void native_setup(int streamType, int volume); + + private native final void native_finalize(); + protected void finalize() { native_finalize(); } + + private int mNativeContext; // accessed by native methods + + +} diff --git a/media/java/android/media/package.html b/media/java/android/media/package.html new file mode 100644 index 0000000..3de7167 --- /dev/null +++ b/media/java/android/media/package.html @@ -0,0 +1,13 @@ +<HTML> +<BODY> +Provides classes that manage various media interfaces in audio and video. +<p>The Media APIs are used to play and, in some cases, record media files. This includes +audio (e.g., play MP3s or other music files, ringtones, game sound effects, or DTMF tones) +and video (e.g., play a video streamed over the web or from local storage). +</p> +<p>Other special classes in the package offer the ability to detect the faces of people +in Bitmaps ({@link android.media.FaceDetector}), control audio routing (to the device or a headset) +and control alerts such as ringtones and phone vibrations ({@link android.media.AudioManager}). +</p> +</BODY> +</HTML> |