diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000000..91f4c50b401 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*~ +.DS_Store +.idea/ +gen/ diff --git a/Android.mk b/Android.mk old mode 100644 new mode 100755 index 0ba92cf576f..ab2df443d36 --- a/Android.mk +++ b/Android.mk @@ -32,6 +32,8 @@ framework_res_source_path := APPS/framework-res_intermediates/src # up into smaller pieces. # ============================================================ +LOCAL_CPPFLAGS := -std=gnu++11 + # embedded builds use nothing in frameworks/base ifneq ($(ANDROID_BUILD_EMBEDDED),true) @@ -71,6 +73,7 @@ LOCAL_SRC_FILES += \ core/java/android/app/IAppTask.aidl \ core/java/android/app/ITaskStackListener.aidl \ core/java/android/app/IBackupAgent.aidl \ + core/java/android/app/IBatteryService.aidl \ core/java/android/app/IInstrumentationWatcher.aidl \ core/java/android/app/INotificationManager.aidl \ core/java/android/app/IProcessObserver.aidl \ @@ -95,11 +98,15 @@ LOCAL_SRC_FILES += \ core/java/android/app/backup/IRestoreObserver.aidl \ core/java/android/app/backup/IRestoreSession.aidl \ core/java/android/app/usage/IUsageStatsManager.aidl \ + core/java/android/wipower/IWipower.aidl \ + core/java/android/wipower/IWipowerManagerCallback.aidl \ core/java/android/bluetooth/IBluetooth.aidl \ + core/java/android/bluetooth/IQBluetooth.aidl \ core/java/android/bluetooth/IBluetoothA2dp.aidl \ core/java/android/bluetooth/IBluetoothA2dpSink.aidl \ core/java/android/bluetooth/IBluetoothAvrcpController.aidl \ core/java/android/bluetooth/IBluetoothCallback.aidl \ + core/java/android/bluetooth/IQBluetoothAdapterCallback.aidl \ core/java/android/bluetooth/IBluetoothProfileServiceConnection.aidl \ core/java/android/bluetooth/IBluetoothHeadset.aidl \ core/java/android/bluetooth/IBluetoothHeadsetPhone.aidl \ @@ -109,13 +116,18 @@ LOCAL_SRC_FILES += \ core/java/android/bluetooth/IBluetoothPan.aidl \ core/java/android/bluetooth/IBluetoothManager.aidl \ core/java/android/bluetooth/IBluetoothManagerCallback.aidl \ + core/java/android/bluetooth/IQBluetoothManagerCallback.aidl \ core/java/android/bluetooth/IBluetoothPbap.aidl \ core/java/android/bluetooth/IBluetoothMap.aidl \ core/java/android/bluetooth/IBluetoothStateChangeCallback.aidl \ core/java/android/bluetooth/IBluetoothHeadsetClient.aidl \ + core/java/android/bluetooth/IBluetoothHidDevice.aidl \ + core/java/android/bluetooth/IBluetoothHidDeviceCallback.aidl \ core/java/android/bluetooth/IBluetoothGatt.aidl \ core/java/android/bluetooth/IBluetoothGattCallback.aidl \ core/java/android/bluetooth/IBluetoothGattServerCallback.aidl \ + core/java/android/bluetooth/IBluetoothDun.aidl \ + core/java/android/bluetooth/IBluetoothSap.aidl \ core/java/android/content/IClipboard.aidl \ core/java/android/content/IContentService.aidl \ core/java/android/content/IIntentReceiver.aidl \ @@ -139,6 +151,9 @@ LOCAL_SRC_FILES += \ core/java/android/content/pm/IPackageManager.aidl \ core/java/android/content/pm/IPackageMoveObserver.aidl \ core/java/android/content/pm/IPackageStatsObserver.aidl \ + core/java/android/content/res/IThemeChangeListener.aidl \ + core/java/android/content/res/IThemeProcessingListener.aidl \ + core/java/android/content/res/IThemeService.aidl \ core/java/android/database/IContentObserver.aidl \ core/java/android/hardware/ICameraService.aidl \ core/java/android/hardware/ICameraServiceListener.aidl \ @@ -147,6 +162,8 @@ LOCAL_SRC_FILES += \ core/java/android/hardware/IConsumerIrService.aidl \ core/java/android/hardware/IProCameraUser.aidl \ core/java/android/hardware/IProCameraCallbacks.aidl \ + core/java/android/hardware/ITorchService.aidl \ + core/java/android/hardware/ITorchCallback.aidl \ core/java/android/hardware/camera2/ICameraDeviceUser.aidl \ core/java/android/hardware/camera2/ICameraDeviceCallbacks.aidl \ core/java/android/hardware/ISerialManager.aidl \ @@ -199,6 +216,7 @@ LOCAL_SRC_FILES += \ core/java/android/os/INetworkActivityListener.aidl \ core/java/android/os/INetworkManagementService.aidl \ core/java/android/os/IPermissionController.aidl \ + core/java/android/os/IProcessInfoService.aidl \ core/java/android/os/IPowerManager.aidl \ core/java/android/os/IRemoteCallback.aidl \ core/java/android/os/ISchedulingPolicyService.aidl \ @@ -233,6 +251,10 @@ LOCAL_SRC_FILES += \ core/java/android/service/voice/IVoiceInteractionService.aidl \ core/java/android/service/voice/IVoiceInteractionSession.aidl \ core/java/android/service/voice/IVoiceInteractionSessionService.aidl \ + core/java/android/service/gesture/IEdgeGestureService.aidl \ + core/java/android/service/gesture/IEdgeGestureActivationListener.aidl \ + core/java/android/service/gesture/IEdgeGestureHostCallback.aidl \ + core/java/android/service/gesture/IGestureService.aidl \ core/java/android/service/wallpaper/IWallpaperConnection.aidl \ core/java/android/service/wallpaper/IWallpaperEngine.aidl \ core/java/android/service/wallpaper/IWallpaperService.aidl \ @@ -257,6 +279,7 @@ LOCAL_SRC_FILES += \ core/java/android/speech/IRecognitionService.aidl \ core/java/android/speech/tts/ITextToSpeechCallback.aidl \ core/java/android/speech/tts/ITextToSpeechService.aidl \ + core/java/codeaurora/ultrasound/IDigitalPenDimensionsCallback.aidl \ core/java/com/android/internal/app/IAppOpsCallback.aidl \ core/java/com/android/internal/app/IAppOpsService.aidl \ core/java/com/android/internal/app/IBatteryStats.aidl \ @@ -279,6 +302,7 @@ LOCAL_SRC_FILES += \ core/java/com/android/internal/os/IDropBoxManagerService.aidl \ core/java/com/android/internal/os/IParcelFileDescriptorFactory.aidl \ core/java/com/android/internal/os/IResultReceiver.aidl \ + core/java/com/android/internal/os/IKillSwitchService.aidl \ core/java/com/android/internal/statusbar/IStatusBar.aidl \ core/java/com/android/internal/statusbar/IStatusBarService.aidl \ core/java/com/android/internal/textservice/ISpellCheckerService.aidl \ @@ -305,6 +329,8 @@ LOCAL_SRC_FILES += \ location/java/android/location/IGeofenceProvider.aidl \ location/java/android/location/IGpsMeasurementsListener.aidl \ location/java/android/location/IGpsNavigationMessageListener.aidl \ + location/java/android/location/IGeoFencer.aidl \ + location/java/android/location/IGeoFenceListener.aidl \ location/java/android/location/IGpsStatusListener.aidl \ location/java/android/location/IGpsStatusProvider.aidl \ location/java/android/location/ILocationListener.aidl \ @@ -362,7 +388,6 @@ LOCAL_SRC_FILES += \ telecomm/java/com/android/internal/telecom/RemoteServiceCallback.aidl \ telephony/java/com/android/ims/internal/IImsCallSession.aidl \ telephony/java/com/android/ims/internal/IImsCallSessionListener.aidl \ - telephony/java/com/android/ims/internal/IImsConfig.aidl \ telephony/java/com/android/ims/internal/IImsRegistrationListener.aidl \ telephony/java/com/android/ims/internal/IImsEcbm.aidl \ telephony/java/com/android/ims/internal/IImsEcbmListener.aidl \ @@ -390,6 +415,14 @@ LOCAL_SRC_FILES += \ packages/services/Proxy/com/android/net/IProxyCallback.aidl \ packages/services/Proxy/com/android/net/IProxyPortListener.aidl \ +# AOSP is using a slightly different version of IMS than the latest +# BSP from Qualcomm. Let's support both. +ifeq ($(call is-vendor-board-platform,QCOM),true) +LOCAL_SRC_FILES += telephony/java/com/android/ims/internal/IImsConfig.aidl +else +LOCAL_SRC_FILES += telephony/aosp/com/android/ims/internal/IImsConfig.aidl +endif + # FRAMEWORKS_BASE_JAVA_SRC_DIRS comes from build/core/pathmap.mk LOCAL_AIDL_INCLUDES += $(FRAMEWORKS_BASE_JAVA_SRC_DIRS) @@ -532,6 +565,7 @@ aidl_files := \ frameworks/base/core/java/android/speech/tts/Voice.aidl \ frameworks/base/core/java/android/app/usage/UsageEvents.aidl \ frameworks/base/core/java/android/app/Notification.aidl \ + frameworks/base/core/java/android/app/NotificationGroup.aidl \ frameworks/base/core/java/android/app/WallpaperInfo.aidl \ frameworks/base/core/java/android/app/AppOpsManager.aidl \ frameworks/base/core/java/android/app/ActivityManager.aidl \ @@ -678,7 +712,11 @@ framework_docs_LOCAL_API_CHECK_JAVA_LIBRARIES := \ ext \ framework \ telephony-common \ - voip-common + voip-common \ + +ifeq ($(BOARD_USES_DPM),true) +framework_docs_LOCAL_API_CHECK_JAVA_LIBRARIES += tcmiface +endif framework_docs_LOCAL_JAVA_LIBRARIES := \ $(framework_docs_LOCAL_API_CHECK_JAVA_LIBRARIES) \ @@ -1034,6 +1072,10 @@ LOCAL_JAVA_RESOURCE_DIRS := $(ext_res_dirs) LOCAL_MODULE_TAGS := optional LOCAL_MODULE := ext +ifeq ($(BOARD_USES_DPM),true) +LOCAL_JAVA_LIBRARIES += tcmiface +endif + LOCAL_DX_FLAGS := --core-library include $(BUILD_JAVA_LIBRARY) diff --git a/CleanSpec.mk b/CleanSpec.mk index d66022428a8..9ecda39ec7f 100644 --- a/CleanSpec.mk +++ b/CleanSpec.mk @@ -229,3 +229,12 @@ $(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/framew # ****************************************************************** # NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST ABOVE THIS BANNER # ****************************************************************** + +# clean steps for aapt +$(call add-clean-step, rm -rf $(OUT_DIR)/host/$(HOST_PREBUILT_TAG)/bin/aapt) +$(call add-clean-step, rm -rf $(OUT_DIR)/host/$(HOST_PREBUILT_TAG)/obj32/EXECUTABLES/aapt_intermediates) +$(call add-clean-step, rm -rf $(OUT_DIR)/host/$(HOST_PREBUILT_TAG)/bin/libaapt_tests) +$(call add-clean-step, rm -rf $(PRODUCT_OUT)/obj/EXECUTABLES/aapt_intermediates) +$(call add-clean-step, rm -rf $(PRODUCT_OUT)/system/bin/aapt) +$(call add-clean-step, rm -rf $(PRODUCT_OUT)/symbols/system/bin/aapt) +$(call add-clean-step, rm -rf $(OUT_DIR)/host/$(HOST_PREBUILT_TAG)/obj32/STATIC_LIBRARIES/libaapt_intermediates) diff --git a/api/current.txt b/api/current.txt index 144f2923b75..5de135195d6 100644 --- a/api/current.txt +++ b/api/current.txt @@ -1718,6 +1718,8 @@ package android { field public static final int title = 16908310; // 0x1020016 field public static final int toggle = 16908311; // 0x1020017 field public static final int widget_frame = 16908312; // 0x1020018 + field public static final int monitor_box = 16908334; // 0x01020031 + field public static final int seek_bar = 16908335; // 0x01020032 } public static final class R.integer { @@ -1774,6 +1776,7 @@ package android { field public static final int simple_spinner_item = 17367048; // 0x1090008 field public static final int test_list_item = 17367052; // 0x109000c field public static final int two_line_list_item = 17367053; // 0x109000d + field public static final int slider_preference = 17367064; // 0x01090018 } public static final class R.menu { @@ -1819,6 +1822,7 @@ package android { field public static final int unknownName = 17039374; // 0x104000e field public static final int untitled = 17039375; // 0x104000f field public static final int yes = 17039379; // 0x1040013 + field public static final int default_string = 17039384; // 0x01040018 } public static final class R.style { @@ -5336,7 +5340,6 @@ package android.app.admin { method public boolean getScreenCaptureDisabled(android.content.ComponentName); method public boolean getStorageEncryption(android.content.ComponentName); method public int getStorageEncryptionStatus(); - method public java.util.List getTrustAgentConfiguration(android.content.ComponentName, android.content.ComponentName); method public boolean hasCaCertInstalled(android.content.ComponentName, byte[]); method public boolean hasGrantedPolicy(android.content.ComponentName, int); method public boolean installCaCert(android.content.ComponentName, byte[]); @@ -5385,7 +5388,6 @@ package android.app.admin { method public void setScreenCaptureDisabled(android.content.ComponentName, boolean); method public void setSecureSetting(android.content.ComponentName, java.lang.String, java.lang.String); method public int setStorageEncryption(android.content.ComponentName, boolean); - method public void setTrustAgentConfiguration(android.content.ComponentName, android.content.ComponentName, android.os.PersistableBundle); method public void setUninstallBlocked(android.content.ComponentName, java.lang.String, boolean); method public boolean switchUser(android.content.ComponentName, android.os.UserHandle); method public void uninstallAllUserCaCerts(android.content.ComponentName); @@ -9164,6 +9166,10 @@ package android.content.res { field public static final int UI_MODE_TYPE_TELEVISION = 4; // 0x4 field public static final int UI_MODE_TYPE_UNDEFINED = 0; // 0x0 field public static final int UI_MODE_TYPE_WATCH = 6; // 0x6 + field public static final int UI_THEME_MODE_UNDEFINED = 0; // 0x0 + field public static final int UI_THEME_MODE_NORMAL = 1; // 0x1 + field public static final int UI_THEME_MODE_HOLO_DARK = 2; // 0x2 + field public static final int UI_THEME_MODE_HOLO_LIGHT = 3; // 0x3 field public int densityDpi; field public float fontScale; field public int hardKeyboardHidden; @@ -9180,6 +9186,7 @@ package android.content.res { field public int screenWidthDp; field public int smallestScreenWidthDp; field public int touchscreen; + field public int uiThemeMode; field public int uiMode; } @@ -16996,6 +17003,7 @@ package android.net { } public final class IpPrefix implements android.os.Parcelable { + method public boolean contains(java.net.InetAddress); method public int describeContents(); method public java.net.InetAddress getAddress(); method public int getPrefixLength(); diff --git a/api/system-current.txt b/api/system-current.txt index ca783e12ac4..c702ceefb36 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -5469,7 +5469,6 @@ package android.app.admin { method public boolean getScreenCaptureDisabled(android.content.ComponentName); method public boolean getStorageEncryption(android.content.ComponentName); method public int getStorageEncryptionStatus(); - method public java.util.List getTrustAgentConfiguration(android.content.ComponentName, android.content.ComponentName); method public boolean hasCaCertInstalled(android.content.ComponentName, byte[]); method public boolean hasGrantedPolicy(android.content.ComponentName, int); method public boolean installCaCert(android.content.ComponentName, byte[]); @@ -5519,7 +5518,6 @@ package android.app.admin { method public void setScreenCaptureDisabled(android.content.ComponentName, boolean); method public void setSecureSetting(android.content.ComponentName, java.lang.String, java.lang.String); method public int setStorageEncryption(android.content.ComponentName, boolean); - method public void setTrustAgentConfiguration(android.content.ComponentName, android.content.ComponentName, android.os.PersistableBundle); method public void setUninstallBlocked(android.content.ComponentName, java.lang.String, boolean); method public boolean switchUser(android.content.ComponentName, android.os.UserHandle); method public void uninstallAllUserCaCerts(android.content.ComponentName); @@ -18255,6 +18253,7 @@ package android.net { } public final class IpPrefix implements android.os.Parcelable { + method public boolean contains(java.net.InetAddress); method public int describeContents(); method public java.net.InetAddress getAddress(); method public int getPrefixLength(); @@ -30345,6 +30344,7 @@ package android.telecom { method public android.telecom.PhoneAccountHandle getDefaultOutgoingPhoneAccount(java.lang.String); method public android.content.ComponentName getDefaultPhoneApp(); method public java.lang.String getLine1Number(android.telecom.PhoneAccountHandle); + method public java.lang.String getVoiceMailNumber(android.telecom.PhoneAccountHandle); method public android.telecom.PhoneAccount getPhoneAccount(android.telecom.PhoneAccountHandle); method public java.util.List getPhoneAccountsForPackage(); method public java.util.List getPhoneAccountsSupportingScheme(java.lang.String); diff --git a/btobex/Android.mk b/btobex/Android.mk new file mode 100644 index 00000000000..078a47e33dc --- /dev/null +++ b/btobex/Android.mk @@ -0,0 +1,9 @@ +LOCAL_PATH:= $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +LOCAL_MODULE:= javax.btobex + +include $(BUILD_JAVA_LIBRARY) diff --git a/btobex/MODULE_LICENSE_BSD_LIKE b/btobex/MODULE_LICENSE_BSD_LIKE new file mode 100644 index 00000000000..e69de29bb2d diff --git a/btobex/NOTICE b/btobex/NOTICE new file mode 100644 index 00000000000..92e8e59ef3b --- /dev/null +++ b/btobex/NOTICE @@ -0,0 +1,29 @@ +Copyright (c) 2008-2009, Motorola, Inc. + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +- Neither the name of the Motorola, Inc. nor the names of its contributors +may be used to endorse or promote products derived from this software +without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/btobex/javax/btobex/ApplicationParameter.java b/btobex/javax/btobex/ApplicationParameter.java new file mode 100644 index 00000000000..10a87b55690 --- /dev/null +++ b/btobex/javax/btobex/ApplicationParameter.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * Copyright (c) 2008-2009, Motorola, Inc. + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * - Neither the name of the Motorola, Inc. nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package javax.btobex; + +/** + * @hide + */ +public final class ApplicationParameter { + + private byte[] mArray; + + private int mLength; + + private int mMaxLength = 1000; + + public static class TRIPLET_TAGID { + public static final byte ORDER_TAGID = 0x01; + + public static final byte SEARCH_VALUE_TAGID = 0x02; + + public static final byte SEARCH_ATTRIBUTE_TAGID = 0x03; + + // if equals to "0", PSE only reply number of contacts + public static final byte MAXLISTCOUNT_TAGID = 0x04; + + public static final byte LISTSTARTOFFSET_TAGID = 0x05; + + public static final byte FILTER_TAGID = 0x06; + + public static final byte FORMAT_TAGID = 0x07; + + // only used if max list count = 0 + public static final byte PHONEBOOKSIZE_TAGID = 0x08; + + // only used in "mch" in response + public static final byte NEWMISSEDCALLS_TAGID = 0x09; + } + + public static class TRIPLET_VALUE { + public static class ORDER { + public static final byte ORDER_BY_INDEX = 0x00; + + public static final byte ORDER_BY_ALPHANUMERIC = 0x01; + + public static final byte ORDER_BY_PHONETIC = 0x02; + } + + public static class SEARCHATTRIBUTE { + public static final byte SEARCH_BY_NAME = 0x00; + + public static final byte SEARCH_BY_NUMBER = 0x01; + + public static final byte SEARCH_BY_SOUND = 0x02; + } + + public static class FORMAT { + public static final byte VCARD_VERSION_21 = 0x00; + + public static final byte VCARD_VERSION_30 = 0x01; + } + } + + public static class TRIPLET_LENGTH { + public static final byte ORDER_LENGTH = 1; + + public static final byte SEARCH_ATTRIBUTE_LENGTH = 1; + + public static final byte MAXLISTCOUNT_LENGTH = 2; + + public static final byte LISTSTARTOFFSET_LENGTH = 2; + + public static final byte FILTER_LENGTH = 8; + + public static final byte FORMAT_LENGTH = 1; + + public static final byte PHONEBOOKSIZE_LENGTH = 2; + + public static final byte NEWMISSEDCALLS_LENGTH = 1; + } + + public ApplicationParameter() { + mArray = new byte[mMaxLength]; + mLength = 0; + } + + public void addAPPHeader(byte tag, byte len, byte[] value) { + if ((mLength + len + 2) > mMaxLength) { + byte[] array_tmp = new byte[mLength + 4 * len]; + System.arraycopy(mArray, 0, array_tmp, 0, mLength); + mArray = array_tmp; + mMaxLength = mLength + 4 * len; + } + mArray[mLength++] = tag; + mArray[mLength++] = len; + System.arraycopy(value, 0, mArray, mLength, len); + mLength += len; + } + + public byte[] getAPPparam() { + byte[] para = new byte[mLength]; + System.arraycopy(mArray, 0, para, 0, mLength); + return para; + } +} diff --git a/btobex/javax/btobex/Authenticator.java b/btobex/javax/btobex/Authenticator.java new file mode 100644 index 00000000000..bbe69f22eb6 --- /dev/null +++ b/btobex/javax/btobex/Authenticator.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * Copyright (c) 2008-2009, Motorola, Inc. + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * - Neither the name of the Motorola, Inc. nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + + package javax.btobex; + +/** + * This interface provides a way to respond to authentication challenge and + * authentication response headers. When a client or server receives an + * authentication challenge or authentication response header, the + * onAuthenticationChallenge() or + * onAuthenticationResponse() will be called, respectively, by the + * implementation. + *

+ * For more information on how the authentication procedure works in OBEX, + * please review the IrOBEX specification at http://www.irda.org. + *

+ * Authentication Challenges + *

+ * When a client or server receives an authentication challenge header, the + * onAuthenticationChallenge() method will be invoked by the OBEX + * API implementation. The application will then return the user name (if + * needed) and password via a PasswordAuthentication object. The + * password in this object is not sent in the authentication response. Instead, + * the 16-byte challenge received in the authentication challenge is combined + * with the password returned from the onAuthenticationChallenge() + * method and passed through the MD5 hash algorithm. The resulting value is sent + * in the authentication response along with the user name if it was provided. + *

+ * Authentication Responses + *

+ * When a client or server receives an authentication response header, the + * onAuthenticationResponse() method is invoked by the API + * implementation with the user name received in the authentication response + * header. (The user name will be null if no user name was provided + * in the authentication response header.) The application must determine the + * correct password. This value should be returned from the + * onAuthenticationResponse() method. If the authentication request + * should fail without the implementation checking the password, + * null should be returned by the application. (This is needed for + * reasons like not recognizing the user name, etc.) If the returned value is + * not null, the OBEX API implementation will combine the password + * returned from the onAuthenticationResponse() method and + * challenge sent via the authentication challenge, apply the MD5 hash + * algorithm, and compare the result to the response hash received in the + * authentication response header. If the values are not equal, an + * IOException will be thrown if the client requested + * authentication. If the server requested authentication, the + * onAuthenticationFailure() method will be called on the + * ServerRequestHandler that failed authentication. The connection + * is not closed if authentication failed. + * @hide + */ +public interface Authenticator { + + /** + * Called when a client or a server receives an authentication challenge + * header. It should respond to the challenge with a + * PasswordAuthentication that contains the correct user name + * and password for the challenge. + * @param description the description of which user name and password should + * be used; if no description is provided in the authentication + * challenge or the description is encoded in an encoding scheme that + * is not supported, an empty string will be provided + * @param isUserIdRequired true if the user ID is required; + * false if the user ID is not required + * @param isFullAccess true if full access to the server will + * be granted; false if read only access will be granted + * @return a PasswordAuthentication object containing the user + * name and password used for authentication + */ + PasswordAuthentication onAuthenticationChallenge(String description, boolean isUserIdRequired, + boolean isFullAccess); + + /** + * Called when a client or server receives an authentication response + * header. This method will provide the user name and expect the correct + * password to be returned. + * @param userName the user name provided in the authentication response; may + * be null + * @return the correct password for the user name provided; if + * null is returned then the authentication request + * failed + */ + byte[] onAuthenticationResponse(byte[] userName); +} diff --git a/btobex/javax/btobex/BaseStream.java b/btobex/javax/btobex/BaseStream.java new file mode 100755 index 00000000000..e99bf9bf8bd --- /dev/null +++ b/btobex/javax/btobex/BaseStream.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * Copyright (c) 2008-2009, Motorola, Inc. + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * - Neither the name of the Motorola, Inc. nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package javax.btobex; + +import java.io.IOException; + +/** + * This interface defines the methods needed by a parent that uses the + * PrivateInputStream and PrivateOutputStream objects defined in this package. + * @hide + */ +public interface BaseStream { + + /** + * Verifies that this object is still open. + * @throws IOException if the object is closed + */ + void ensureOpen() throws IOException; + + /** + * Verifies that additional information may be sent. In other words, the + * operation is not done. + * @throws IOException if the operation is completed + */ + void ensureNotDone() throws IOException; + + /** + * Continues the operation since there is no data to read. + * @param sendEmpty true if the operation should send an empty + * packet or not send anything if there is no data to send + * @param inStream true if the stream is input stream or is + * output stream + * @return true if the operation was completed; + * false if no operation took place + * @throws IOException if an IO error occurs + */ + boolean continueOperation(boolean sendEmpty, boolean inStream) throws IOException; + + /** + * Called when the output or input stream is closed. + * @param inStream true if the input stream is closed; + * false if the output stream is closed + * @throws IOException if an IO error occurs + */ + void streamClosed(boolean inStream) throws IOException; +} diff --git a/btobex/javax/btobex/ClientOperation.java b/btobex/javax/btobex/ClientOperation.java new file mode 100755 index 00000000000..fee98746b38 --- /dev/null +++ b/btobex/javax/btobex/ClientOperation.java @@ -0,0 +1,869 @@ +/* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * Copyright (c) 2008-2009, Motorola, Inc. + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * - Neither the name of the Motorola, Inc. nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package javax.btobex; + +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.ByteArrayOutputStream; + +/** + * This class implements the Operation interface. It will read and + * write data via puts and gets. + * @hide + */ +public final class ClientOperation implements Operation, BaseStream { + private static final String TAG = "Obex ClientOperation"; + + private static final boolean VERBOSE = ObexHelper.VERBOSE; + + private ClientSession mParent; + + private boolean mInputOpen; + + private PrivateInputStream mPrivateInput; + + private boolean mPrivateInputOpen; + + private PrivateOutputStream mPrivateOutput; + + private boolean mPrivateOutputOpen; + + private String mExceptionMessage; + + private int mMaxPacketSize; + + private boolean mOperationDone; + + private boolean mGetOperation; + + private boolean mGetFinalFlag; + + private HeaderSet mRequestHeader; + + private HeaderSet mReplyHeader; + + private boolean mEndOfBodySent; + + private boolean mSingleResponseActiveClient; + + private boolean mSrmGetActiveClient; + + private ObexHelper mSrmClientSession; + /** + * Creates new OperationImpl to read and write data to a server + * @param maxSize the maximum packet size + * @param p the parent to this object + * @param type true if this is a get request; + * falseResponseCodes interface. + * @return the response code retrieved from the server + * @throws IOException if an error occurred in the transport layer during + * the transaction; if this method is called on a + * HeaderSet object created by calling + * createHeaderSet in a ClientSession + * object + */ + public synchronized int getResponseCode() throws IOException { + //avoid dup validateConnection + if ((mReplyHeader.responseCode == -1) + || (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE)) { + validateConnection(); + } + + return mReplyHeader.responseCode; + } + + /** + * This method will always return null + * @return null + */ + public String getEncoding() { + return null; + } + + /** + * Returns the type of content that the resource connected to is providing. + * E.g. if the connection is via HTTP, then the value of the content-type + * header field is returned. + * @return the content type of the resource that the URL references, or + * null if not known + */ + public String getType() { + try { + return (String)mReplyHeader.getHeader(HeaderSet.TYPE); + } catch (IOException e) { + return null; + } + } + + /** + * Returns the length of the content which is being provided. E.g. if the + * connection is via HTTP, then the value of the content-length header field + * is returned. + * @return the content length of the resource that this connection's URL + * references, or -1 if the content length is not known + */ + public long getLength() { + try { + Long temp = (Long)mReplyHeader.getHeader(HeaderSet.LENGTH); + + if (temp == null) { + return -1; + } else { + return temp.longValue(); + } + } catch (IOException e) { + return -1; + } + } + + /** + * Open and return an input stream for a connection. + * @return an input stream + * @throws IOException if an I/O error occurs + */ + public InputStream openInputStream() throws IOException { + + ensureOpen(); + + if (mPrivateInputOpen) + throw new IOException("no more input streams available"); + if (mGetOperation) { + // send the GET request here + validateConnection(); + } else { + if (mPrivateInput == null) { + mPrivateInput = new PrivateInputStream(this); + } + } + + mPrivateInputOpen = true; + + return mPrivateInput; + } + + /** + * Open and return a data input stream for a connection. + * @return an input stream + * @throws IOException if an I/O error occurs + */ + public DataInputStream openDataInputStream() throws IOException { + return new DataInputStream(openInputStream()); + } + + /** + * Open and return an output stream for a connection. + * @return an output stream + * @throws IOException if an I/O error occurs + */ + public OutputStream openOutputStream() throws IOException { + + ensureOpen(); + ensureNotDone(); + + if (mPrivateOutputOpen) + throw new IOException("no more output streams available"); + + if (mPrivateOutput == null) { + // there are 3 bytes operation headers and 3 bytes body headers // + mPrivateOutput = new PrivateOutputStream(this, getMaxPacketSize()); + } + + mPrivateOutputOpen = true; + + return mPrivateOutput; + } + + public int getMaxPacketSize() { + return mMaxPacketSize - 6 - getHeaderLength(); + } + + public int getHeaderLength() { + // OPP may need it + byte[] headerArray = ObexHelper.createHeader(mRequestHeader, false); + if(VERBOSE) Log.v(TAG,"headerArrayLen: "+headerArray.length); + return headerArray.length; + } + + /** + * Open and return a data output stream for a connection. + * @return an output stream + * @throws IOException if an I/O error occurs + */ + public DataOutputStream openDataOutputStream() throws IOException { + return new DataOutputStream(openOutputStream()); + } + + /** + * Closes the connection and ends the transaction + * @throws IOException if the operation has already ended or is closed + */ + public void close() throws IOException { + mInputOpen = false; + mPrivateInputOpen = false; + mPrivateOutputOpen = false; + mParent.setRequestInactive(); + } + + /** + * Returns the headers that have been received during the operation. + * Modifying the object returned has no effect on the headers that are sent + * or retrieved. + * @return the headers received during this Operation + * @throws IOException if this Operation has been closed + */ + public HeaderSet getReceivedHeader() throws IOException { + ensureOpen(); + + return mReplyHeader; + } + + /** + * Specifies the headers that should be sent in the next OBEX message that + * is sent. + * @param headers the headers to send in the next message + * @throws IOException if this Operation has been closed or the + * transaction has ended and no further messages will be exchanged + * @throws IllegalArgumentException if headers was not created + * by a call to ServerRequestHandler.createHeaderSet() + * @throws NullPointerException if headers is null + */ + public void sendHeaders(HeaderSet headers) throws IOException { + ensureOpen(); + if (mOperationDone) { + throw new IOException("Operation has already exchanged all data"); + } + + if (headers == null) { + throw new IOException("Headers may not be null"); + } + + int[] headerList = headers.getHeaderList(); + if (headerList != null) { + for (int i = 0; i < headerList.length; i++) { + mRequestHeader.setHeader(headerList[i], headers.getHeader(headerList[i])); + } + } + } + + /** + * Verifies that additional information may be sent. In other words, the + * operation is not done. + * @throws IOException if the operation is completed + */ + public void ensureNotDone() throws IOException { + if (mOperationDone) { + throw new IOException("Operation has completed"); + } + } + + /** + * Verifies that the connection is open and no exceptions should be thrown. + * @throws IOException if an exception needs to be thrown + */ + public void ensureOpen() throws IOException { + mParent.ensureOpen(); + + if (mExceptionMessage != null) { + throw new IOException(mExceptionMessage); + } + if (!mInputOpen) { + throw new IOException("Operation has already ended"); + } + } + + /** + * Verifies that the connection is open and the proper data has been read. + * @throws IOException if an IO error occurs + */ + private void validateConnection() throws IOException { + ensureOpen(); + + // to sure only one privateInput object exist. + if (mPrivateInput == null) { + startProcessing(); + } + } + + /** + * Sends a request to the client of the specified type + * @param opCode the request code to send to the client + * @return true if there is more data to send; + * false if there is no more data to send + * @throws IOException if an IO error occurs + */ + private boolean sendRequest(int opCode) throws IOException { + + if (VERBOSE) Log.v(TAG, "sendRequest mSingleResponseActiveClient " + + mSingleResponseActiveClient); + + boolean returnValue = false; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + int bodyLength = -1; + byte[] headerArray = ObexHelper.createHeader(mRequestHeader, true); + if (mPrivateOutput != null) { + bodyLength = mPrivateOutput.size(); + } + + /* + * Determine if there is space to add a body request. At present + * this method checks to see if there is room for at least a 17 + * byte body header. This number needs to be at least 6 so that + * there is room for the header ID and length and the reply ID and + * length, but it is a waste of resources if we can't send much of + * the body. + */ + if ((ObexHelper.BASE_PACKET_LENGTH + headerArray.length) > mMaxPacketSize) { + int end = 0; + int start = 0; + // split & send the headerArray in multiple packets. + + while (end != headerArray.length) { + //split the headerArray + end = ObexHelper.findHeaderEnd(headerArray, start, mMaxPacketSize + - ObexHelper.BASE_PACKET_LENGTH); + // can not split + if (end == -1) { + mOperationDone = true; + abort(); + mExceptionMessage = "Header larger then can be sent in a packet"; + mInputOpen = false; + + if (mPrivateInput != null) { + mPrivateInput.close(); + } + + if (mPrivateOutput != null) { + mPrivateOutput.close(); + } + throw new IOException("OBEX Packet exceeds max packet size"); + } + + byte[] sendHeader = new byte[end - start]; + System.arraycopy(headerArray, start, sendHeader, 0, sendHeader.length); + if (!mParent.sendRequest(opCode, sendHeader, mReplyHeader, + mPrivateInput, mSingleResponseActiveClient, mSrmGetActiveClient)) { + return false; + } + + if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE) { + return false; + } + + start = end; + } + + if (bodyLength > 0) { + return true; + } else { + return false; + } + } else { + out.write(headerArray); + } + + if (bodyLength > 0) { + /* + * Determine if we can send the whole body or just part of + * the body. Remember that there is the 3 bytes for the + * response message and 3 bytes for the header ID and length + */ + if (bodyLength > (mMaxPacketSize - headerArray.length - 6)) { + returnValue = true; + + bodyLength = mMaxPacketSize - headerArray.length - 6; + } + + byte[] body = mPrivateOutput.readBytes(bodyLength); + + /* + * Since this is a put request if the final bit is set or + * the output stream is closed we need to send the 0x49 + * (End of Body) otherwise, we need to send 0x48 (Body) + */ + if ((mPrivateOutput.isClosed()) && (!returnValue) && (!mEndOfBodySent) + && ((opCode & 0x80) != 0)) { + out.write(0x49); + mEndOfBodySent = true; + } else { + out.write(0x48); + } + + bodyLength += 3; + out.write((byte)(bodyLength >> 8)); + out.write((byte)bodyLength); + + if (body != null) { + out.write(body); + } + } + + if (mPrivateOutputOpen && bodyLength <= 0 && !mEndOfBodySent) { + // only 0x82 or 0x83 can send 0x49 + if ((opCode & 0x80) == 0) { + out.write(0x48); + } else { + out.write(0x49); + mEndOfBodySent = true; + + } + + bodyLength = 3; + out.write((byte)(bodyLength >> 8)); + out.write((byte)bodyLength); + } + + if (VERBOSE) Log.v(TAG, "sendRequest with out.size " + out.size() + +"mSingleResponseActiveClient "+mSingleResponseActiveClient); + + if (out.size() == 0) { + if (!mParent.sendRequest(opCode, null, mReplyHeader, mPrivateInput, + mSingleResponseActiveClient, mSrmGetActiveClient)) { + return false; + } + return returnValue; + } + if ((out.size() > 0) + && (!mParent.sendRequest(opCode, out.toByteArray(), mReplyHeader, + mPrivateInput, mSingleResponseActiveClient, mSrmGetActiveClient))) { + return false; + } + + // send all of the output data in 0x48, + // send 0x49 with empty body + if ((mPrivateOutput != null) && (mPrivateOutput.size() > 0)) + returnValue = true; + + return returnValue; + } + + /** + * This method starts the processing thread results. It will send the + * initial request. If the response takes more then one packet, a thread + * will be started to handle additional requests + * @throws IOException if an IO error occurs + */ + private synchronized void startProcessing() throws IOException { + + if (VERBOSE) Log.v(TAG, "startProcessing mSingleResponseActiveClient " + +mSingleResponseActiveClient); + + if (mPrivateInput == null) { + mPrivateInput = new PrivateInputStream(this); + } + boolean more = true; + + if (mGetOperation) { + if (!mOperationDone) { + if (!mGetFinalFlag) { + mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE; + while ((more) && (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE)) { + if (VERBOSE) Log.v(TAG,"startProcessing SendRequest(0x03)"); + more = sendRequest(0x03); + if (VERBOSE) Log.v(TAG,"more "+more); + } + + if (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE) { + if (VERBOSE) Log.v(TAG,"startProcessing mParent.sendRequest(0x83)"); + mParent.sendRequest(0x83, null, mReplyHeader, mPrivateInput, false, false); + } + if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE) { + mOperationDone = true; + } + } else { + more = sendRequest(0x83); + + if (more) { + throw new IOException("FINAL_GET forced but data did not fit into single packet!"); + } + + mOperationDone = true; + } + } + } else { + + if (!mOperationDone) { + mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE; + while ((more) && (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE)) { + more = sendRequest(0x02); + + } + } + + if (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE) { + mParent.sendRequest(0x82, null, mReplyHeader, mPrivateInput, false, false); + } + + if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE) { + mOperationDone = true; + } + } + } + + /** + * Continues the operation since there is no data to read. + * @param sendEmpty true if the operation should send an empty + * packet or not send anything if there is no data to send + * @param inStream true if the stream is input stream or is + * output stream + * @throws IOException if an IO error occurs + */ + public synchronized boolean continueOperation(boolean sendEmpty, boolean inStream) + throws IOException { + + if (VERBOSE) Log.v(TAG, "continueOperation mSingleResponseActiveClient " + + mSingleResponseActiveClient); + + if (mGetOperation) { + if ((inStream) && (!mOperationDone)) { + // to deal with inputstream in get operation + if (VERBOSE) Log.v(TAG, "continueOperation inStream&&!mOperationDone" + +" mReplyHeader.responseCode = "+mReplyHeader.responseCode); + + Byte srm = (Byte)mReplyHeader.getHeader(HeaderSet.SINGLE_RESPONSE_MODE); + if ((srm == ObexHelper.OBEX_SRM_SUPPORTED)|| + (srm == ObexHelper.OBEX_SRM_ENABLED)) { + mSrmClientSession.setRemoteSrmStatus(ObexHelper.SRM_CAPABLE); + if (VERBOSE) Log.v(TAG, "Remote SRM status: Enabled by Server response"); + } + + // Turn on SRM only if supported by both Client and Server. + // Otherwise, don't turn on SRM. + if (mSrmClientSession.getRemoteSrmStatus()) { + if (VERBOSE) Log.v(TAG, "continueOperation: Remote SRM Enabled"); + mSrmGetActiveClient = mSrmClientSession.getLocalSrmStatus(); + } + + mParent.sendRequest(0x83, null, mReplyHeader, mPrivateInput, false, + mSrmGetActiveClient); + + /* + * Determine if that was not the last packet in the operation + */ + if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE) { + mOperationDone = true; + } + + return true; + + } else if ((!inStream) && (!mOperationDone)) { + // to deal with outputstream in get operation + if (VERBOSE) Log.v(TAG, "continueOperation (!inStream) && (!mOperationDone) "); + + if (mPrivateInput == null) { + mPrivateInput = new PrivateInputStream(this); + } + + if (!mGetFinalFlag) { + sendRequest(0x03); + } else { + sendRequest(0x83); + + if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE) { + mOperationDone = true; + } + } + return true; + + } else if (mOperationDone) { + if (VERBOSE) Log.v(TAG, "continueOperation mOperationDone "); + return false; + } + + } else { + if ((!inStream) && (!mOperationDone)) { + // to deal with outputstream in put operation + if (mReplyHeader.responseCode == -1) { + mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE; + } + sendRequest(0x02); + + if (VERBOSE) Log.v(TAG, "continueOperation: Client setting SRM, sendEmpty clause"); + Byte srm = (Byte)mReplyHeader.getHeader(HeaderSet.SINGLE_RESPONSE_MODE); + if ((srm == ObexHelper.OBEX_SRM_SUPPORTED)||(srm == ObexHelper.OBEX_SRM_ENABLED)) { + mSrmClientSession.setRemoteSrmStatus(ObexHelper.SRM_CAPABLE); + if (VERBOSE) Log.v(TAG, "Remote SRM status: Enabled by Server response"); + } + + // Turn on SRM only if supported by both Client and Server. Otherwise, don't turn on SRM. + if (mSrmClientSession.getRemoteSrmStatus()) { + if (VERBOSE) Log.v(TAG, "continueOperation: Remote SRM Enabled"); + mSingleResponseActiveClient = mSrmClientSession.getLocalSrmStatus(); + } + if (VERBOSE) Log.v(TAG, "continueOperation: Client SRM status: " + mSingleResponseActiveClient); + + if (mSingleResponseActiveClient == ObexHelper.LOCAL_SRM_ENABLED) { + if (VERBOSE) Log.v(TAG, "continueOperation: Client SRM enabled"); + Byte srmp = (Byte)mReplyHeader.getHeader(HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER); + if (VERBOSE) Log.v(TAG, "SRMP header (CONTINUE): " + srmp); + if (srmp == ObexHelper.OBEX_SRM_PARAM_WAIT) { + if (VERBOSE) Log.v(TAG, "continueOperation: Client SRMP WAIT requested by Server"); + mSrmClientSession.setLocalSrmpWait(true); + mSingleResponseActiveClient = ObexHelper.LOCAL_SRM_DISABLED; + } else { + if (VERBOSE) Log.v(TAG, "continueOperation: Client SRMP NONE"); + mSrmClientSession.setLocalSrmpWait(false); + } + mReplyHeader.setHeader(HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER, ObexHelper.OBEX_SRM_PARAM_NONE); + } else { + if (VERBOSE) Log.v(TAG, "continueOperation: Client SRM disabled"); + } + return true; + } else if ((inStream) && (!mOperationDone)) { + // How to deal with inputstream in put operation ? + return false; + + } else if (mOperationDone) { + return false; + } + + } + return false; + } + + /** + * Called when the output or input stream is closed. + * @param inStream true if the input stream is closed; + * false if the output stream is closed + * @throws IOException if an IO error occurs + */ + public void streamClosed(boolean inStream) throws IOException { + if (!mGetOperation) { + if ((!inStream) && (!mOperationDone)) { + // to deal with outputstream in put operation + + boolean more = true; + + if ((mPrivateOutput != null) && (mPrivateOutput.size() <= 0)) { + byte[] headerArray = ObexHelper.createHeader(mRequestHeader, false); + if (headerArray.length <= 0) + more = false; + } + // If have not sent any data so send all now + if (mReplyHeader.responseCode == -1) { + mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE; + } + + while ((more) && (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE)) { + more = sendRequest(0x02); + } + + if (VERBOSE) Log.v(TAG, "streamClosed: Client SRM Disabled"); + + mSrmClientSession.resetSrmStatus(); + mSingleResponseActiveClient = ObexHelper.LOCAL_SRM_DISABLED; + + /* + * According to the IrOBEX specification, after the final put, you + * only have a single reply to send. so we don't need the while + * loop. + */ + while (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE) { + + sendRequest(0x82); + } + mOperationDone = true; + } else if ((inStream) && (mOperationDone)) { + // how to deal with input stream in put stream ? + mOperationDone = true; + } + } else { + if ((inStream) && (!mOperationDone)) { + + // to deal with inputstream in get operation + // Have not sent any data so send it all now + + if (mReplyHeader.responseCode == -1) { + mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE; + } + if (VERBOSE) Log.v(TAG,"(inStream) && (!mOperationDone) StreamClosed"); + while (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE) { + if (VERBOSE) Log.v(TAG,"StreamClosed sendRequest(0x83)"); + if (!sendRequest(0x83)) { + break; + } + } + if (VERBOSE) Log.v(TAG, "streamClosed: Client SRM Disabled"); + /* Reset Srm status once the operation completes */ + mSrmClientSession.resetSrmStatus(); + mSrmGetActiveClient = ObexHelper.LOCAL_SRM_DISABLED; + + while (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE) { + if (VERBOSE) Log.v(TAG,"StreamClosed mParent.sendRequest(0x83)"); + mParent.sendRequest(0x83, null, mReplyHeader, mPrivateInput, false, false); + } + mOperationDone = true; + } else if ((!inStream) && (!mOperationDone)) { + // to deal with outputstream in get operation + // part of the data may have been sent in continueOperation. + + boolean more = true; + + if ((mPrivateOutput != null) && (mPrivateOutput.size() <= 0)) { + byte[] headerArray = ObexHelper.createHeader(mRequestHeader, false); + if (headerArray.length <= 0) + more = false; + } + + if (mPrivateInput == null) { + mPrivateInput = new PrivateInputStream(this); + } + if ((mPrivateOutput != null) && (mPrivateOutput.size() <= 0)) + more = false; + + mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE; + while ((more) && (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE)) { + more = sendRequest(0x03); + } + sendRequest(0x83); + // parent.sendRequest(0x83, null, replyHeaders, privateInput, false); + if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE) { + mOperationDone = true; + } + } + } + } + + public void noBodyHeader(){ + } + public void noEndofBody() { + + } +} diff --git a/btobex/javax/btobex/ClientSession.java b/btobex/javax/btobex/ClientSession.java new file mode 100755 index 00000000000..fccf657e00f --- /dev/null +++ b/btobex/javax/btobex/ClientSession.java @@ -0,0 +1,722 @@ +/* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * Copyright (c) 2008-2009, Motorola, Inc. + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * - Neither the name of the Motorola, Inc. nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package javax.btobex; + +import android.util.Log; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import android.os.Handler; +import android.os.Message; +import android.os.Looper; +import android.os.HandlerThread; + +/** + * This class in an implementation of the OBEX ClientSession. + * @hide + */ +public final class ClientSession extends ObexSession { + private static final String TAG = "Obex ClientSession"; + + private static final boolean VERBOSE = ObexHelper.VERBOSE; + + private boolean mOpen; + + // Determines if an OBEX layer connection has been established + private boolean mObexConnected; + + private byte[] mConnectionId = null; + + /* + * The max Packet size must be at least 256 according to the OBEX + * specification. + */ + private int maxPacketSize = 256; + + private static final int OBEX_RESPONSE_TIME_OUT = 1; + + /* Response timeout for OBEX request sent */ + private static final int OBEX_RESPONSE_TIME_OUT_VALUE = 30000; + + private boolean mRequestActive; + + private boolean setMTU = false; + + private final InputStream mInput; + + private final OutputStream mOutput; + + private long mTotalSize = 0; + public ObexHelper mSrmClient; + + private ClientSessionHandler mClientSessionHandler = null; + + private HandlerThread mHandlerThread = null; + + public ClientSession(final ObexTransport trans) throws IOException { + mInput = trans.openInputStream(); + mOutput = trans.openOutputStream(); + mOpen = true; + mRequestActive = false; + mSrmClient = new ObexHelper(); + mHandlerThread = new HandlerThread("OBEX Time Out Handler"); + mHandlerThread.start(); + mClientSessionHandler = new ClientSessionHandler(mHandlerThread.getLooper()); + } + + public void setMaxPacketSize(int size) { + if (VERBOSE) Log.v(TAG, "setMaxPacketSize" + size); + maxPacketSize = size; + } + + public HeaderSet connect(final HeaderSet header) throws IOException { + ensureOpen(); + if (mObexConnected) { + throw new IOException("Already connected to server"); + } + setRequestActive(); + + int totalLength = 4; + byte[] head = null; + + // Determine the header byte array + if (header != null) { + if (header.nonce != null) { + mChallengeDigest = new byte[16]; + System.arraycopy(header.nonce, 0, mChallengeDigest, 0, 16); + } + head = ObexHelper.createHeader(header, false); + totalLength += head.length; + } + /* + * Write the OBEX CONNECT packet to the server. + * Byte 0: 0x80 + * Byte 1&2: Connect Packet Length + * Byte 3: OBEX Version Number (Presently, 0x10) + * Byte 4: Flags (For TCP 0x00) + * Byte 5&6: Max OBEX Packet Length + * Byte 7 to n: headers + */ + byte[] requestPacket = new byte[totalLength]; + // We just need to start at byte 3 since the sendRequest() method will + // handle the length and 0x80. + requestPacket[0] = (byte)0x10; + requestPacket[1] = (byte)0x00; + requestPacket[2] = (byte)(maxPacketSize >> 8); + requestPacket[3] = (byte)(maxPacketSize & 0xFF); + if (head != null) { + System.arraycopy(head, 0, requestPacket, 4, head.length); + } + + // check with local max packet size + if ((requestPacket.length + 3) > maxPacketSize) { + throw new IOException("Packet size exceeds max packet size"); + } + + HeaderSet returnHeaderSet = new HeaderSet(); + sendRequest(ObexHelper.OBEX_OPCODE_CONNECT, requestPacket, returnHeaderSet, null, false, + false); + + /* + * Read the response from the OBEX server. + * Byte 0: Response Code (If successful then OBEX_HTTP_OK) + * Byte 1&2: Packet Length + * Byte 3: OBEX Version Number + * Byte 4: Flags3 + * Byte 5&6: Max OBEX packet Length + * Byte 7 to n: Optional HeaderSet + */ + if (returnHeaderSet.responseCode == ResponseCodes.OBEX_HTTP_OK) { + if(returnHeaderSet.mConnectionID != null ){ + System.arraycopy(returnHeaderSet.mConnectionID, 0, mConnectionId, 0, 4); + } + mObexConnected = true; + + Byte srm = (Byte)returnHeaderSet.getHeader(HeaderSet.SINGLE_RESPONSE_MODE); + if (srm == ObexHelper.OBEX_SRM_SUPPORTED) { + mSrmClient.setRemoteSrmStatus(ObexHelper.SRM_CAPABLE); + if (VERBOSE) Log.v(TAG, "SRM status: Enabled by Server response"); + } else { + mSrmClient.setRemoteSrmStatus(ObexHelper.SRM_INCAPABLE); + if (VERBOSE) Log.v(TAG, "SRM status: Disabled by Server response"); + } + } + setRequestInactive(); + + return returnHeaderSet; + } + + public Operation get(HeaderSet header) throws IOException { + + if (!mObexConnected) { + throw new IOException("Not connected to the server"); + } + setRequestActive(); + + ensureOpen(); + + HeaderSet head; + if (header == null) { + head = new HeaderSet(); + } else { + head = header; + if (head.nonce != null) { + mChallengeDigest = new byte[16]; + System.arraycopy(head.nonce, 0, mChallengeDigest, 0, 16); + } + } + // Add the connection ID if one exists + if (mConnectionId != null) { + head.mConnectionID = new byte[4]; + System.arraycopy(mConnectionId, 0, head.mConnectionID, 0, 4); + } + + return new ClientOperation(maxPacketSize, this, head, true); + } + + /** + * 0xCB Connection Id an identifier used for OBEX connection multiplexing + */ + public void setConnectionID(long id) { + if ((id < 0) || (id > 0xFFFFFFFFL)) { + throw new IllegalArgumentException("Connection ID is not in a valid range"); + } + mConnectionId = ObexHelper.convertToByteArray(id); + } + + public HeaderSet delete(HeaderSet header) throws IOException { + + Operation op = put(header); + op.getResponseCode(); + HeaderSet returnValue = op.getReceivedHeader(); + op.close(); + + return returnValue; + } + + public HeaderSet disconnect(HeaderSet header) throws IOException { + if (!mObexConnected) { + throw new IOException("Not connected to the server"); + } + setRequestActive(); + + ensureOpen(); + // Determine the header byte array + byte[] head = null; + if (header != null) { + if (header.nonce != null) { + mChallengeDigest = new byte[16]; + System.arraycopy(header.nonce, 0, mChallengeDigest, 0, 16); + } + // Add the connection ID if one exists + if (mConnectionId != null) { + header.mConnectionID = new byte[4]; + System.arraycopy(mConnectionId, 0, header.mConnectionID, 0, 4); + } + head = ObexHelper.createHeader(header, false); + + if ((head.length + 3) > maxPacketSize) { + throw new IOException("Packet size exceeds max packet size"); + } + } else { + // Add the connection ID if one exists + if (mConnectionId != null) { + head = new byte[5]; + head[0] = (byte)HeaderSet.CONNECTION_ID; + System.arraycopy(mConnectionId, 0, head, 1, 4); + } + } + + HeaderSet returnHeaderSet = new HeaderSet(); + sendRequest(ObexHelper.OBEX_OPCODE_DISCONNECT, head, returnHeaderSet, null, false, false); + + /* + * An OBEX DISCONNECT reply from the server: + * Byte 1: Response code + * Bytes 2 & 3: packet size + * Bytes 4 & up: headers + */ + + /* response code , and header are ignored + * */ + + synchronized (this) { + mObexConnected = false; + setRequestInactive(); + } + + /* OBEX disconnect from app. Stop the handler thread */ + if (mHandlerThread != null ) { + mHandlerThread.quit(); + mHandlerThread = null; + mClientSessionHandler = null; + } + return returnHeaderSet; + } + + public long getConnectionID() { + + if (mConnectionId == null) { + return -1; + } + return ObexHelper.convertToLong(mConnectionId); + } + + public void reduceMTU(boolean enable) { + setMTU = enable; + } + + public Operation put(HeaderSet header) throws IOException { + if (!mObexConnected) { + throw new IOException("Not connected to the server"); + } + setRequestActive(); + + ensureOpen(); + HeaderSet head; + if (header == null) { + head = new HeaderSet(); + } else { + head = header; + // when auth is initiated by client ,save the digest + if (head.nonce != null) { + mChallengeDigest = new byte[16]; + System.arraycopy(head.nonce, 0, mChallengeDigest, 0, 16); + } + } + + // Add the connection ID if one exists + if (mConnectionId != null) { + + head.mConnectionID = new byte[4]; + System.arraycopy(mConnectionId, 0, head.mConnectionID, 0, 4); + } + + return new ClientOperation(maxPacketSize, this, head, false); + } + + public void setAuthenticator(Authenticator auth) throws IOException { + if (auth == null) { + throw new IOException("Authenticator may not be null"); + } + mAuthenticator = auth; + } + + public HeaderSet setPath(HeaderSet header, boolean backup, boolean create) throws IOException { + if (!mObexConnected) { + throw new IOException("Not connected to the server"); + } + setRequestActive(); + ensureOpen(); + + int totalLength = 2; + byte[] head = null; + HeaderSet headset; + if (header == null) { + headset = new HeaderSet(); + } else { + headset = header; + if (headset.nonce != null) { + mChallengeDigest = new byte[16]; + System.arraycopy(headset.nonce, 0, mChallengeDigest, 0, 16); + } + } + + // when auth is initiated by client ,save the digest + if (headset.nonce != null) { + mChallengeDigest = new byte[16]; + System.arraycopy(headset.nonce, 0, mChallengeDigest, 0, 16); + } + + // Add the connection ID if one exists + if (mConnectionId != null) { + headset.mConnectionID = new byte[4]; + System.arraycopy(mConnectionId, 0, headset.mConnectionID, 0, 4); + } + + head = ObexHelper.createHeader(headset, false); + totalLength += head.length; + + if (totalLength > maxPacketSize) { + throw new IOException("Packet size exceeds max packet size"); + } + + int flags = 0; + /* + * The backup flag bit is bit 0 so if we add 1, this will set that bit + */ + if (backup) { + flags++; + } + /* + * The create bit is bit 1 so if we or with 2 the bit will be set. + */ + if (!create) { + flags |= 2; + } + + /* + * An OBEX SETPATH packet to the server: + * Byte 1: 0x85 + * Byte 2 & 3: packet size + * Byte 4: flags + * Byte 5: constants + * Byte 6 & up: headers + */ + byte[] packet = new byte[totalLength]; + packet[0] = (byte)flags; + packet[1] = (byte)0x00; + if (headset != null) { + System.arraycopy(head, 0, packet, 2, head.length); + } + + HeaderSet returnHeaderSet = new HeaderSet(); + sendRequest(ObexHelper.OBEX_OPCODE_SETPATH, packet, returnHeaderSet, null, false, false); + + /* + * An OBEX SETPATH reply from the server: + * Byte 1: Response code + * Bytes 2 & 3: packet size + * Bytes 4 & up: headers + */ + + setRequestInactive(); + + return returnHeaderSet; + } + + /** + * Verifies that the connection is open. + * @throws IOException if the connection is closed + */ + public synchronized void ensureOpen() throws IOException { + if (!mOpen) { + throw new IOException("Connection closed"); + } + } + + /** + * Set request inactive. Allows Put and get operation objects to tell this + * object when they are done. + */ + /*package*/synchronized void setRequestInactive() { + mRequestActive = false; + } + + /** + * Set request to active. + * @throws IOException if already active + */ + private synchronized void setRequestActive() throws IOException { + if (mRequestActive) { + throw new IOException("OBEX request is already being performed"); + } + mRequestActive = true; + } + + /** + * Sends a standard request to the client. If ignoreResponse is not set, it + * will then wait for the reply and update the header set object provided. + * If any authentication headers (i.e. authentication challenge or + * authentication response) are received, they will be processed. + * + * If ignoreResponse is set, this will not wait for a reply and the header + * set object will not be updated. This is intended for use during Single + * Response Mode operation. + * + * @param opCode the type of request to send to the client + * @param head the headers to send to the client + * @param header the header object to update with the response + * @param privateInput the input stream used by the Operation object; null + * if this is called on a CONNECT, SETPATH or DISCONNECT return + * true if the operation completed successfully; + * false if an authentication response failed to pass + * @param ignoreResponse true if a response shouldn't be received (e.g., + * when operating under Single Response Mode (SRM). false otherwise. + * @throws IOException if an IO error occurs + */ + public boolean sendRequest(int opCode, byte[] head, HeaderSet header, + PrivateInputStream privateInput, + boolean ignoreResponse, + boolean supressSend) throws IOException { + + if (VERBOSE) Log.v(TAG, "sendRequest ignore: " + ignoreResponse + + ", SRMP WAIT: " + mSrmClient.getLocalSrmpWait() + + " supressSend : "+supressSend); + + //check header length with local max size + if (head != null) { + if ((head.length + 3) > maxPacketSize) { + throw new IOException("header too large "); + } + } + + int bytesReceived; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write((byte)opCode); + if (VERBOSE) Log.v(TAG, "sendRequest opCode = "+opCode); + + // Determine if there are any headers to send + if (head == null) { + out.write(0x00); + out.write(0x03); + } else { + out.write((byte)((head.length + 3) >> 8)); + out.write((byte)(head.length + 3)); + out.write(head); + if (VERBOSE) Log.v(TAG, "sendRequest head.length = "+head.length); + } + + if(!supressSend) { + // Write the request to the output stream and flush the stream + mOutput.write(out.toByteArray()); + mOutput.flush(); + } + + if ( (!ignoreResponse) || (mSrmClient.getLocalSrmpWait()) ) { + startResponseTimer(); + try { + header.responseCode = mInput.read(); + if (VERBOSE) Log.v(TAG, "sendRequest responseCode "+header.responseCode); + } catch (IOException e) { + Log.v(TAG, "Response timed out. Clean up the handler: " + e); + if (mHandlerThread != null ) { + mHandlerThread.quit(); + mHandlerThread = null; + mClientSessionHandler = null; + } + } + + if (mClientSessionHandler != null) stopResponseTimer(); + + int length = ((mInput.read() << 8) | (mInput.read())); + if (VERBOSE) Log.v(TAG, "sendRequest response length "+length); + + if (length > maxPacketSize) { + throw new IOException("Packet received exceeds packet size limit"); + } + if (length > ObexHelper.BASE_PACKET_LENGTH) { + byte[] data = null; + if (opCode == ObexHelper.OBEX_OPCODE_CONNECT) { + @SuppressWarnings("unused") + int version = mInput.read(); + @SuppressWarnings("unused") + int flags = mInput.read(); + maxPacketSize = (mInput.read() << 8) + mInput.read(); + + //check with local max size + if (setMTU) { + maxPacketSize = ObexHelper.A2DP_SCO_OBEX_MAX_CLIENT_PACKET_SIZE; + setMTU = false; + } else if (maxPacketSize > ObexHelper.MAX_CLIENT_PACKET_SIZE) { + maxPacketSize = ObexHelper.MAX_CLIENT_PACKET_SIZE; + } + + if (length > 7) { + data = new byte[length - 7]; + bytesReceived = mInput.read(data); + while (bytesReceived != (length - 7)) { + bytesReceived += mInput.read(data, bytesReceived, data.length + - bytesReceived); + } + } else { + return true; + } + } else { + data = new byte[length - 3]; + bytesReceived = mInput.read(data); + + while (bytesReceived != (length - 3)) { + bytesReceived += mInput.read(data, bytesReceived, data.length - bytesReceived); + } + if (opCode == ObexHelper.OBEX_OPCODE_ABORT) { + return true; + } + } + + byte[] body = ObexHelper.updateHeaderSet(header, data); + if ((privateInput != null) && (body != null)) { + privateInput.writeBytes(body, 1); + mTotalSize += (long)(body.length - 1); + if((body[0] == HeaderSet.END_OF_BODY) && + (header.getHeader(HeaderSet.LENGTH) == null)){ + header.setHeader(HeaderSet.LENGTH, mTotalSize); + if (VERBOSE) Log.v(TAG, " header.mLength : " + + header.getHeader(HeaderSet.LENGTH)); + mTotalSize = 0; + } + } + + if (header.mConnectionID != null) { + mConnectionId = new byte[4]; + System.arraycopy(header.mConnectionID, 0, mConnectionId, 0, 4); + } + + if (header.mAuthResp != null) { + if (!handleAuthResp(header.mAuthResp)) { + setRequestInactive(); + throw new IOException("Authentication Failed"); + } + } + + if ((header.responseCode == ResponseCodes.OBEX_HTTP_UNAUTHORIZED) + && (header.mAuthChall != null)) { + + if (handleAuthChall(header)) { + out.write((byte)HeaderSet.AUTH_RESPONSE); + out.write((byte)((header.mAuthResp.length + 3) >> 8)); + out.write((byte)(header.mAuthResp.length + 3)); + out.write(header.mAuthResp); + header.mAuthChall = null; + header.mAuthResp = null; + + byte[] sendHeaders = new byte[out.size() - 3]; + System.arraycopy(out.toByteArray(), 3, sendHeaders, 0, sendHeaders.length); + + return sendRequest(opCode, sendHeaders, header, privateInput, false, false); + } + } + } + } + + return true; + } + + public HeaderSet action(HeaderSet header,int action) throws IOException { + if (!mObexConnected) { + throw new IOException("Not connected to the server"); + } + setRequestActive(); + ensureOpen(); + + int totalLength = 2; + byte[] head = null; + HeaderSet headset; + if (header == null) { + headset = new HeaderSet(); + } else { + headset = header; + if (headset.nonce != null) { + mChallengeDigest = new byte[16]; + System.arraycopy(headset.nonce, 0, mChallengeDigest, 0, 16); + } + } + + // when auth is initiated by client ,save the digest + if (headset.nonce != null) { + mChallengeDigest = new byte[16]; + System.arraycopy(headset.nonce, 0, mChallengeDigest, 0, 16); + } + + // Add the connection ID if one exists + if (mConnectionId != null) { + headset.mConnectionID = new byte[4]; + System.arraycopy(mConnectionId, 0, headset.mConnectionID, 0, 4); + } + + head = ObexHelper.createHeader(headset, false); + totalLength += head.length; + + if (totalLength > maxPacketSize) { + throw new IOException("Packet size exceeds max packet size"); + } + + /* + * An OBEX ACTION packet to the server: + * Byte 1: 0x86 + * Byte 2 & 3: packet size + * Byte 4: 0x94 + * Byte 5: action + * Byte 6 & up: headers + */ + byte[] packet = new byte[totalLength]; + packet[0] = (byte)0x94; + packet[1] = (byte)action; + + if (headset != null) { + System.arraycopy(head, 0, packet, 2, head.length); + } + + HeaderSet returnHeaderSet = new HeaderSet(); + sendRequest(ObexHelper.OBEX_OPCODE_ACTION, packet, returnHeaderSet, null, false, false); + + setRequestInactive(); + + return returnHeaderSet; + } + + public void close() throws IOException { + mOpen = false; + mInput.close(); + mOutput.close(); + } + private final class ClientSessionHandler extends Handler { + public ClientSessionHandler (Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + if (VERBOSE) Log.v(TAG, "Handler(): Response Time out. Close the socket"); + switch (msg.what) { + case OBEX_RESPONSE_TIME_OUT: + try { + close(); + } catch (IOException e) { + if (VERBOSE) Log.v(TAG, "Response time out: " + e); + } + break; + } + } + } + + private void startResponseTimer() { + if (VERBOSE) Log.v(TAG, "OBEX: Start response timer"); + if (mClientSessionHandler != null) { + mClientSessionHandler.sendMessageDelayed(mClientSessionHandler + .obtainMessage(OBEX_RESPONSE_TIME_OUT), OBEX_RESPONSE_TIME_OUT_VALUE); + } + return; + } + + private void stopResponseTimer() { + if (VERBOSE) Log.v(TAG, "OBEX: Stop response timer"); + if (mClientSessionHandler != null) { + mClientSessionHandler.removeMessages(OBEX_RESPONSE_TIME_OUT); + } + return; + } +} diff --git a/btobex/javax/btobex/HeaderSet.java b/btobex/javax/btobex/HeaderSet.java new file mode 100755 index 00000000000..d7bff550155 --- /dev/null +++ b/btobex/javax/btobex/HeaderSet.java @@ -0,0 +1,762 @@ +/* + * Copyright (C) 2013 The Linux Foundation. All rights reserved. + * Copyright (c) 2008-2009, Motorola, Inc. + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * - Neither the name of the Motorola, Inc. nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package javax.btobex; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Calendar; +import java.security.SecureRandom; + +/** + * This class implements the javax.obex.HeaderSet interface for OBEX over + * RFCOMM. + * @hide + */ +public final class HeaderSet { + + /** + * Represents the OBEX Count header. This allows the connection statement to + * tell the server how many objects it plans to send or retrieve. + *

+ * The value of COUNT is 0xC0 (192). + */ + public static final int COUNT = 0xC0; + + /** + * Represents the OBEX Name header. This specifies the name of the object. + *

+ * The value of NAME is 0x01 (1). + */ + public static final int NAME = 0x01; + + /** + * Represents the OBEX Type header. This allows a request to specify the + * type of the object (e.g. text, html, binary, etc.). + *

+ * The value of TYPE is 0x42 (66). + */ + public static final int TYPE = 0x42; + + /** + * Represents the OBEX Length header. This is the length of the object in + * bytes. + *

+ * The value of LENGTH is 0xC3 (195). + */ + public static final int LENGTH = 0xC3; + + /** + * Represents the OBEX Time header using the ISO 8601 standards. This is the + * preferred time header. + *

+ * The value of TIME_ISO_8601 is 0x44 (68). + */ + public static final int TIME_ISO_8601 = 0x44; + + /** + * Represents the OBEX Time header using the 4 byte representation. This is + * only included for backwards compatibility. It represents the number of + * seconds since January 1, 1970. + *

+ * The value of TIME_4_BYTE is 0xC4 (196). + */ + public static final int TIME_4_BYTE = 0xC4; + + /** + * Represents the OBEX Description header. This is a text description of the + * object. + *

+ * The value of DESCRIPTION is 0x05 (5). + */ + public static final int DESCRIPTION = 0x05; + + /** + * Represents the OBEX Target header. This is the name of the service an + * operation is targeted to. + *

+ * The value of TARGET is 0x46 (70). + */ + public static final int TARGET = 0x46; + + /** + * Represents the OBEX HTTP header. This allows an HTTP 1.X header to be + * included in a request or reply. + *

+ * The value of HTTP is 0x47 (71). + */ + public static final int HTTP = 0x47; + + /** + * Represents the OBEX BODY header. + *

+ * The value of BODY is 0x48 (72). + */ + public static final int BODY = 0x48; + + /** + * Represents the OBEX End of BODY header. + *

+ * The value of BODY is 0x49 (73). + */ + public static final int END_OF_BODY = 0x49; + + /** + * Represents the OBEX Who header. Identifies the OBEX application to + * determine if the two peers are talking to each other. + *

+ * The value of WHO is 0x4A (74). + */ + public static final int WHO = 0x4A; + + /** + * Represents the OBEX Connection ID header. Identifies used for OBEX + * connection multiplexing. + *

+ * The value of CONNECTION_ID is 0xCB (203). + */ + + public static final int CONNECTION_ID = 0xCB; + + /** + * Represents the OBEX Application Parameter header. This header specifies + * additional application request and response information. + *

+ * The value of APPLICATION_PARAMETER is 0x4C (76). + */ + public static final int APPLICATION_PARAMETER = 0x4C; + + /** + * Represents the OBEX authentication digest-challenge. + *

+ * The value of AUTH_CHALLENGE is 0x4D (77). + */ + public static final int AUTH_CHALLENGE = 0x4D; + + /** + * Represents the OBEX authentication digest-response. + *

+ * The value of AUTH_RESPONSE is 0x4E (78). + */ + public static final int AUTH_RESPONSE = 0x4E; + + /** + * Represents the OBEX Object Class header. This header specifies the OBEX + * object class of the object. + *

+ * The value of OBJECT_CLASS is 0x4F (79). + */ + public static final int OBJECT_CLASS = 0x4F; + + /** + * Represents the OBEX Single Response Mode (SRM). + *

+ * The value of SINGLE_RESPONSE_MODE is 0x97 (151). + */ + public static final int SINGLE_RESPONSE_MODE = 0x97; + + /** + * Represents the OBEX Single Response Mode (SRM) Parameter. + *

+ * The value of SINGLE_RESPONSE_MODE_PARAMETER is 0x98 (152). + */ + public static final int SINGLE_RESPONSE_MODE_PARAMETER = 0x98; + + /** + * Represents the OBEX Action Id. + *

+ * The value of ACTION_ID is 0x94 (148). + */ + public static final int ACTION_ID = 0x94; + + /** + * Represents the OBEX Destination name. + *

+ * The value of DEST_NAME is 0x15 (21). + */ + public static final int DEST_NAME = 0x15; + + /** + * Represents the OBEX Permissions. + *

+ * The value of PERMISSION is 0xD6 (214). + */ + public static final int PERMISSION = 0xD6; + + private Long mCount; // 4 byte unsigned integer + + private Byte mActionId; // 4 byte unsigned integer + private String mName; // null terminated Unicode text string + private boolean mEmptyName; + private String mDestName; // null terminated Unicode text string + + private String mType; // null terminated ASCII text string + + private Long mLength; // 4 byte unsigend integer + private Long mPermission; // 4 byte unsigend integer + + private Calendar mIsoTime; // String of the form YYYYMMDDTHHMMSSZ + + private Calendar mByteTime; // 4 byte unsigned integer + + private String mDescription; // null terminated Unicode text String + + private byte[] mTarget; // byte sequence + + private byte[] mHttpHeader; // byte sequence + + private byte[] mWho; // length prefixed byte sequence + + private byte[] mAppParam; // byte sequence of the form tag length value + + private byte[] mObjectClass; // byte sequence + + private String[] mUnicodeUserDefined; //null terminated unicode string + + private byte[][] mSequenceUserDefined; // byte sequence user defined + + private Byte[] mByteUserDefined; // 1 byte + + private Long[] mIntegerUserDefined; // 4 byte unsigned integer + + private final SecureRandom mRandom; + + private Byte mSingleRespMode; // 1 byte value to setup the OBEX Single Response Mode (SRM) + + // 1 byte value for setting parameters used during the OBEX Single Response Mode (SRM) + private Byte mSingleRespModeParam; + + /*package*/ byte[] nonce; + + public byte[] mAuthChall; // The authentication challenge header + + public byte[] mAuthResp; // The authentication response header + + public byte[] mConnectionID; // THe connection ID + + public int responseCode; + + /** + * Creates new HeaderSet object. + * @param size the max packet size for this connection + */ + public HeaderSet() { + mUnicodeUserDefined = new String[16]; + mSequenceUserDefined = new byte[16][]; + mByteUserDefined = new Byte[16]; + mIntegerUserDefined = new Long[16]; + responseCode = -1; + mRandom = new SecureRandom(); + } + + /** + * Sets flag for special "value" of NAME header which should be empty. This + * is not the same as NAME header with empty string in which case it will + * have length of 5 bytes. It should be 3 bytes with only header id and + * length field. + */ + public void setEmptyNameHeader() { + mName = null; + mEmptyName = true; + } + + /** + * Gets flag for special "value" of NAME header which should be empty. See + * above. + */ + public boolean getEmptyNameHeader() { + return mEmptyName; + } + + /** + * Sets the value of the header identifier to the value provided. The type + * of object must correspond to the Java type defined in the description of + * this interface. If null is passed as the + * headerValue then the header will be removed from the set of + * headers to include in the next request. + * @param headerID the identifier to include in the message + * @param headerValue the value of the header identifier + * @throws IllegalArgumentException if the header identifier provided is not + * one defined in this interface or a user-defined header; if the + * type of headerValue is not the correct Java type as + * defined in the description of this interface\ + */ + public void setHeader(int headerID, Object headerValue) { + long temp = -1; + + switch (headerID) { + case COUNT: + if (!(headerValue instanceof Long)) { + if (headerValue == null) { + mCount = null; + break; + } + throw new IllegalArgumentException("Count must be a Long"); + } + temp = ((Long)headerValue).longValue(); + if ((temp < 0L) || (temp > 0xFFFFFFFFL)) { + throw new IllegalArgumentException("Count must be between 0 and 0xFFFFFFFF"); + } + mCount = (Long)headerValue; + break; + case ACTION_ID: + if ((headerValue != null) && (!(headerValue instanceof Byte))) { + throw new IllegalArgumentException( + "Action Id must be a Byte"); + } + mActionId = (Byte)headerValue; + + break; + case NAME: + if ((headerValue != null) && (!(headerValue instanceof String))) { + throw new IllegalArgumentException("Name must be a String"); + } + mEmptyName = false; + mName = (String)headerValue; + break; + case DEST_NAME: + if ((headerValue != null) && (!(headerValue instanceof String))) { + throw new IllegalArgumentException("Name must be a String"); + } + mDestName = (String)headerValue; + + break; + case TYPE: + if ((headerValue != null) && (!(headerValue instanceof String))) { + throw new IllegalArgumentException("Type must be a String"); + } + mType = (String)headerValue; + break; + case PERMISSION: + if (!(headerValue instanceof Long)) { + if (headerValue == null) { + mPermission = null; + break; + } + throw new IllegalArgumentException("Length must be a Long"); + } + temp = ((Long)headerValue).longValue(); + if ((temp < 0L) || (temp > 0xFFFFFFFFL)) { + throw new IllegalArgumentException("Length must be between 0 and 0xFFFFFFFF"); + } + mPermission = (Long)headerValue; + break; + case LENGTH: + if (!(headerValue instanceof Long)) { + if (headerValue == null) { + mLength = null; + break; + } + throw new IllegalArgumentException("Length must be a Long"); + } + temp = ((Long)headerValue).longValue(); + if ((temp < 0L) || (temp > 0xFFFFFFFFL)) { + throw new IllegalArgumentException("Length must be between 0 and 0xFFFFFFFF"); + } + mLength = (Long)headerValue; + break; + case TIME_ISO_8601: + if ((headerValue != null) && (!(headerValue instanceof Calendar))) { + throw new IllegalArgumentException("Time ISO 8601 must be a Calendar"); + } + mIsoTime = (Calendar)headerValue; + break; + case TIME_4_BYTE: + if ((headerValue != null) && (!(headerValue instanceof Calendar))) { + throw new IllegalArgumentException("Time 4 Byte must be a Calendar"); + } + mByteTime = (Calendar)headerValue; + break; + case DESCRIPTION: + if ((headerValue != null) && (!(headerValue instanceof String))) { + throw new IllegalArgumentException("Description must be a String"); + } + mDescription = (String)headerValue; + break; + case TARGET: + if (headerValue == null) { + mTarget = null; + } else { + if (!(headerValue instanceof byte[])) { + throw new IllegalArgumentException("Target must be a byte array"); + } else { + mTarget = new byte[((byte[])headerValue).length]; + System.arraycopy(headerValue, 0, mTarget, 0, mTarget.length); + } + } + break; + case HTTP: + if (headerValue == null) { + mHttpHeader = null; + } else { + if (!(headerValue instanceof byte[])) { + throw new IllegalArgumentException("HTTP must be a byte array"); + } else { + mHttpHeader = new byte[((byte[])headerValue).length]; + System.arraycopy(headerValue, 0, mHttpHeader, 0, mHttpHeader.length); + } + } + break; + case WHO: + if (headerValue == null) { + mWho = null; + } else { + if (!(headerValue instanceof byte[])) { + throw new IllegalArgumentException("WHO must be a byte array"); + } else { + mWho = new byte[((byte[])headerValue).length]; + System.arraycopy(headerValue, 0, mWho, 0, mWho.length); + } + } + break; + case OBJECT_CLASS: + if (headerValue == null) { + mObjectClass = null; + } else { + if (!(headerValue instanceof byte[])) { + throw new IllegalArgumentException("Object Class must be a byte array"); + } else { + mObjectClass = new byte[((byte[])headerValue).length]; + System.arraycopy(headerValue, 0, mObjectClass, 0, mObjectClass.length); + } + } + break; + case APPLICATION_PARAMETER: + if (headerValue == null) { + mAppParam = null; + } else { + if (!(headerValue instanceof byte[])) { + throw new IllegalArgumentException( + "Application Parameter must be a byte array"); + } else { + mAppParam = new byte[((byte[])headerValue).length]; + System.arraycopy(headerValue, 0, mAppParam, 0, mAppParam.length); + } + } + break; + case SINGLE_RESPONSE_MODE: + if ((headerValue != null) && (!(headerValue instanceof Byte))) { + throw new IllegalArgumentException( + "Single Response Mode must be a Byte"); + } + mSingleRespMode = (Byte)headerValue; + break; + case SINGLE_RESPONSE_MODE_PARAMETER: + if ((headerValue != null) && (!(headerValue instanceof Byte))) { + throw new IllegalArgumentException( + "Single Response Mode Parameter must be a Byte"); + } + mSingleRespModeParam = (Byte)headerValue; + break; + default: + // Verify that it was not a Unicode String user Defined + if ((headerID >= 0x30) && (headerID <= 0x3F)) { + if ((headerValue != null) && (!(headerValue instanceof String))) { + throw new IllegalArgumentException( + "Unicode String User Defined must be a String"); + } + mUnicodeUserDefined[headerID - 0x30] = (String)headerValue; + + break; + } + // Verify that it was not a byte sequence user defined value + if ((headerID >= 0x70) && (headerID <= 0x7F)) { + + if (headerValue == null) { + mSequenceUserDefined[headerID - 0x70] = null; + } else { + if (!(headerValue instanceof byte[])) { + throw new IllegalArgumentException( + "Byte Sequence User Defined must be a byte array"); + } else { + mSequenceUserDefined[headerID - 0x70] = new byte[((byte[])headerValue).length]; + System.arraycopy(headerValue, 0, mSequenceUserDefined[headerID - 0x70], + 0, mSequenceUserDefined[headerID - 0x70].length); + } + } + break; + } + // Verify that it was not a Byte user Defined + if ((headerID >= 0xB0) && (headerID <= 0xBF)) { + if ((headerValue != null) && (!(headerValue instanceof Byte))) { + throw new IllegalArgumentException("ByteUser Defined must be a Byte"); + } + mByteUserDefined[headerID - 0xB0] = (Byte)headerValue; + + break; + } + // Verify that is was not the 4 byte unsigned integer user + // defined header + if ((headerID >= 0xF0) && (headerID <= 0xFF)) { + if (!(headerValue instanceof Long)) { + if (headerValue == null) { + mIntegerUserDefined[headerID - 0xF0] = null; + break; + } + throw new IllegalArgumentException("Integer User Defined must be a Long"); + } + temp = ((Long)headerValue).longValue(); + if ((temp < 0L) || (temp > 0xFFFFFFFFL)) { + throw new IllegalArgumentException( + "Integer User Defined must be between 0 and 0xFFFFFFFF"); + } + mIntegerUserDefined[headerID - 0xF0] = (Long)headerValue; + break; + } + throw new IllegalArgumentException("Invalid Header Identifier"); + } + } + + /** + * Retrieves the value of the header identifier provided. The type of the + * Object returned is defined in the description of this interface. + * @param headerID the header identifier whose value is to be returned + * @return the value of the header provided or null if the + * header identifier specified is not part of this + * HeaderSet object + * @throws IllegalArgumentException if the headerID is not one + * defined in this interface or any of the user-defined headers + * @throws IOException if an error occurred in the transport layer during + * the operation or if the connection has been closed + */ + public Object getHeader(int headerID) throws IOException { + + switch (headerID) { + case COUNT: + return mCount; + case ACTION_ID: + return mActionId; + case NAME: + return mName; + case DEST_NAME: + return mDestName; + case TYPE: + return mType; + case LENGTH: + return mLength; + case PERMISSION: + return mPermission; + case TIME_ISO_8601: + return mIsoTime; + case TIME_4_BYTE: + return mByteTime; + case DESCRIPTION: + return mDescription; + case TARGET: + return mTarget; + case HTTP: + return mHttpHeader; + case WHO: + return mWho; + case CONNECTION_ID: + return mConnectionID; + case OBJECT_CLASS: + return mObjectClass; + case APPLICATION_PARAMETER: + return mAppParam; + case SINGLE_RESPONSE_MODE: + return mSingleRespMode; + case SINGLE_RESPONSE_MODE_PARAMETER: + return mSingleRespModeParam; + default: + // Verify that it was not a Unicode String user Defined + if ((headerID >= 0x30) && (headerID <= 0x3F)) { + return mUnicodeUserDefined[headerID - 0x30]; + } + // Verify that it was not a byte sequence user defined header + if ((headerID >= 0x70) && (headerID <= 0x7F)) { + return mSequenceUserDefined[headerID - 0x70]; + } + // Verify that it was not a byte user defined header + if ((headerID >= 0xB0) && (headerID <= 0xBF)) { + return mByteUserDefined[headerID - 0xB0]; + } + // Verify that it was not a integer user defined header + if ((headerID >= 0xF0) && (headerID <= 0xFF)) { + return mIntegerUserDefined[headerID - 0xF0]; + } + throw new IllegalArgumentException("Invalid Header Identifier"); + } + } + + /** + * Retrieves the list of headers that may be retrieved via the + * getHeader method that will not return null. In + * other words, this method returns all the headers that are available in + * this object. + * @see #getHeader + * @return the array of headers that are set in this object or + * null if no headers are available + * @throws IOException if an error occurred in the transport layer during + * the operation or the connection has been closed + */ + public int[] getHeaderList() throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + if (mCount != null) { + out.write(COUNT); + } + if(mActionId != null) { + out.write(ACTION_ID); + } + if (mName != null) { + out.write(NAME); + } + if (mDestName != null) { + out.write(DEST_NAME); + } + if (mType != null) { + out.write(TYPE); + } + if (mLength != null) { + out.write(LENGTH); + } + if (mPermission != null) { + out.write(PERMISSION); + } + if (mIsoTime != null) { + out.write(TIME_ISO_8601); + } + if (mByteTime != null) { + out.write(TIME_4_BYTE); + } + if (mDescription != null) { + out.write(DESCRIPTION); + } + if (mTarget != null) { + out.write(TARGET); + } + if (mHttpHeader != null) { + out.write(HTTP); + } + if (mWho != null) { + out.write(WHO); + } + if (mAppParam != null) { + out.write(APPLICATION_PARAMETER); + } + if (mObjectClass != null) { + out.write(OBJECT_CLASS); + } + if (mSingleRespMode != null) { + out.write(SINGLE_RESPONSE_MODE); + } + if (mSingleRespModeParam != null) { + out.write(SINGLE_RESPONSE_MODE_PARAMETER); + } + for (int i = 0x30; i < 0x40; i++) { + if (mUnicodeUserDefined[i - 0x30] != null) { + out.write(i); + } + } + + for (int i = 0x70; i < 0x80; i++) { + if (mSequenceUserDefined[i - 0x70] != null) { + out.write(i); + } + } + + for (int i = 0xB0; i < 0xC0; i++) { + if (mByteUserDefined[i - 0xB0] != null) { + out.write(i); + } + } + + for (int i = 0xF0; i < 0x100; i++) { + if (mIntegerUserDefined[i - 0xF0] != null) { + out.write(i); + } + } + + byte[] headers = out.toByteArray(); + out.close(); + + if ((headers == null) || (headers.length == 0)) { + return null; + } + + int[] result = new int[headers.length]; + for (int i = 0; i < headers.length; i++) { + // Convert the byte to a positive integer. That is, an integer + // between 0 and 256. + result[i] = headers[i] & 0xFF; + } + + return result; + } + + /** + * Sets the authentication challenge header. The realm will be + * encoded based upon the default encoding scheme used by the implementation + * to encode strings. Therefore, the encoding scheme used to encode the + * realm is application dependent. + * @param realm a short description that describes what password to use; if + * null no realm will be sent in the authentication + * challenge header + * @param userID if true, a user ID is required in the reply; + * if false, no user ID is required + * @param access if true then full access will be granted if + * successful; if false then read-only access will be + * granted if successful + * @throws IOException + */ + public void createAuthenticationChallenge(String realm, boolean userID, boolean access) + throws IOException { + + nonce = new byte[16]; + for (int i = 0; i < 16; i++) { + nonce[i] = (byte)mRandom.nextInt(); + } + + mAuthChall = ObexHelper.computeAuthenticationChallenge(nonce, realm, access, userID); + } + + /** + * Returns the response code received from the server. Response codes are + * defined in the ResponseCodes class. + * @see ResponseCodes + * @return the response code retrieved from the server + * @throws IOException if an error occurred in the transport layer during + * the transaction; if this method is called on a + * HeaderSet object created by calling + * createHeaderSet() in a ClientSession + * object; if this object was created by an OBEX server + */ + public int getResponseCode() throws IOException { + if (responseCode == -1) { + throw new IOException("May not be called on a server"); + } else { + return responseCode; + } + } +} diff --git a/btobex/javax/btobex/ObexHelper.java b/btobex/javax/btobex/ObexHelper.java new file mode 100755 index 00000000000..f26c1c031d6 --- /dev/null +++ b/btobex/javax/btobex/ObexHelper.java @@ -0,0 +1,1228 @@ +/* + * Copyright (C) 2013 The Linux Foundation. All rights reserved. + * Copyright (c) 2008-2009, Motorola, Inc. + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * - Neither the name of the Motorola, Inc. nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package javax.btobex; + +import android.os.SystemProperties; +import android.util.Log; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; + +/** + * This class defines a set of helper methods for the implementation of Obex. + * @hide + */ +public final class ObexHelper { + private static final String TAG = "ObexHelper"; + public static final boolean VERBOSE = true; + + /* Debugging hooks to control AMP-related operations */ + private static final String DEBUG_FORCE_SRM = "debug.obex.force_srm_capable"; + private static final String DEBUG_FORCE_SRMP = "debug.obex.force_srmp_enabled"; + + /** + * Defines the basic packet length used by OBEX. Every OBEX packet has the + * same basic format:
+ * Byte 0: Request or Response Code Byte 1&2: Length of the packet. + */ + public static final int BASE_PACKET_LENGTH = 3; + + /** Object construction of helper class */ + public ObexHelper() { + } + + /** + * The maximum packet size for OBEX packets that this client can handle. At + * present, this must be changed for each port. TODO: The max packet size + * should be the Max incoming MTU minus TODO: L2CAP package headers and + * RFCOMM package headers. TODO: Retrieve the max incoming MTU from TODO: + * LocalDevice.getProperty(). + */ + /* + * android note set as 0xFFFE to match remote MPS + */ + public static final int MAX_PACKET_SIZE_INT = 0xFDE8; + + /** + * Temporary workaround to be able to push files to Windows 7. + * TODO: Should be removed as soon as Microsoft updates their driver. + */ + public static final int MAX_CLIENT_PACKET_SIZE = 0xFC00; + + public static final int A2DP_SCO_OBEX_MAX_CLIENT_PACKET_SIZE = 0x2000; + + public static final int OBEX_OPCODE_CONNECT = 0x80; + + public static final int OBEX_OPCODE_DISCONNECT = 0x81; + + public static final int OBEX_OPCODE_PUT = 0x02; + + public static final int OBEX_OPCODE_PUT_FINAL = 0x82; + + public static final int OBEX_OPCODE_GET = 0x03; + + public static final int OBEX_OPCODE_GET_FINAL = 0x83; + + public static final int OBEX_OPCODE_RESERVED = 0x04; + + public static final int OBEX_OPCODE_RESERVED_FINAL = 0x84; + + public static final int OBEX_OPCODE_SETPATH = 0x85; + + public static final int OBEX_OPCODE_ACTION = 0x86; + + public static final int OBEX_OPCODE_ABORT = 0xFF; + + public static final int OBEX_AUTH_REALM_CHARSET_ASCII = 0x00; + + public static final int OBEX_AUTH_REALM_CHARSET_ISO_8859_1 = 0x01; + + public static final int OBEX_AUTH_REALM_CHARSET_ISO_8859_2 = 0x02; + + public static final int OBEX_AUTH_REALM_CHARSET_ISO_8859_3 = 0x03; + + public static final int OBEX_AUTH_REALM_CHARSET_ISO_8859_4 = 0x04; + + public static final int OBEX_AUTH_REALM_CHARSET_ISO_8859_5 = 0x05; + + public static final int OBEX_AUTH_REALM_CHARSET_ISO_8859_6 = 0x06; + + public static final int OBEX_AUTH_REALM_CHARSET_ISO_8859_7 = 0x07; + + public static final int OBEX_AUTH_REALM_CHARSET_ISO_8859_8 = 0x08; + + public static final int OBEX_AUTH_REALM_CHARSET_ISO_8859_9 = 0x09; + + public static final int OBEX_AUTH_REALM_CHARSET_UNICODE = 0xFF; + + /** + * The values used for various ACTION commands + */ + public static final Byte OBEX_ACTION_COPY = 0x0; + + public static final Byte OBEX_ACTION_MOVE_RENAME = 0x1; + + public static final Byte OBEX_ACTION_SET_PERM = 0x2; + + /** + * The value that is used for the OBEX Single Response Mode (SRM) + */ + public static final Byte OBEX_SRM_DISABLED = 0x0; + + public static final Byte OBEX_SRM_ENABLED = 0x1; + + public static final Byte OBEX_SRM_SUPPORTED = 0x2; + + /** + * The value that is used for the OBEX Single Response Mode (SRM) Parameters + */ + public static final Byte OBEX_SRM_PARAM_RSVP = 0x0; + + public static final Byte OBEX_SRM_PARAM_WAIT = 0x1; + + public static final Byte OBEX_SRM_PARAM_RSVP_AND_WAIT = 0x2; + + /** An invalid SRM parameter value to indicate no parameter set */ + public static final Byte OBEX_SRM_PARAM_NONE = 0xF; + + /** + * The value that is used for the SRM capability of Bluetooth device + */ + public static final boolean SRM_INCAPABLE = false; + + public static final boolean SRM_CAPABLE = true; + + private boolean mLocalSingleResponseCapability = SRM_INCAPABLE; + + public boolean getLocalSrmCapability() { + if (VERBOSE) Log.v(TAG, "getLocalSrmCapability: " + mLocalSingleResponseCapability); + return mLocalSingleResponseCapability; + } + + public void setLocalSrmCapability(boolean SrmCapable) { + if (!SystemProperties.get(DEBUG_FORCE_SRM).equals("")) { + if (SystemProperties.getBoolean(DEBUG_FORCE_SRM, false)) { + if (VERBOSE) Log.v(TAG, "DEBUG: Forcing SRM on"); + mLocalSingleResponseCapability = SRM_CAPABLE; + } else { + if (VERBOSE) Log.v(TAG, "DEBUG: Forcing SRM off"); + mLocalSingleResponseCapability = SRM_INCAPABLE; + } + } else { + mLocalSingleResponseCapability = SrmCapable ? SRM_CAPABLE : SRM_INCAPABLE; + } + + Log.d(TAG, "setLocalSrmCapability: " + mLocalSingleResponseCapability); + } + + /** + * The value that is used for the SRM status of current operation + */ + public static final boolean LOCAL_SRM_DISABLED = false; + + public static final boolean LOCAL_SRM_ENABLED = true; + + /** + * This is used for the SRM status of local device + */ + private boolean mLocalSingleResponseActive; + + public boolean getLocalSrmStatus() { + if (VERBOSE) Log.v(TAG, "getLocalSrmStatus: " + mLocalSingleResponseActive); + return mLocalSingleResponseActive; + } + + public void setLocalSrmStatus(boolean SrmEnabled) { + mLocalSingleResponseActive = SrmEnabled ? LOCAL_SRM_ENABLED : LOCAL_SRM_DISABLED; + if (VERBOSE) Log.v(TAG, "setLocalSrmStatus: " + mLocalSingleResponseActive); + } + + /** + * This is used for the SRM status of remote device + */ + private boolean mRemoteSingleResponseActive; + + public boolean getRemoteSrmStatus() { + if (VERBOSE) Log.v(TAG, "getRemoteSrmStatus: " + mRemoteSingleResponseActive); + return mRemoteSingleResponseActive; + } + + public void setRemoteSrmStatus(boolean SrmCapable) { + mRemoteSingleResponseActive = SrmCapable ? SRM_CAPABLE : SRM_INCAPABLE; + if (VERBOSE) Log.v(TAG, "setRemoteSrmStatus: " + mRemoteSingleResponseActive); + } + + /** + * This is used for the Single Response Mode Parameters (SRMP) value + */ + private boolean mLocalSrmpWait; + + public boolean getLocalSrmpWait() { + if (VERBOSE) Log.v(TAG, "getLocalSrmpWait: " + mLocalSrmpWait); + return mLocalSrmpWait; + } + + public void setLocalSrmpWait(boolean SrmpWait) { + if (VERBOSE) Log.v(TAG, "setLocalSrmpWait: " + SrmpWait); + mLocalSrmpWait = SrmpWait; + } + + /** + * The value that is used for the SRMP status of local device + */ + public static final boolean SRMP_DISABLED = false; + + public static final boolean SRMP_ENABLED = true; + + private boolean mLocalSrmpActive = SRMP_DISABLED; + + public boolean getLocalSrmParamStatus() { + if (!SystemProperties.get(DEBUG_FORCE_SRMP).equals("")) { + if (SystemProperties.getBoolean(DEBUG_FORCE_SRMP, false)) { + if (VERBOSE) Log.v(TAG, "DEBUG: Forcing SRMP on"); + mLocalSrmpActive = SRMP_ENABLED; + } else { + if (VERBOSE) Log.v(TAG, "DEBUG: Forcing SRMP off"); + mLocalSrmpActive = SRMP_DISABLED; + } + } + if (VERBOSE) Log.v(TAG, "getLocalSrmParamStatus: " + mLocalSrmpActive); + return mLocalSrmpActive; + } + + public void setLocalSrmParamStatus(boolean SrmpEnabled) { + if (!SystemProperties.get(DEBUG_FORCE_SRMP).equals("")) { + if (SystemProperties.getBoolean(DEBUG_FORCE_SRMP, false)) { + if (VERBOSE) Log.v(TAG, "DEBUG: Forcing SRMP on"); + mLocalSrmpActive = SRMP_ENABLED; + } else { + if (VERBOSE) Log.v(TAG, "DEBUG: Forcing SRMP off"); + mLocalSrmpActive = SRMP_DISABLED; + } + } else { + mLocalSrmpActive = SrmpEnabled ? SRMP_ENABLED : SRMP_DISABLED; + } + if (VERBOSE) Log.v(TAG, "setLocalSrmParamStatus: " + mLocalSrmpActive); + } + + /** + * This is used to reset the SRM status to default + */ + public void resetSrmStatus() { + if (VERBOSE) Log.v(TAG, "resetSrmStatus"); + setLocalSrmStatus(LOCAL_SRM_DISABLED); + setLocalSrmpWait(false); + setRemoteSrmStatus(SRM_INCAPABLE); + } + + /** + * Updates the HeaderSet with the headers received in the byte array + * provided. Invalid headers are ignored. + *

+ * The first two bits of an OBEX Header specifies the type of object that is + * being sent. The table below specifies the meaning of the high bits. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Bits 8 and 7ValueDescription
000x00Null Terminated Unicode text, prefixed with 2 byte unsigned integer
010x40Byte Sequence, length prefixed with 2 byte unsigned integer
100x801 byte quantity
110xC04 byte quantity - transmitted in network byte order (high byte first
+ * This method uses the information in this table to determine the type of + * Java object to create and passes that object with the full header to + * setHeader() to update the HeaderSet object. Invalid headers will cause an + * exception to be thrown. When it is thrown, it is ignored. + * @param header the HeaderSet to update + * @param headerArray the byte array containing headers + * @return the result of the last start body or end body header provided; + * the first byte in the result will specify if a body or end of + * body is received + * @throws IOException if an invalid header was found + */ + public static byte[] updateHeaderSet(HeaderSet header, byte[] headerArray) throws IOException { + int index = 0; + int length = 0; + int headerID; + byte[] value = null; + byte[] body = null; + HeaderSet headerImpl = header; + try { + while (index < headerArray.length) { + headerID = 0xFF & headerArray[index]; + if (VERBOSE) Log.e(TAG,"updateHeaderSet headerID = "+ headerID); + switch (headerID & (0xC0)) { + + /* + * 0x00 is a unicode null terminate string with the first + * two bytes after the header identifier being the length + */ + case 0x00: + // Fall through + /* + * 0x40 is a byte sequence with the first + * two bytes after the header identifier being the length + */ + case 0x40: + boolean trimTail = true; + index++; + length = 0xFF & headerArray[index]; + length = length << 8; + index++; + length += 0xFF & headerArray[index]; + length -= 3; + index++; + value = new byte[length]; + System.arraycopy(headerArray, index, value, 0, length); + if (length == 0 || (length > 0 && (value[length - 1] != 0))) { + trimTail = false; + } + switch (headerID) { + case HeaderSet.TYPE: + try { + // Remove trailing null + if (trimTail == false) { + headerImpl.setHeader(headerID, new String(value, 0, + value.length, "ISO8859_1")); + } else { + headerImpl.setHeader(headerID, new String(value, 0, + value.length - 1, "ISO8859_1")); + } + } catch (UnsupportedEncodingException e) { + throw e; + } + break; + + case HeaderSet.AUTH_CHALLENGE: + headerImpl.mAuthChall = new byte[length]; + System.arraycopy(headerArray, index, headerImpl.mAuthChall, 0, + length); + break; + + case HeaderSet.AUTH_RESPONSE: + headerImpl.mAuthResp = new byte[length]; + System.arraycopy(headerArray, index, headerImpl.mAuthResp, 0, + length); + break; + + case HeaderSet.BODY: + /* Fall Through */ + case HeaderSet.END_OF_BODY: + body = new byte[length + 1]; + body[0] = (byte)headerID; + System.arraycopy(headerArray, index, body, 1, length); + break; + + case HeaderSet.TIME_ISO_8601: + try { + String dateString = new String(value, "ISO8859_1"); + Calendar temp = Calendar.getInstance(); + if ((dateString.length() == 16) + && (dateString.charAt(15) == 'Z')) { + temp.setTimeZone(TimeZone.getTimeZone("UTC")); + } + temp.set(Calendar.YEAR, Integer.parseInt(dateString.substring( + 0, 4))); + temp.set(Calendar.MONTH, Integer.parseInt(dateString.substring( + 4, 6))); + temp.set(Calendar.DAY_OF_MONTH, Integer.parseInt(dateString + .substring(6, 8))); + temp.set(Calendar.HOUR_OF_DAY, Integer.parseInt(dateString + .substring(9, 11))); + temp.set(Calendar.MINUTE, Integer.parseInt(dateString + .substring(11, 13))); + temp.set(Calendar.SECOND, Integer.parseInt(dateString + .substring(13, 15))); + headerImpl.setHeader(HeaderSet.TIME_ISO_8601, temp); + } catch (UnsupportedEncodingException e) { + throw e; + } + break; + + default: + if ((headerID & 0xC0) == 0x00) { + headerImpl.setHeader(headerID, ObexHelper.convertToUnicode( + value, true)); + } else { + headerImpl.setHeader(headerID, value); + } + } + + index += length; + break; + + /* + * 0x80 is a byte header. The only valid byte headers are + * the 16 user defined byte headers. + */ + case 0x80: + index++; + try { + headerImpl.setHeader(headerID, Byte.valueOf(headerArray[index])); + } catch (Exception e) { + // Not a valid header so ignore + } + index++; + break; + + /* + * 0xC0 is a 4 byte unsigned integer header and with the + * exception of TIME_4_BYTE will be converted to a Long + * and added. + */ + case 0xC0: + index++; + value = new byte[4]; + System.arraycopy(headerArray, index, value, 0, 4); + try { + if (headerID != HeaderSet.TIME_4_BYTE) { + // Determine if it is a connection ID. These + // need to be handled differently + if (headerID == HeaderSet.CONNECTION_ID) { + headerImpl.mConnectionID = new byte[4]; + System.arraycopy(value, 0, headerImpl.mConnectionID, 0, 4); + } else { + headerImpl.setHeader(headerID, Long + .valueOf(convertToLong(value))); + } + } else { + Calendar temp = Calendar.getInstance(); + temp.setTime(new Date(convertToLong(value) * 1000L)); + headerImpl.setHeader(HeaderSet.TIME_4_BYTE, temp); + } + } catch (Exception e) { + // Not a valid header so ignore + throw new IOException("Header was not formatted properly"); + } + index += 4; + break; + } + + } + } catch (IOException e) { + throw new IOException("Header was not formatted properly"); + } + + return body; + } + + /** + * Creates the header part of OBEX packet based on the header provided. + * TODO: Could use getHeaderList() to get the array of headers to include + * and then use the high two bits to determine the the type of the object + * and construct the byte array from that. This will make the size smaller. + * @param head the header used to construct the byte array + * @param nullOut true if the header should be set to + * null once it is added to the array or + * false if it should not be nulled out + * @return the header of an OBEX packet + */ + public static byte[] createHeader(HeaderSet head, boolean nullOut) { + Long intHeader = null; + String stringHeader = null; + Calendar dateHeader = null; + Byte byteHeader = null; + StringBuffer buffer = null; + byte[] value = null; + byte[] result = null; + byte[] lengthArray = new byte[2]; + int length; + HeaderSet headImpl = null; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + headImpl = head; + + try { + /* + * Determine if there is a connection ID to send. If there is, + * then it should be the first header in the packet. + */ + if (VERBOSE) Log.e(TAG,"createHeader = "+ head); + if ((headImpl.mConnectionID != null) && (headImpl.getHeader(HeaderSet.TARGET) == null)) { + if (VERBOSE) Log.v(TAG," Add Header = "+ HeaderSet.CONNECTION_ID); + out.write((byte)HeaderSet.CONNECTION_ID); + out.write(headImpl.mConnectionID); + } + + // Count Header + intHeader = (Long)headImpl.getHeader(HeaderSet.COUNT); + if (intHeader != null) { + if (VERBOSE) Log.v(TAG," Add Header = "+ HeaderSet.COUNT); + out.write((byte)HeaderSet.COUNT); + value = ObexHelper.convertToByteArray(intHeader.longValue()); + out.write(value); + if (nullOut) { + headImpl.setHeader(HeaderSet.COUNT, null); + } + } + + // Name Header + stringHeader = (String)headImpl.getHeader(HeaderSet.NAME); + if (stringHeader != null) { + if (VERBOSE) Log.v(TAG," Add Header = "+ HeaderSet.NAME); + out.write((byte)HeaderSet.NAME); + value = ObexHelper.convertToUnicodeByteArray(stringHeader); + length = value.length + 3; + lengthArray[0] = (byte)(0xFF & (length >> 8)); + lengthArray[1] = (byte)(0xFF & length); + out.write(lengthArray); + out.write(value); + if (nullOut) { + headImpl.setHeader(HeaderSet.NAME, null); + } + } else if (headImpl.getEmptyNameHeader()) { + out.write((byte) HeaderSet.NAME); + lengthArray[0] = (byte) 0x00; + lengthArray[1] = (byte) 0x03; + out.write(lengthArray); + } + // Dest Name Header + stringHeader = (String)headImpl.getHeader(HeaderSet.DEST_NAME); + if (stringHeader != null) { + if (VERBOSE) Log.v(TAG," Add Header = "+ HeaderSet.DEST_NAME); + out.write((byte)HeaderSet.DEST_NAME); + value = ObexHelper.convertToUnicodeByteArray(stringHeader); + length = value.length + 3; + lengthArray[0] = (byte)(0xFF & (length >> 8)); + lengthArray[1] = (byte)(0xFF & length); + out.write(lengthArray); + out.write(value); + if (nullOut) { + headImpl.setHeader(HeaderSet.DEST_NAME, null); + } + } + // Type Header + stringHeader = (String)headImpl.getHeader(HeaderSet.TYPE); + if (stringHeader != null) { + if (VERBOSE) Log.v(TAG," Add Header = "+ HeaderSet.TYPE); + out.write((byte)HeaderSet.TYPE); + try { + value = stringHeader.getBytes("ISO8859_1"); + } catch (UnsupportedEncodingException e) { + throw e; + } + + length = value.length + 4; + lengthArray[0] = (byte)(255 & (length >> 8)); + lengthArray[1] = (byte)(255 & length); + out.write(lengthArray); + out.write(value); + out.write(0x00); + if (nullOut) { + headImpl.setHeader(HeaderSet.TYPE, null); + } + } + + // Length Header + intHeader = (Long)headImpl.getHeader(HeaderSet.LENGTH); + if (intHeader != null) { + if (VERBOSE) Log.v(TAG," Add Header = "+ HeaderSet.LENGTH); + out.write((byte)HeaderSet.LENGTH); + value = ObexHelper.convertToByteArray(intHeader.longValue()); + out.write(value); + if (nullOut) { + headImpl.setHeader(HeaderSet.LENGTH, null); + } + } + + // Time ISO Header + dateHeader = (Calendar)headImpl.getHeader(HeaderSet.TIME_ISO_8601); + if (dateHeader != null) { + + /* + * The ISO Header should take the form YYYYMMDDTHHMMSSZ. The + * 'Z' will only be included if it is a UTC time. + */ + buffer = new StringBuffer(); + int temp = dateHeader.get(Calendar.YEAR); + for (int i = temp; i < 1000; i = i * 10) { + buffer.append("0"); + } + buffer.append(temp); + temp = dateHeader.get(Calendar.MONTH); + if (temp < 10) { + buffer.append("0"); + } + buffer.append(temp); + temp = dateHeader.get(Calendar.DAY_OF_MONTH); + if (temp < 10) { + buffer.append("0"); + } + buffer.append(temp); + buffer.append("T"); + temp = dateHeader.get(Calendar.HOUR_OF_DAY); + if (temp < 10) { + buffer.append("0"); + } + buffer.append(temp); + temp = dateHeader.get(Calendar.MINUTE); + if (temp < 10) { + buffer.append("0"); + } + buffer.append(temp); + temp = dateHeader.get(Calendar.SECOND); + if (temp < 10) { + buffer.append("0"); + } + buffer.append(temp); + + if (dateHeader.getTimeZone().getID().equals("UTC")) { + buffer.append("Z"); + } + + try { + value = buffer.toString().getBytes("ISO8859_1"); + } catch (UnsupportedEncodingException e) { + throw e; + } + + length = value.length + 3; + lengthArray[0] = (byte)(255 & (length >> 8)); + lengthArray[1] = (byte)(255 & length); + out.write(HeaderSet.TIME_ISO_8601); + out.write(lengthArray); + out.write(value); + if (nullOut) { + headImpl.setHeader(HeaderSet.TIME_ISO_8601, null); + } + } + + // Time 4 Byte Header + dateHeader = (Calendar)headImpl.getHeader(HeaderSet.TIME_4_BYTE); + if (dateHeader != null) { + out.write(HeaderSet.TIME_4_BYTE); + + /* + * Need to call getTime() twice. The first call will return + * a java.util.Date object. The second call returns the number + * of milliseconds since January 1, 1970. We need to convert + * it to seconds since the TIME_4_BYTE expects the number of + * seconds since January 1, 1970. + */ + value = ObexHelper.convertToByteArray(dateHeader.getTime().getTime() / 1000L); + out.write(value); + if (nullOut) { + headImpl.setHeader(HeaderSet.TIME_4_BYTE, null); + } + } + + // Description Header + stringHeader = (String)headImpl.getHeader(HeaderSet.DESCRIPTION); + if (stringHeader != null) { + out.write((byte)HeaderSet.DESCRIPTION); + value = ObexHelper.convertToUnicodeByteArray(stringHeader); + length = value.length + 3; + lengthArray[0] = (byte)(255 & (length >> 8)); + lengthArray[1] = (byte)(255 & length); + out.write(lengthArray); + out.write(value); + if (nullOut) { + headImpl.setHeader(HeaderSet.DESCRIPTION, null); + } + } + + // Target Header + value = (byte[])headImpl.getHeader(HeaderSet.TARGET); + if (value != null) { + if (VERBOSE) Log.v(TAG," Add Header = "+ HeaderSet.TARGET); + out.write((byte)HeaderSet.TARGET); + length = value.length + 3; + lengthArray[0] = (byte)(255 & (length >> 8)); + lengthArray[1] = (byte)(255 & length); + out.write(lengthArray); + out.write(value); + if (nullOut) { + headImpl.setHeader(HeaderSet.TARGET, null); + } + } + + // HTTP Header + value = (byte[])headImpl.getHeader(HeaderSet.HTTP); + if (value != null) { + out.write((byte)HeaderSet.HTTP); + length = value.length + 3; + lengthArray[0] = (byte)(255 & (length >> 8)); + lengthArray[1] = (byte)(255 & length); + out.write(lengthArray); + out.write(value); + if (nullOut) { + headImpl.setHeader(HeaderSet.HTTP, null); + } + } + + // Who Header + value = (byte[])headImpl.getHeader(HeaderSet.WHO); + if (value != null) { + if (VERBOSE) Log.v(TAG," Add Header = "+ HeaderSet.WHO); + out.write((byte)HeaderSet.WHO); + length = value.length + 3; + lengthArray[0] = (byte)(255 & (length >> 8)); + lengthArray[1] = (byte)(255 & length); + out.write(lengthArray); + out.write(value); + if (nullOut) { + headImpl.setHeader(HeaderSet.WHO, null); + } + } + + // Application Parameter Header + value = (byte[])headImpl.getHeader(HeaderSet.APPLICATION_PARAMETER); + if (value != null) { + out.write((byte)HeaderSet.APPLICATION_PARAMETER); + length = value.length + 3; + lengthArray[0] = (byte)(255 & (length >> 8)); + lengthArray[1] = (byte)(255 & length); + out.write(lengthArray); + out.write(value); + if (nullOut) { + headImpl.setHeader(HeaderSet.APPLICATION_PARAMETER, null); + } + } + + // Object Class Header + value = (byte[])headImpl.getHeader(HeaderSet.OBJECT_CLASS); + if (value != null) { + out.write((byte)HeaderSet.OBJECT_CLASS); + length = value.length + 3; + lengthArray[0] = (byte)(255 & (length >> 8)); + lengthArray[1] = (byte)(255 & length); + out.write(lengthArray); + out.write(value); + if (nullOut) { + headImpl.setHeader(HeaderSet.OBJECT_CLASS, null); + } + } + + // Single Response Mode (SRM) Header + byteHeader = (Byte)headImpl.getHeader(HeaderSet.SINGLE_RESPONSE_MODE); + if (byteHeader != null) { + if (VERBOSE) Log.v(TAG," Add Header = "+ HeaderSet.SINGLE_RESPONSE_MODE); + out.write((byte)HeaderSet.SINGLE_RESPONSE_MODE); + out.write(byteHeader.byteValue()); + if (VERBOSE) Log.v(TAG," Add SRM value = "+ byteHeader.byteValue()); + if (nullOut) { + headImpl.setHeader(HeaderSet.SINGLE_RESPONSE_MODE, null); + } + } + + // Single Response Mode (SRM) Parameter Header + byteHeader = (Byte)headImpl.getHeader(HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER); + if (byteHeader != null) { + if (VERBOSE) Log.v(TAG," Add Header = "+ HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER); + out.write((byte)HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER); + out.write(byteHeader.byteValue()); + if (nullOut) { + headImpl.setHeader(HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER, null); + } + } + + // Check User Defined Headers + for (int i = 0; i < 16; i++) { + + //Unicode String Header + stringHeader = (String)headImpl.getHeader(i + 0x30); + if (stringHeader != null) { + out.write((byte)i + 0x30); + value = ObexHelper.convertToUnicodeByteArray(stringHeader); + length = value.length + 3; + lengthArray[0] = (byte)(255 & (length >> 8)); + lengthArray[1] = (byte)(255 & length); + out.write(lengthArray); + out.write(value); + if (nullOut) { + headImpl.setHeader(i + 0x30, null); + } + } + + // Byte Sequence Header + value = (byte[])headImpl.getHeader(i + 0x70); + if (value != null) { + out.write((byte)i + 0x70); + length = value.length + 3; + lengthArray[0] = (byte)(255 & (length >> 8)); + lengthArray[1] = (byte)(255 & length); + out.write(lengthArray); + out.write(value); + if (nullOut) { + headImpl.setHeader(i + 0x70, null); + } + } + + // Byte Header + byteHeader = (Byte)headImpl.getHeader(i + 0xB0); + if (byteHeader != null) { + out.write((byte)i + 0xB0); + out.write(byteHeader.byteValue()); + if (nullOut) { + headImpl.setHeader(i + 0xB0, null); + } + } + + // Integer header + intHeader = (Long)headImpl.getHeader(i + 0xF0); + if (intHeader != null) { + out.write((byte)i + 0xF0); + out.write(ObexHelper.convertToByteArray(intHeader.longValue())); + if (nullOut) { + headImpl.setHeader(i + 0xF0, null); + } + } + } + + // Add the authentication challenge header + if (headImpl.mAuthChall != null) { + out.write((byte)HeaderSet.AUTH_CHALLENGE); + length = headImpl.mAuthChall.length + 3; + lengthArray[0] = (byte)(255 & (length >> 8)); + lengthArray[1] = (byte)(255 & length); + out.write(lengthArray); + out.write(headImpl.mAuthChall); + if (nullOut) { + headImpl.mAuthChall = null; + } + } + + // Add the authentication response header + if (headImpl.mAuthResp != null) { + out.write((byte)HeaderSet.AUTH_RESPONSE); + length = headImpl.mAuthResp.length + 3; + lengthArray[0] = (byte)(255 & (length >> 8)); + lengthArray[1] = (byte)(255 & length); + out.write(lengthArray); + out.write(headImpl.mAuthResp); + if (nullOut) { + headImpl.mAuthResp = null; + } + } + + } catch (IOException e) { + } finally { + result = out.toByteArray(); + try { + out.close(); + } catch (Exception ex) { + } + } + + return result; + + } + + /** + * Determines where the maximum divide is between headers. This method is + * used by put and get operations to separate headers to a size that meets + * the max packet size allowed. + * @param headerArray the headers to separate + * @param start the starting index to search + * @param maxSize the maximum size of a packet + * @return the index of the end of the header block to send or -1 if the + * header could not be divided because the header is too large + */ + public static int findHeaderEnd(byte[] headerArray, int start, int maxSize) { + + int fullLength = 0; + int lastLength = -1; + int index = start; + int length = 0; + + while ((fullLength < maxSize) && (index < headerArray.length)) { + int headerID = (headerArray[index] < 0 ? headerArray[index] + 256 : headerArray[index]); + lastLength = fullLength; + + switch (headerID & (0xC0)) { + + case 0x00: + // Fall through + case 0x40: + + index++; + length = (headerArray[index] < 0 ? headerArray[index] + 256 + : headerArray[index]); + length = length << 8; + index++; + length += (headerArray[index] < 0 ? headerArray[index] + 256 + : headerArray[index]); + length -= 3; + index++; + index += length; + fullLength += length + 3; + break; + + case 0x80: + + index++; + index++; + fullLength += 2; + break; + + case 0xC0: + + index += 5; + fullLength += 5; + break; + + } + + } + + /* + * Determine if this is the last header or not + */ + if (lastLength == 0) { + /* + * Since this is the last header, check to see if the size of this + * header is less then maxSize. If it is, return the length of the + * header, otherwise return -1. The length of the header is + * returned since it would be the start of the next header + */ + if (fullLength < maxSize) { + return headerArray.length; + } else { + return -1; + } + } else { + return lastLength + start; + } + } + + /** + * Converts the byte array to a long. + * @param b the byte array to convert to a long + * @return the byte array as a long + */ + public static long convertToLong(byte[] b) { + long result = 0; + long value = 0; + long power = 0; + + for (int i = (b.length - 1); i >= 0; i--) { + value = b[i]; + if (value < 0) { + value += 256; + } + + result = result | (value << power); + power += 8; + } + + return result; + } + + /** + * Converts the long to a 4 byte array. The long must be non negative. + * @param l the long to convert + * @return a byte array that is the same as the long + */ + public static byte[] convertToByteArray(long l) { + byte[] b = new byte[4]; + + b[0] = (byte)(255 & (l >> 24)); + b[1] = (byte)(255 & (l >> 16)); + b[2] = (byte)(255 & (l >> 8)); + b[3] = (byte)(255 & l); + + return b; + } + + /** + * Converts the String to a UNICODE byte array. It will also add the ending + * null characters to the end of the string. + * @param s the string to convert + * @return the unicode byte array of the string + */ + public static byte[] convertToUnicodeByteArray(String s) { + if (s == null) { + return null; + } + + char c[] = s.toCharArray(); + byte[] result = new byte[(c.length * 2) + 2]; + for (int i = 0; i < c.length; i++) { + result[(i * 2)] = (byte)(c[i] >> 8); + result[((i * 2) + 1)] = (byte)c[i]; + } + + // Add the UNICODE null character + result[result.length - 2] = 0; + result[result.length - 1] = 0; + + return result; + } + + /** + * Retrieves the value from the byte array for the tag value specified. The + * array should be of the form Tag - Length - Value triplet. + * @param tag the tag to retrieve from the byte array + * @param triplet the byte sequence containing the tag length value form + * @return the value of the specified tag + */ + public static byte[] getTagValue(byte tag, byte[] triplet) { + + int index = findTag(tag, triplet); + if (index == -1) { + return null; + } + + index++; + int length = triplet[index] & 0xFF; + + byte[] result = new byte[length]; + index++; + System.arraycopy(triplet, index, result, 0, length); + + return result; + } + + /** + * Finds the index that starts the tag value pair in the byte array provide. + * @param tag the tag to look for + * @param value the byte array to search + * @return the starting index of the tag or -1 if the tag could not be found + */ + public static int findTag(byte tag, byte[] value) { + int length = 0; + + if (value == null) { + return -1; + } + + int index = 0; + + while ((index < value.length) && (value[index] != tag)) { + length = value[index + 1] & 0xFF; + index += length + 2; + } + + if (index >= value.length) { + return -1; + } + + return index; + } + + /** + * Converts the byte array provided to a unicode string. + * @param b the byte array to convert to a string + * @param includesNull determine if the byte string provided contains the + * UNICODE null character at the end or not; if it does, it will be + * removed + * @return a Unicode string + * @throws IllegalArgumentException if the byte array has an odd length + */ + public static String convertToUnicode(byte[] b, boolean includesNull) { + if (b == null || b.length == 0) { + return null; + } + int arrayLength = b.length; + if (!((arrayLength % 2) == 0)) { + throw new IllegalArgumentException("Byte array not of a valid form"); + } + arrayLength = (arrayLength >> 1); + if (includesNull) { + arrayLength -= 1; + } + + char[] c = new char[arrayLength]; + for (int i = 0; i < arrayLength; i++) { + int upper = b[2 * i]; + int lower = b[(2 * i) + 1]; + if (upper < 0) { + upper += 256; + } + if (lower < 0) { + lower += 256; + } + // If upper and lower both equal 0, it should be the end of string. + // Ignore left bytes from array to avoid potential issues + if (upper == 0 && lower == 0) { + return new String(c, 0, i); + } + + c[i] = (char)((upper << 8) | lower); + } + + return new String(c); + } + + /** + * Compute the MD5 hash of the byte array provided. Does not accumulate + * input. + * @param in the byte array to hash + * @return the MD5 hash of the byte array + */ + public static byte[] computeMd5Hash(byte[] in) { + try { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + return md5.digest(in); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + /** + * Computes an authentication challenge header. + * @param nonce the challenge that will be provided to the peer; the + * challenge must be 16 bytes long + * @param realm a short description that describes what password to use + * @param access if true then full access will be granted if + * successful; if false then read only access will be + * granted if successful + * @param userID if true, a user ID is required in the reply; + * if false, no user ID is required + * @throws IllegalArgumentException if the challenge is not 16 bytes long; + * if the realm can not be encoded in less then 255 bytes + * @throws IOException if the encoding scheme ISO 8859-1 is not supported + */ + public static byte[] computeAuthenticationChallenge(byte[] nonce, String realm, boolean access, + boolean userID) throws IOException { + byte[] authChall = null; + + if (nonce.length != 16) { + throw new IllegalArgumentException("Nonce must be 16 bytes long"); + } + + /* + * The authentication challenge is a byte sequence of the following form + * byte 0: 0x00 - the tag for the challenge + * byte 1: 0x10 - the length of the challenge; must be 16 + * byte 2-17: the authentication challenge + * byte 18: 0x01 - the options tag; this is optional in the spec, but + * we are going to include it in every message + * byte 19: 0x01 - length of the options; must be 1 + * byte 20: the value of the options; bit 0 is set if user ID is + * required; bit 1 is set if access mode is read only + * byte 21: 0x02 - the tag for authentication realm; only included if + * an authentication realm is specified + * byte 22: the length of the authentication realm; only included if + * the authentication realm is specified + * byte 23: the encoding scheme of the authentication realm; we will use + * the ISO 8859-1 encoding scheme since it is part of the KVM + * byte 24 & up: the realm if one is specified. + */ + if (realm == null) { + authChall = new byte[21]; + } else { + if (realm.length() >= 255) { + throw new IllegalArgumentException("Realm must be less then 255 bytes"); + } + authChall = new byte[24 + realm.length()]; + authChall[21] = 0x02; + authChall[22] = (byte)(realm.length() + 1); + authChall[23] = 0x01; // ISO 8859-1 Encoding + System.arraycopy(realm.getBytes("ISO8859_1"), 0, authChall, 24, realm.length()); + } + + // Include the nonce field in the header + authChall[0] = 0x00; + authChall[1] = 0x10; + System.arraycopy(nonce, 0, authChall, 2, 16); + + // Include the options header + authChall[18] = 0x01; + authChall[19] = 0x01; + authChall[20] = 0x00; + + if (!access) { + authChall[20] = (byte)(authChall[20] | 0x02); + } + if (userID) { + authChall[20] = (byte)(authChall[20] | 0x01); + } + + return authChall; + } +} diff --git a/btobex/javax/btobex/ObexSession.java b/btobex/javax/btobex/ObexSession.java new file mode 100755 index 00000000000..59f56785810 --- /dev/null +++ b/btobex/javax/btobex/ObexSession.java @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * Copyright (c) 2008-2009, Motorola, Inc. + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * - Neither the name of the Motorola, Inc. nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package javax.btobex; + +import java.io.IOException; + +/** + * The ObexSession interface characterizes the term + * "OBEX Connection" as defined in the IrDA Object Exchange Protocol v1.2, which + * could be the server-side view of an OBEX connection, or the client-side view + * of the same connection, which is established by server's accepting of a + * client issued "CONNECT". + *

+ * This interface serves as the common super class for + * ClientSession and ServerSession. + * @hide + */ +public class ObexSession { + + protected Authenticator mAuthenticator; + + protected byte[] mChallengeDigest; + + /** + * Called when the server received an authentication challenge header. This + * will cause the authenticator to handle the authentication challenge. + * @param header the header with the authentication challenge + * @return true if the last request should be resent; + * false if the last request should not be resent + * @throws IOException + */ + public boolean handleAuthChall(HeaderSet header) throws IOException { + if (mAuthenticator == null) { + return false; + } + + /* + * An authentication challenge is made up of one required and two + * optional tag length value triplets. The tag 0x00 is required to be in + * the authentication challenge and it represents the challenge digest + * that was received. The tag 0x01 is the options tag. This tag tracks + * if user ID is required and if full access will be granted. The tag + * 0x02 is the realm, which provides a description of which user name + * and password to use. + */ + byte[] challenge = ObexHelper.getTagValue((byte)0x00, header.mAuthChall); + byte[] option = ObexHelper.getTagValue((byte)0x01, header.mAuthChall); + byte[] description = ObexHelper.getTagValue((byte)0x02, header.mAuthChall); + + String realm = null; + if (description != null) { + byte[] realmString = new byte[description.length - 1]; + System.arraycopy(description, 1, realmString, 0, realmString.length); + + switch (description[0] & 0xFF) { + + case ObexHelper.OBEX_AUTH_REALM_CHARSET_ASCII: + // ASCII encoding + // Fall through + case ObexHelper.OBEX_AUTH_REALM_CHARSET_ISO_8859_1: + // ISO-8859-1 encoding + try { + realm = new String(realmString, "ISO8859_1"); + } catch (Exception e) { + throw new IOException("Unsupported Encoding Scheme"); + } + break; + + case ObexHelper.OBEX_AUTH_REALM_CHARSET_UNICODE: + // UNICODE Encoding + realm = ObexHelper.convertToUnicode(realmString, false); + break; + + default: + throw new IOException("Unsupported Encoding Scheme"); + } + } + + boolean isUserIDRequired = false; + boolean isFullAccess = true; + if (option != null) { + if ((option[0] & 0x01) != 0) { + isUserIDRequired = true; + } + + if ((option[0] & 0x02) != 0) { + isFullAccess = false; + } + } + + PasswordAuthentication result = null; + header.mAuthChall = null; + + try { + result = mAuthenticator + .onAuthenticationChallenge(realm, isUserIDRequired, isFullAccess); + } catch (Exception e) { + return false; + } + + /* + * If no password is provided then we not resent the request + */ + if (result == null) { + return false; + } + + byte[] password = result.getPassword(); + if (password == null) { + return false; + } + + byte[] userName = result.getUserName(); + + /* + * Create the authentication response header. It includes 1 required and + * 2 option tag length value triples. The required triple has a tag of + * 0x00 and is the response digest. The first optional tag is 0x01 and + * represents the user ID. If no user ID is provided, then no user ID + * will be sent. The second optional tag is 0x02 and is the challenge + * that was received. This will always be sent + */ + if (userName != null) { + header.mAuthResp = new byte[38 + userName.length]; + header.mAuthResp[36] = (byte)0x01; + header.mAuthResp[37] = (byte)userName.length; + System.arraycopy(userName, 0, header.mAuthResp, 38, userName.length); + } else { + header.mAuthResp = new byte[36]; + } + + // Create the secret String + byte[] digest = new byte[challenge.length + password.length + 1]; + System.arraycopy(challenge, 0, digest, 0, challenge.length); + // Insert colon between challenge and password + digest[challenge.length] = (byte)0x3A; + System.arraycopy(password, 0, digest, challenge.length + 1, password.length); + + // Add the Response Digest + header.mAuthResp[0] = (byte)0x00; + header.mAuthResp[1] = (byte)0x10; + + System.arraycopy(ObexHelper.computeMd5Hash(digest), 0, header.mAuthResp, 2, 16); + + // Add the challenge + header.mAuthResp[18] = (byte)0x02; + header.mAuthResp[19] = (byte)0x10; + System.arraycopy(challenge, 0, header.mAuthResp, 20, 16); + + return true; + } + + /** + * Called when the server received an authentication response header. This + * will cause the authenticator to handle the authentication response. + * @param authResp the authentication response + * @return true if the response passed; false if + * the response failed + */ + public boolean handleAuthResp(byte[] authResp) { + if (mAuthenticator == null) { + return false; + } + // get the correct password from the application + byte[] correctPassword = mAuthenticator.onAuthenticationResponse(ObexHelper.getTagValue( + (byte)0x01, authResp)); + if (correctPassword == null) { + return false; + } + + byte[] temp = new byte[correctPassword.length + 16]; + + System.arraycopy(mChallengeDigest, 0, temp, 0, 16); + System.arraycopy(correctPassword, 0, temp, 16, correctPassword.length); + + byte[] correctResponse = ObexHelper.computeMd5Hash(temp); + byte[] actualResponse = ObexHelper.getTagValue((byte)0x00, authResp); + + // compare the MD5 hash array . + for (int i = 0; i < 16; i++) { + if (correctResponse[i] != actualResponse[i]) { + return false; + } + } + + return true; + } +} diff --git a/btobex/javax/btobex/ObexTransport.java b/btobex/javax/btobex/ObexTransport.java new file mode 100755 index 00000000000..b794386fddb --- /dev/null +++ b/btobex/javax/btobex/ObexTransport.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * Copyright (c) 2008-2009, Motorola, Inc. + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * - Neither the name of the Motorola, Inc. nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package javax.btobex; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * The ObexTransport interface defines the underlying transport + * connection which carries the OBEX protocol( such as TCP, RFCOMM device file + * exposed by Bluetooth or USB in kernel, RFCOMM socket emulated in Android + * platform, Irda). This interface provides an abstract layer to be used by the + * ObexConnection. Each kind of medium shall have its own + * implementation to wrap and follow the same interface. + *

+ * See section 1.2.2 of IrDA Object Exchange Protocol specification. + *

+ * Different kind of medium may have different construction - for example, the + * RFCOMM device file medium may be constructed from a file descriptor or simply + * a string while the TCP medium usually from a socket. + * @hide + */ +public interface ObexTransport { + + void create() throws IOException; + + void listen() throws IOException; + + void close() throws IOException; + + void connect() throws IOException; + + void disconnect() throws IOException; + + InputStream openInputStream() throws IOException; + + OutputStream openOutputStream() throws IOException; + + DataInputStream openDataInputStream() throws IOException; + + DataOutputStream openDataOutputStream() throws IOException; + +} diff --git a/btobex/javax/btobex/Operation.java b/btobex/javax/btobex/Operation.java new file mode 100755 index 00000000000..74197c023c7 --- /dev/null +++ b/btobex/javax/btobex/Operation.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * Copyright (c) 2008-2009, Motorola, Inc. + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * - Neither the name of the Motorola, Inc. nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package javax.btobex; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * The Operation interface provides ways to manipulate a single + * OBEX PUT or GET operation. The implementation of this interface sends OBEX + * packets as they are built. If during the operation the peer in the operation + * ends the operation, an IOException is thrown on the next read + * from the input stream, write to the output stream, or call to + * sendHeaders(). + *

+ * Definition of methods inherited from ContentConnection + * + *

+ * getEncoding() will always return null.
+ * getLength() will return the length specified by the OBEX Length + * header or -1 if the OBEX Length header was not included.
+ * getType() will return the value specified in the OBEX Type + * header or null if the OBEX Type header was not included.
+ *

+ * How Headers are Handled + *

+ * As headers are received, they may be retrieved through the + * getReceivedHeaders() method. If new headers are set during the + * operation, the new headers will be sent during the next packet exchange. + *

+ * PUT example + *

+ *

+ * void putObjectViaOBEX(ClientSession conn, HeaderSet head, byte[] obj) throws IOException {
+ *     // Include the length header
+ *     head.setHeader(head.LENGTH, new Long(obj.length));
+ *     // Initiate the PUT request
+ *     Operation op = conn.put(head);
+ *     // Open the output stream to put the object to it
+ *     DataOutputStream out = op.openDataOutputStream();
+ *     // Send the object to the server
+ *     out.write(obj);
+ *     // End the transaction
+ *     out.close();
+ *     op.close();
+ * }
+ * 
+ *

+ * GET example + *

+ *

+ * byte[] getObjectViaOBEX(ClientSession conn, HeaderSet head) throws IOException {
+ *     // Send the initial GET request to the server
+ *     Operation op = conn.get(head);
+ *     // Retrieve the length of the object being sent back
+ *     int length = op.getLength();
+ *     // Create space for the object
+ *     byte[] obj = new byte[length];
+ *     // Get the object from the input stream
+ *     DataInputStream in = trans.openDataInputStream();
+ *     in.read(obj);
+ *     // End the transaction
+ *     in.close();
+ *     op.close();
+ *     return obj;
+ * }
+ * 
+ * + *

Client PUT Operation Flow

For PUT operations, a call to + * close() the OutputStream returned from + * openOutputStream() or openDataOutputStream() will + * signal that the request is done. (In OBEX terms, the End-Of-Body header + * should be sent and the final bit in the request will be set.) At this point, + * the reply from the server may begin to be processed. A call to + * getResponseCode() will do an implicit close on the + * OutputStream and therefore signal that the request is done. + *

Client GET Operation Flow

For GET operation, a call to + * openInputStream() or openDataInputStream() will + * signal that the request is done. (In OBEX terms, the final bit in the request + * will be set.) A call to getResponseCode() will cause an implicit + * close on the InputStream. No further data may be read at this + * point. + * @hide + */ +public interface Operation { + + /** + * Sends an ABORT message to the server. By calling this method, the + * corresponding input and output streams will be closed along with this + * object. No headers are sent in the abort request. This will end the + * operation since close() will be called by this method. + * @throws IOException if the transaction has already ended or if an OBEX + * server calls this method + */ + void abort() throws IOException; + + /** + * Returns the headers that have been received during the operation. + * Modifying the object returned has no effect on the headers that are sent + * or retrieved. + * @return the headers received during this Operation + * @throws IOException if this Operation has been closed + */ + HeaderSet getReceivedHeader() throws IOException; + + /** + * Specifies the headers that should be sent in the next OBEX message that + * is sent. + * @param headers the headers to send in the next message + * @throws IOException if this Operation has been closed or the + * transaction has ended and no further messages will be exchanged + * @throws IllegalArgumentException if headers was not created + * by a call to ServerRequestHandler.createHeaderSet() + * or ClientSession.createHeaderSet() + * @throws NullPointerException if headers if null + */ + void sendHeaders(HeaderSet headers) throws IOException; + + /** + * Returns the response code received from the server. Response codes are + * defined in the ResponseCodes class. + * @see ResponseCodes + * @return the response code retrieved from the server + * @throws IOException if an error occurred in the transport layer during + * the transaction; if this object was created by an OBEX server + */ + int getResponseCode() throws IOException; + + String getEncoding(); + + long getLength(); + + int getHeaderLength(); + + String getType(); + + InputStream openInputStream() throws IOException; + + DataInputStream openDataInputStream() throws IOException; + + OutputStream openOutputStream() throws IOException; + + DataOutputStream openDataOutputStream() throws IOException; + + void close() throws IOException; + + void noEndofBody(); + + int getMaxPacketSize(); + + public void noBodyHeader(); +} diff --git a/btobex/javax/btobex/PasswordAuthentication.java b/btobex/javax/btobex/PasswordAuthentication.java new file mode 100755 index 00000000000..a8688c972ff --- /dev/null +++ b/btobex/javax/btobex/PasswordAuthentication.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * Copyright (c) 2008-2009, Motorola, Inc. + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * - Neither the name of the Motorola, Inc. nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package javax.btobex; + +/** + * This class holds user name and password combinations. + * @hide + */ +public final class PasswordAuthentication { + + private byte[] mUserName; + + private final byte[] mPassword; + + /** + * Creates a new PasswordAuthentication with the user name and + * password provided. + * @param userName the user name to include; this may be null + * @param password the password to include in the response + * @throws NullPointerException if password is + * null + */ + public PasswordAuthentication(final byte[] userName, final byte[] password) { + if (userName != null) { + mUserName = new byte[userName.length]; + System.arraycopy(userName, 0, mUserName, 0, userName.length); + } + + mPassword = new byte[password.length]; + System.arraycopy(password, 0, mPassword, 0, password.length); + } + + /** + * Retrieves the user name that was specified in the constructor. The user + * name may be null. + * @return the user name + */ + public byte[] getUserName() { + return mUserName; + } + + /** + * Retrieves the password. + * @return the password + */ + public byte[] getPassword() { + return mPassword; + } +} diff --git a/btobex/javax/btobex/PrivateInputStream.java b/btobex/javax/btobex/PrivateInputStream.java new file mode 100644 index 00000000000..a164c518384 --- /dev/null +++ b/btobex/javax/btobex/PrivateInputStream.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * Copyright (c) 2008-2009, Motorola, Inc. + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * - Neither the name of the Motorola, Inc. nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package javax.btobex; + +import java.io.InputStream; +import java.io.IOException; + +/** + * This object provides an input stream to the Operation objects used in this + * package. + * @hide + */ +public final class PrivateInputStream extends InputStream { + + private BaseStream mParent; + + private byte[] mData; + + private int mIndex; + + private boolean mOpen; + + /** + * Creates an input stream for the Operation to read from + * @param p the connection this input stream is for + */ + public PrivateInputStream(BaseStream p) { + mParent = p; + mData = new byte[0]; + mIndex = 0; + mOpen = true; + } + + /** + * Returns the number of bytes that can be read (or skipped over) from this + * input stream without blocking by the next caller of a method for this + * input stream. The next caller might be the same thread or or another + * thread. + * @return the number of bytes that can be read from this input stream + * without blocking + * @throws IOException if an I/O error occurs + */ + @Override + public synchronized int available() throws IOException { + ensureOpen(); + return mData.length - mIndex; + } + + /** + * Reads the next byte of data from the input stream. The value byte is + * returned as an int in the range 0 to 255. If no byte is available because + * the end of the stream has been reached, the value -1 is returned. This + * method blocks until input data is available, the end of the stream is + * detected, or an exception is thrown. + * @return the byte read from the input stream or -1 if it reaches the end of + * stream + * @throws IOException if an I/O error occurs + */ + @Override + public synchronized int read() throws IOException { + ensureOpen(); + while (mData.length == mIndex) { + if (!mParent.continueOperation(true, true)) { + return -1; + } + } + return (mData[mIndex++] & 0xFF); + } + + @Override + public int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } + + @Override + public synchronized int read(byte[] b, int offset, int length) throws IOException { + + if (b == null) { + throw new IOException("buffer is null"); + } + if ((offset | length) < 0 || length > b.length - offset) { + throw new ArrayIndexOutOfBoundsException("index outof bound"); + } + ensureOpen(); + + int currentDataLength = mData.length - mIndex; + int remainReadLength = length; + int offset1 = offset; + int result = 0; + + while (currentDataLength <= remainReadLength) { + System.arraycopy(mData, mIndex, b, offset1, currentDataLength); + mIndex += currentDataLength; + offset1 += currentDataLength; + result += currentDataLength; + remainReadLength -= currentDataLength; + + if (!mParent.continueOperation(true, true)) { + return result == 0 ? -1 : result; + } + currentDataLength = mData.length - mIndex; + } + if (remainReadLength > 0) { + System.arraycopy(mData, mIndex, b, offset1, remainReadLength); + mIndex += remainReadLength; + result += remainReadLength; + } + return result; + } + + /** + * Allows the OperationImpl thread to add body data to the + * input stream. + * @param body the data to add to the stream + * @param start the start of the body to array to copy + */ + public synchronized void writeBytes(byte[] body, int start) { + + int length = (body.length - start) + (mData.length - mIndex); + byte[] temp = new byte[length]; + + System.arraycopy(mData, mIndex, temp, 0, mData.length - mIndex); + System.arraycopy(body, start, temp, mData.length - mIndex, body.length - start); + + mData = temp; + mIndex = 0; + notifyAll(); + } + + /** + * Verifies that this stream is open + * @throws IOException if the stream is not open + */ + private void ensureOpen() throws IOException { + mParent.ensureOpen(); + if (!mOpen) { + throw new IOException("Input stream is closed"); + } + } + + /** + * Closes the input stream. If the input stream is already closed, do + * nothing. + * @throws IOException this will never happen + */ + @Override + public void close() throws IOException { + mOpen = false; + mParent.streamClosed(true); + } +} diff --git a/btobex/javax/btobex/PrivateOutputStream.java b/btobex/javax/btobex/PrivateOutputStream.java new file mode 100644 index 00000000000..c5fbf1286d1 --- /dev/null +++ b/btobex/javax/btobex/PrivateOutputStream.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * Copyright (c) 2008-2009, Motorola, Inc. + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * - Neither the name of the Motorola, Inc. nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package javax.btobex; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.ByteArrayOutputStream; + +/** + * This object provides an output stream to the Operation objects used in this + * package. + * @hide + */ +public final class PrivateOutputStream extends OutputStream { + + private BaseStream mParent; + + private ByteArrayOutputStream mArray; + + private boolean mOpen; + + private int mMaxPacketSize; + + /** + * Creates an empty PrivateOutputStream to write to. + * @param p the connection that this stream runs over + */ + public PrivateOutputStream(BaseStream p, int maxSize) { + mParent = p; + mArray = new ByteArrayOutputStream(); + mMaxPacketSize = maxSize; + mOpen = true; + } + + /** + * Determines how many bytes have been written to the output stream. + * @return the number of bytes written to the output stream + */ + public int size() { + return mArray.size(); + } + + /** + * Writes the specified byte to this output stream. The general contract for + * write is that one byte is written to the output stream. The byte to be + * written is the eight low-order bits of the argument b. The 24 high-order + * bits of b are ignored. + * @param b the byte to write + * @throws IOException if an I/O error occurs + */ + @Override + public synchronized void write(int b) throws IOException { + ensureOpen(); + mParent.ensureNotDone(); + mArray.write(b); + if (mArray.size() == mMaxPacketSize) { + mParent.continueOperation(true, false); + } + } + + @Override + public void write(byte[] buffer) throws IOException { + write(buffer, 0, buffer.length); + } + + @Override + public synchronized void write(byte[] buffer, int offset, int count) throws IOException { + int offset1 = offset; + int remainLength = count; + + if (buffer == null) { + throw new IOException("buffer is null"); + } + if ((offset | count) < 0 || count > buffer.length - offset) { + throw new IndexOutOfBoundsException("index outof bound"); + } + + ensureOpen(); + mParent.ensureNotDone(); + while ((mArray.size() + remainLength) >= mMaxPacketSize) { + int bufferLeft = mMaxPacketSize - mArray.size(); + mArray.write(buffer, offset1, bufferLeft); + offset1 += bufferLeft; + remainLength -= bufferLeft; + mParent.continueOperation(true, false); + } + if (remainLength > 0) { + mArray.write(buffer, offset1, remainLength); + } + } + + /** + * Reads the bytes that have been written to this stream. + * @param size the size of the array to return + * @return the byte array that is written + */ + public synchronized byte[] readBytes(int size) { + if (mArray.size() > 0) { + byte[] temp = mArray.toByteArray(); + mArray.reset(); + byte[] result = new byte[size]; + System.arraycopy(temp, 0, result, 0, size); + if (temp.length != size) { + mArray.write(temp, size, temp.length - size); + } + return result; + } else { + return null; + } + } + + /** + * Verifies that this stream is open + * @throws IOException if the stream is not open + */ + private void ensureOpen() throws IOException { + mParent.ensureOpen(); + if (!mOpen) { + throw new IOException("Output stream is closed"); + } + } + + /** + * Closes the output stream. If the input stream is already closed, do + * nothing. + * @throws IOException this will never happen + */ + @Override + public void close() throws IOException { + mOpen = false; + mParent.streamClosed(false); + } + + /** + * Determines if the connection is closed + * @return true if the connection is closed; false + * if the connection is open + */ + public boolean isClosed() { + return !mOpen; + } +} diff --git a/btobex/javax/btobex/ResponseCodes.java b/btobex/javax/btobex/ResponseCodes.java new file mode 100755 index 00000000000..d92a90eac91 --- /dev/null +++ b/btobex/javax/btobex/ResponseCodes.java @@ -0,0 +1,327 @@ +/* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * Copyright (c) 2008-2009, Motorola, Inc. + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * - Neither the name of the Motorola, Inc. nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package javax.btobex; + +/** + * The ResponseCodes class contains the list of valid response + * codes a server may send to a client. + *

+ * IMPORTANT NOTE + *

+ * The values in this interface represent the values defined in the IrOBEX + * specification, which is different with the HTTP specification. + *

+ * OBEX_DATABASE_FULL and OBEX_DATABASE_LOCKED require + * further description since they are not defined in HTTP. The server will send + * an OBEX_DATABASE_FULL message when the client requests that + * something be placed into a database but the database is full (cannot take + * more data). OBEX_DATABASE_LOCKED will be returned when the + * client wishes to access a database, database table, or database record that + * has been locked. + * @hide + */ +public final class ResponseCodes { + + /** + * Defines the OBEX CONTINUE response code. + *

+ * The value of OBEX_HTTP_CONTINUE is 0x90 (144). + */ + public static final int OBEX_HTTP_CONTINUE = 0x90; + + /** + * Defines the OBEX SUCCESS response code. + *

+ * The value of OBEX_HTTP_OK is 0xA0 (160). + */ + public static final int OBEX_HTTP_OK = 0xA0; + + /** + * Defines the OBEX CREATED response code. + *

+ * The value of OBEX_HTTP_CREATED is 0xA1 (161). + */ + public static final int OBEX_HTTP_CREATED = 0xA1; + + /** + * Defines the OBEX ACCEPTED response code. + *

+ * The value of OBEX_HTTP_ACCEPTED is 0xA2 (162). + */ + public static final int OBEX_HTTP_ACCEPTED = 0xA2; + + /** + * Defines the OBEX NON-AUTHORITATIVE INFORMATION response code. + *

+ * The value of OBEX_HTTP_NOT_AUTHORITATIVE is 0xA3 (163). + */ + public static final int OBEX_HTTP_NOT_AUTHORITATIVE = 0xA3; + + /** + * Defines the OBEX NO CONTENT response code. + *

+ * The value of OBEX_HTTP_NO_CONTENT is 0xA4 (164). + */ + public static final int OBEX_HTTP_NO_CONTENT = 0xA4; + + /** + * Defines the OBEX RESET CONTENT response code. + *

+ * The value of OBEX_HTTP_RESET is 0xA5 (165). + */ + public static final int OBEX_HTTP_RESET = 0xA5; + + /** + * Defines the OBEX PARTIAL CONTENT response code. + *

+ * The value of OBEX_HTTP_PARTIAL is 0xA6 (166). + */ + public static final int OBEX_HTTP_PARTIAL = 0xA6; + + /** + * Defines the OBEX MULTIPLE_CHOICES response code. + *

+ * The value of OBEX_HTTP_MULT_CHOICE is 0xB0 (176). + */ + public static final int OBEX_HTTP_MULT_CHOICE = 0xB0; + + /** + * Defines the OBEX MOVED PERMANENTLY response code. + *

+ * The value of OBEX_HTTP_MOVED_PERM is 0xB1 (177). + */ + public static final int OBEX_HTTP_MOVED_PERM = 0xB1; + + /** + * Defines the OBEX MOVED TEMPORARILY response code. + *

+ * The value of OBEX_HTTP_MOVED_TEMP is 0xB2 (178). + */ + public static final int OBEX_HTTP_MOVED_TEMP = 0xB2; + + /** + * Defines the OBEX SEE OTHER response code. + *

+ * The value of OBEX_HTTP_SEE_OTHER is 0xB3 (179). + */ + public static final int OBEX_HTTP_SEE_OTHER = 0xB3; + + /** + * Defines the OBEX NOT MODIFIED response code. + *

+ * The value of OBEX_HTTP_NOT_MODIFIED is 0xB4 (180). + */ + public static final int OBEX_HTTP_NOT_MODIFIED = 0xB4; + + /** + * Defines the OBEX USE PROXY response code. + *

+ * The value of OBEX_HTTP_USE_PROXY is 0xB5 (181). + */ + public static final int OBEX_HTTP_USE_PROXY = 0xB5; + + /** + * Defines the OBEX BAD REQUEST response code. + *

+ * The value of OBEX_HTTP_BAD_REQUEST is 0xC0 (192). + */ + public static final int OBEX_HTTP_BAD_REQUEST = 0xC0; + + /** + * Defines the OBEX UNAUTHORIZED response code. + *

+ * The value of OBEX_HTTP_UNAUTHORIZED is 0xC1 (193). + */ + public static final int OBEX_HTTP_UNAUTHORIZED = 0xC1; + + /** + * Defines the OBEX PAYMENT REQUIRED response code. + *

+ * The value of OBEX_HTTP_PAYMENT_REQUIRED is 0xC2 (194). + */ + public static final int OBEX_HTTP_PAYMENT_REQUIRED = 0xC2; + + /** + * Defines the OBEX FORBIDDEN response code. + *

+ * The value of OBEX_HTTP_FORBIDDEN is 0xC3 (195). + */ + public static final int OBEX_HTTP_FORBIDDEN = 0xC3; + + /** + * Defines the OBEX NOT FOUND response code. + *

+ * The value of OBEX_HTTP_NOT_FOUND is 0xC4 (196). + */ + public static final int OBEX_HTTP_NOT_FOUND = 0xC4; + + /** + * Defines the OBEX METHOD NOT ALLOWED response code. + *

+ * The value of OBEX_HTTP_BAD_METHOD is 0xC5 (197). + */ + public static final int OBEX_HTTP_BAD_METHOD = 0xC5; + + /** + * Defines the OBEX NOT ACCEPTABLE response code. + *

+ * The value of OBEX_HTTP_NOT_ACCEPTABLE is 0xC6 (198). + */ + public static final int OBEX_HTTP_NOT_ACCEPTABLE = 0xC6; + + /** + * Defines the OBEX PROXY AUTHENTICATION REQUIRED response code. + *

+ * The value of OBEX_HTTP_PROXY_AUTH is 0xC7 (199). + */ + public static final int OBEX_HTTP_PROXY_AUTH = 0xC7; + + /** + * Defines the OBEX REQUEST TIME OUT response code. + *

+ * The value of OBEX_HTTP_TIMEOUT is 0xC8 (200). + */ + public static final int OBEX_HTTP_TIMEOUT = 0xC8; + + /** + * Defines the OBEX METHOD CONFLICT response code. + *

+ * The value of OBEX_HTTP_CONFLICT is 0xC9 (201). + */ + public static final int OBEX_HTTP_CONFLICT = 0xC9; + + /** + * Defines the OBEX METHOD GONE response code. + *

+ * The value of OBEX_HTTP_GONE is 0xCA (202). + */ + public static final int OBEX_HTTP_GONE = 0xCA; + + /** + * Defines the OBEX METHOD LENGTH REQUIRED response code. + *

+ * The value of OBEX_HTTP_LENGTH_REQUIRED is 0xCB (203). + */ + public static final int OBEX_HTTP_LENGTH_REQUIRED = 0xCB; + + /** + * Defines the OBEX PRECONDITION FAILED response code. + *

+ * The value of OBEX_HTTP_PRECON_FAILED is 0xCC (204). + */ + public static final int OBEX_HTTP_PRECON_FAILED = 0xCC; + + /** + * Defines the OBEX REQUESTED ENTITY TOO LARGE response code. + *

+ * The value of OBEX_HTTP_ENTITY_TOO_LARGE is 0xCD (205). + */ + public static final int OBEX_HTTP_ENTITY_TOO_LARGE = 0xCD; + + /** + * Defines the OBEX REQUESTED URL TOO LARGE response code. + *

+ * The value of OBEX_HTTP_REQ_TOO_LARGE is 0xCE (206). + */ + public static final int OBEX_HTTP_REQ_TOO_LARGE = 0xCE; + + /** + * Defines the OBEX UNSUPPORTED MEDIA TYPE response code. + *

+ * The value of OBEX_HTTP_UNSUPPORTED_TYPE is 0xCF (207). + */ + public static final int OBEX_HTTP_UNSUPPORTED_TYPE = 0xCF; + + /** + * Defines the OBEX INTERNAL SERVER ERROR response code. + *

+ * The value of OBEX_HTTP_INTERNAL_ERROR is 0xD0 (208). + */ + public static final int OBEX_HTTP_INTERNAL_ERROR = 0xD0; + + /** + * Defines the OBEX NOT IMPLEMENTED response code. + *

+ * The value of OBEX_HTTP_NOT_IMPLEMENTED is 0xD1 (209). + */ + public static final int OBEX_HTTP_NOT_IMPLEMENTED = 0xD1; + + /** + * Defines the OBEX BAD GATEWAY response code. + *

+ * The value of OBEX_HTTP_BAD_GATEWAY is 0xD2 (210). + */ + public static final int OBEX_HTTP_BAD_GATEWAY = 0xD2; + + /** + * Defines the OBEX SERVICE UNAVAILABLE response code. + *

+ * The value of OBEX_HTTP_UNAVAILABLE is 0xD3 (211). + */ + public static final int OBEX_HTTP_UNAVAILABLE = 0xD3; + + /** + * Defines the OBEX GATEWAY TIMEOUT response code. + *

+ * The value of OBEX_HTTP_GATEWAY_TIMEOUT is 0xD4 (212). + */ + public static final int OBEX_HTTP_GATEWAY_TIMEOUT = 0xD4; + + /** + * Defines the OBEX HTTP VERSION NOT SUPPORTED response code. + *

+ * The value of OBEX_HTTP_VERSION is 0xD5 (213). + */ + public static final int OBEX_HTTP_VERSION = 0xD5; + + /** + * Defines the OBEX DATABASE FULL response code. + *

+ * The value of OBEX_DATABASE_FULL is 0xE0 (224). + */ + public static final int OBEX_DATABASE_FULL = 0xE0; + + /** + * Defines the OBEX DATABASE LOCKED response code. + *

+ * The value of OBEX_DATABASE_LOCKED is 0xE1 (225). + */ + public static final int OBEX_DATABASE_LOCKED = 0xE1; + + /** + * Constructor does nothing. + */ + private ResponseCodes() { + } +} diff --git a/btobex/javax/btobex/ServerOperation.java b/btobex/javax/btobex/ServerOperation.java new file mode 100755 index 00000000000..eda28b5533a --- /dev/null +++ b/btobex/javax/btobex/ServerOperation.java @@ -0,0 +1,864 @@ +/* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * Copyright (c) 2008-2009, Motorola, Inc. + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * - Neither the name of the Motorola, Inc. nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package javax.btobex; + +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.io.DataInputStream; +import java.io.OutputStream; +import java.io.DataOutputStream; +import java.io.ByteArrayOutputStream; + +/** + * This class implements the Operation interface for server side connections. + *

+ * Request Codes There are four different request codes that + * are in this class. 0x02 is a PUT request that signals that the request is not + * complete and requires an additional OBEX packet. 0x82 is a PUT request that + * says that request is complete. In this case, the server can begin sending the + * response. The 0x03 is a GET request that signals that the request is not + * finished. When the server receives a 0x83, the client is signaling the server + * that it is done with its request. TODO: Extend the ClientOperation and reuse + * the methods defined TODO: in that class. + * @hide + */ +public final class ServerOperation implements Operation, BaseStream { + private static final String TAG = "Obex ServerOperation"; + + private static final boolean VERBOSE = true; + + public boolean isAborted; + + public HeaderSet requestHeader; + + public HeaderSet replyHeader; + + public boolean finalBitSet; + + private InputStream mInput; + + private ServerSession mParent; + + private int mMaxPacketLength; + + private int mResponseSize; + + private boolean mClosed; + + private boolean mGetOperation; + + private PrivateInputStream mPrivateInput; + + private PrivateOutputStream mPrivateOutput; + + private boolean mPrivateOutputOpen; + + private String mExceptionString; + + private ServerRequestHandler mListener; + + private boolean mRequestFinished; + + private boolean mHasBody; + + private boolean mSendBodyHeader = true; + + private boolean mEndofBody = true; + + public boolean mSingleResponseActive; + + public boolean mSrmGetActive; + + public Byte mSingleResponseModeParameter; + + public ObexHelper mSrmServerSession; + + /** + * Creates new ServerOperation + * @param p the parent that created this object + * @param in the input stream to read from + * @param out the output stream to write to + * @param request the initial request that was received from the client + * @param maxSize the max packet size that the client will accept + * @param listen the listener that is responding to the request + * @throws IOException if an IO error occurs + */ + public ServerOperation(ServerSession p, InputStream in, int request, int maxSize, + ServerRequestHandler listen) throws IOException { + + if (VERBOSE) Log.v(TAG, "ServerOperation"); + + isAborted = false; + mParent = p; + mInput = in; + mMaxPacketLength = maxSize; + mClosed = false; + requestHeader = new HeaderSet(); + replyHeader = new HeaderSet(); + mPrivateInput = new PrivateInputStream(this); + mResponseSize = 3; + mListener = listen; + mRequestFinished = false; + mPrivateOutputOpen = false; + mHasBody = false; + mSingleResponseActive = false; + mSrmGetActive = false; + mSingleResponseModeParameter = ObexHelper.OBEX_SRM_PARAM_NONE; + mSrmServerSession = p.mSrmServer; + int bytesReceived; + Byte rmtRqstSRM = ObexHelper.OBEX_SRM_DISABLED; + + /* + * Determine if this is a PUT request + */ + if ((request == 0x02) || (request == 0x82)) { + /* + * It is a PUT request. + */ + mGetOperation = false; + + /* + * Determine if the final bit is set + */ + if ((request & 0x80) == 0) { + finalBitSet = false; + } else { + finalBitSet = true; + mRequestFinished = true; + } + } else if ((request == 0x03) || (request == 0x83)) { + /* + * It is a GET request. + */ + mGetOperation = true; + + // For Get request, final bit set is decided by server side logic + finalBitSet = false; + + if (request == 0x83) { + mRequestFinished = true; + } + } else { + throw new IOException("ServerOperation can not handle such request"); + } + + int length = in.read(); + length = (length << 8) + in.read(); + + /* + * Determine if the packet length is larger than this device can receive + */ + if (length > mParent.getMaxPacketSize()) { + mParent.sendResponse(ResponseCodes.OBEX_HTTP_REQ_TOO_LARGE, null); + throw new IOException("Packet received was too large"); + } + + /* + * Determine if any headers were sent in the initial request + */ + if (length > 3) { + byte[] data = new byte[length - 3]; + bytesReceived = in.read(data); + + while (bytesReceived != data.length) { + bytesReceived += in.read(data, bytesReceived, data.length - bytesReceived); + } + + byte[] body = ObexHelper.updateHeaderSet(requestHeader, data); + rmtRqstSRM = (Byte)this.requestHeader.getHeader(HeaderSet.SINGLE_RESPONSE_MODE); + if (body != null) { + mHasBody = true; + } + + if (mListener.getConnectionId() != -1 && requestHeader.mConnectionID != null) { + mListener.setConnectionId(ObexHelper.convertToLong(requestHeader.mConnectionID)); + } else { + mListener.setConnectionId(1); + } + + if (requestHeader.mAuthResp != null) { + if (!mParent.handleAuthResp(requestHeader.mAuthResp)) { + mExceptionString = "Authentication Failed"; + mParent.sendResponse(ResponseCodes.OBEX_HTTP_UNAUTHORIZED, null); + mClosed = true; + requestHeader.mAuthResp = null; + return; + } + } + + if (requestHeader.mAuthChall != null) { + mParent.handleAuthChall(requestHeader); + // send the authResp to the client + replyHeader.mAuthResp = new byte[requestHeader.mAuthResp.length]; + System.arraycopy(requestHeader.mAuthResp, 0, replyHeader.mAuthResp, 0, + replyHeader.mAuthResp.length); + requestHeader.mAuthResp = null; + requestHeader.mAuthChall = null; + + } + + if (body != null) { + mPrivateInput.writeBytes(body, 1); + /* Don't Pre-Send continue when Remote requested for SRM + * Let the Application confirm + */ + } else if (rmtRqstSRM != ObexHelper.OBEX_SRM_ENABLED) { + while ((!mGetOperation) && (!finalBitSet)) { + if (VERBOSE) Log.v(TAG, "length > 3 ServerOperation pre-sendreply"); + sendReply(ResponseCodes.OBEX_HTTP_CONTINUE, mSingleResponseActive, false); + if (mPrivateInput.available() > 0) { + break; + } + } + } + } + /* Don't Pre-Send continue when Remote requested for SRM + * Let the Application confirm + */ + while ((rmtRqstSRM != ObexHelper.OBEX_SRM_ENABLED) && (!mGetOperation) && (!finalBitSet) && + (mPrivateInput.available() == 0)) { + if (VERBOSE) Log.v(TAG, "mPrivateInput.available 0 ServerOperation pre-sendreply"); + sendReply(ResponseCodes.OBEX_HTTP_CONTINUE, mSingleResponseActive, false); + + if (mPrivateInput.available() > 0) { + break; + } + } + + // wait for get request finished !!!! + while (mGetOperation && !mRequestFinished) { + sendReply(ResponseCodes.OBEX_HTTP_CONTINUE, false, false); + } + } + + public boolean isValidBody() { + return mHasBody; + } + + /** + * Determines if the operation should continue or should wait. If it should + * continue, this method will continue the operation. + * @param sendEmpty if true then this will continue the + * operation even if no headers will be sent; if false + * then this method will only continue the operation if there are + * headers to send + * @param inStream iftrue the stream is input stream, otherwise + * output stream + * @return true if the operation was completed; + * false if no operation took place + */ + public synchronized boolean continueOperation(boolean sendEmpty, boolean inStream) + throws IOException { + if (VERBOSE) Log.v(TAG, "continueOperation"); + if (!mGetOperation) { + if (!finalBitSet) { + if (sendEmpty) { + sendReply(ResponseCodes.OBEX_HTTP_CONTINUE, mSingleResponseActive, false); + + if (VERBOSE) Log.v(TAG, "continueOperation:ServerSet SRM sendEmpty clause"); + + mSingleResponseActive = mSrmServerSession.getLocalSrmStatus(); + if (mSingleResponseActive == ObexHelper.LOCAL_SRM_ENABLED) { + if (VERBOSE) Log.v(TAG, "continueOperation: Server SRM enabled"); + if (mSrmServerSession.getLocalSrmpWait()) { + if (mSingleResponseModeParameter == ObexHelper.OBEX_SRM_PARAM_WAIT) { + if (VERBOSE) Log.v(TAG, + "continueOperation: Server sent SRMP NONE to stop SRMP WAIT"); + mSingleResponseActive = ObexHelper.LOCAL_SRM_ENABLED; + mSrmServerSession.setLocalSrmpWait(false); + } else { + if (VERBOSE) Log.v(TAG, "continueOperation: Server SRMP WAIT"); + mSingleResponseActive = ObexHelper.LOCAL_SRM_DISABLED; + mSingleResponseModeParameter = ObexHelper.OBEX_SRM_PARAM_WAIT; + } + } + } else { + if (VERBOSE) Log.v(TAG, "continueOperation: Server SRM disabled"); + } + return true; + } else { + if ((mResponseSize > 3) || (mPrivateOutput.size() > 0)) { + sendReply(ResponseCodes.OBEX_HTTP_CONTINUE, mSingleResponseActive, false); + + if (VERBOSE) Log.v(TAG, "continueOperation: Server setting SRM"); + + mSingleResponseActive = mSrmServerSession.getLocalSrmStatus(); + if (mSingleResponseActive == ObexHelper.LOCAL_SRM_ENABLED) { + if (VERBOSE) Log.v(TAG, "continueOperation: Server SRM enabled"); + if (mSrmServerSession.getLocalSrmpWait()) { + if (mSingleResponseModeParameter == ObexHelper.OBEX_SRM_PARAM_WAIT) { + if (VERBOSE) Log.v(TAG, + "continueOperation: ServerSent SRMP NONE to stop SRMP WAIT"); + mSingleResponseActive = ObexHelper.LOCAL_SRM_ENABLED; + mSrmServerSession.setLocalSrmpWait(false); + } else { + if (VERBOSE) Log.v(TAG, "continueOperation: Server SRMP WAIT"); + mSingleResponseActive = ObexHelper.LOCAL_SRM_DISABLED; + mSingleResponseModeParameter = ObexHelper.OBEX_SRM_PARAM_WAIT; + } + } + } else { + if (VERBOSE) Log.v(TAG, "continueOperation: Server SRM disabled"); + } + return true; + } else { + return false; + } + } + } else { + return false; + } + } else { + mSrmGetActive = mSrmServerSession.getLocalSrmStatus(); + requestHeader.setHeader(HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER, + ObexHelper.OBEX_SRM_PARAM_NONE); + sendReply(ResponseCodes.OBEX_HTTP_CONTINUE, mSingleResponseActive, mSrmGetActive); + + if (VERBOSE) Log.v(TAG, "Get continueOperation"); + return true; + } + } + + /** + * Sends a reply to the client. If the reply is a OBEX_HTTP_CONTINUE, it + * will wait for a response from the client before ending. + * @param type the response code to send back to the client + * @param suppressSend suppress sending the reply, e.g., for SRM mode + * @return true if the final bit was not set on the reply; + * false if no reply was received because the operation + * ended, an abort was received, or the final bit was set in the + * reply + * @throws IOException if an IO error occurs + */ + public synchronized boolean sendReply(int type, + boolean suppressSend,boolean supressReceive) throws IOException { + + if (VERBOSE) Log.v(TAG, "sendReply type: " + type + ", suppress: " + + suppressSend + ", SRMP WAIT: "+ mSrmServerSession.getLocalSrmpWait() + + "supressReceive: "+supressReceive); + + int bytesReceived; + + if ((!suppressSend) || (mSrmServerSession.getLocalSrmpWait())) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + long id = mListener.getConnectionId(); + if (id == -1) { + replyHeader.mConnectionID = null; + } else { + replyHeader.mConnectionID = ObexHelper.convertToByteArray(id); + } + + byte[] headerArray = ObexHelper.createHeader(replyHeader, true); + int bodyLength = -1; + int orginalBodyLength = -1; + + if (mPrivateOutput != null) { + bodyLength = mPrivateOutput.size(); + orginalBodyLength = bodyLength; + } + + if ((ObexHelper.BASE_PACKET_LENGTH + headerArray.length) > mMaxPacketLength) { + + int end = 0; + int start = 0; + + while (end != headerArray.length) { + end = ObexHelper.findHeaderEnd(headerArray, start, mMaxPacketLength + - ObexHelper.BASE_PACKET_LENGTH); + if (end == -1) { + + mClosed = true; + + if (mPrivateInput != null) { + mPrivateInput.close(); + } + + if (mPrivateOutput != null) { + mPrivateOutput.close(); + } + mParent.sendResponse(ResponseCodes.OBEX_HTTP_INTERNAL_ERROR, null); + throw new IOException("OBEX Packet exceeds max packet size"); + } + byte[] sendHeader = new byte[end - start]; + System.arraycopy(headerArray, start, sendHeader, 0, sendHeader.length); + + mParent.sendResponse(type, sendHeader); + start = end; + } + + if (bodyLength > 0) { + return true; + } else { + return false; + } + + } else { + out.write(headerArray); + } + + // For Get operation: if response code is OBEX_HTTP_OK, then this is the + // last packet; so set finalBitSet to true. + if (mGetOperation && type == ResponseCodes.OBEX_HTTP_OK) { + finalBitSet = true; + } + + if ((finalBitSet) || (headerArray.length < (mMaxPacketLength - 20))) { + if (bodyLength > 0) { + /* + * Determine if I can send the whole body or just part of + * the body. Remember that there is the 3 bytes for the + * response message and 3 bytes for the header ID and length + */ + if (bodyLength > (mMaxPacketLength - headerArray.length - 6)) { + bodyLength = mMaxPacketLength - headerArray.length - 6; + } + + byte[] body = mPrivateOutput.readBytes(bodyLength); + if(VERBOSE) Log.e(TAG,"Get Operation bodyLength = "+bodyLength); + + /* + * Since this is a put request if the final bit is set or + * the output stream is closed we need to send the 0x49 + * (End of Body) otherwise, we need to send 0x48 (Body) + */ + if ((finalBitSet) || (mPrivateOutput.isClosed())) { + if (mEndofBody) { + out.write(0x49); + if(VERBOSE) Log.e(TAG," write 0x49 + "+bodyLength); + bodyLength += 3; + out.write((byte)(bodyLength >> 8)); + out.write((byte)bodyLength); + out.write(body); + } + } else { + if(mSendBodyHeader == true) { + out.write(0x48); + if(VERBOSE) Log.e(TAG," write 0x48 = "+bodyLength); + bodyLength += 3; + out.write((byte)(bodyLength >> 8)); + out.write((byte)bodyLength); + out.write(body); + } + } + + } + } + + if ((finalBitSet) && (type == ResponseCodes.OBEX_HTTP_OK) && (orginalBodyLength <= 0)) { + if(mSendBodyHeader == true) { + out.write(0x49); + orginalBodyLength = 3; + out.write((byte)(orginalBodyLength >> 8)); + out.write((byte)orginalBodyLength); + } else if (mEndofBody) { + + out.write(0x49); + if(VERBOSE) Log.e(TAG,"type == ResponseCodes.OBEX_HTTP_OK write 0x49"); + orginalBodyLength = 3; + out.write((byte)(orginalBodyLength >> 8)); + out.write((byte)orginalBodyLength); + } + } + + mResponseSize = 3; + mParent.sendResponse(type, out.toByteArray()); + } + + if (type == ResponseCodes.OBEX_HTTP_CONTINUE) { + if((!supressReceive) || (mSrmServerSession.getLocalSrmpWait())) { + int headerID = mInput.read(); + int length = mInput.read(); + length = (length << 8) + mInput.read(); + if (VERBOSE) Log.v(TAG, "sendReply reading headerID " + headerID + " length " + length); + if ((headerID != ObexHelper.OBEX_OPCODE_PUT) + && (headerID != ObexHelper.OBEX_OPCODE_PUT_FINAL) + && (headerID != ObexHelper.OBEX_OPCODE_GET) + && (headerID != ObexHelper.OBEX_OPCODE_GET_FINAL)) { + + if (length > 3) { + byte[] temp = new byte[length - 3]; + // First three bytes already read, compensating for this + bytesReceived = mInput.read(temp); + + while (bytesReceived != temp.length) { + bytesReceived += mInput.read(temp, bytesReceived, + temp.length - bytesReceived); + } + } + + /* + * Determine if an ABORT was sent as the reply + */ + if (headerID == ObexHelper.OBEX_OPCODE_ABORT) { + mParent.sendResponse(ResponseCodes.OBEX_HTTP_OK, null); + mClosed = true; + isAborted = true; + mExceptionString = "Abort Received"; + throw new IOException("Abort Received"); + } else { + mParent.sendResponse(ResponseCodes.OBEX_HTTP_BAD_REQUEST, null); + mClosed = true; + mExceptionString = "Bad Request Received"; + throw new IOException("Bad Request Received"); + } + } else { + + if ((headerID == ObexHelper.OBEX_OPCODE_PUT_FINAL)) { + finalBitSet = true; + } else if (headerID == ObexHelper.OBEX_OPCODE_GET_FINAL) { + mRequestFinished = true; + } + + /* + * Determine if the packet length is larger then this device can receive + */ + if (length > mParent.getMaxPacketSize()) { + mParent.sendResponse(ResponseCodes.OBEX_HTTP_REQ_TOO_LARGE, null); + throw new IOException("Packet received was too large"); + } + + /* + * Determine if any headers were sent in the initial request + */ + if (length > 3) { + if (VERBOSE) Log.v(TAG, "sendReply reading " + (length-3)); + byte[] data = new byte[length - 3]; + bytesReceived = mInput.read(data); + + while (bytesReceived != data.length) { + bytesReceived += mInput.read(data, bytesReceived, data.length + - bytesReceived); + } + if (VERBOSE) Log.v(TAG, "sendReply read " + bytesReceived); + byte[] body = ObexHelper.updateHeaderSet(requestHeader, data); + if (body != null) { + mHasBody = true; + } + if (mListener.getConnectionId() != -1 && requestHeader.mConnectionID != null) { + mListener.setConnectionId(ObexHelper + .convertToLong(requestHeader.mConnectionID)); + } else { + mListener.setConnectionId(1); + } + + if (requestHeader.mAuthResp != null) { + if (!mParent.handleAuthResp(requestHeader.mAuthResp)) { + mExceptionString = "Authentication Failed"; + mParent.sendResponse(ResponseCodes.OBEX_HTTP_UNAUTHORIZED, null); + mClosed = true; + requestHeader.mAuthResp = null; + return false; + } + requestHeader.mAuthResp = null; + } + + if (requestHeader.mAuthChall != null) { + mParent.handleAuthChall(requestHeader); + // send the auhtResp to the client + replyHeader.mAuthResp = new byte[requestHeader.mAuthResp.length]; + System.arraycopy(requestHeader.mAuthResp, 0, replyHeader.mAuthResp, 0, + replyHeader.mAuthResp.length); + requestHeader.mAuthResp = null; + requestHeader.mAuthChall = null; + } + + if (body != null) { + mPrivateInput.writeBytes(body, 1); + } + } + } + return true; + } else { + return false; + } + } else { + return false; + } + } + + /** + * Sends an ABORT message to the server. By calling this method, the + * corresponding input and output streams will be closed along with this + * object. + * @throws IOException if the transaction has already ended or if an OBEX + * server called this method + */ + public void abort() throws IOException { + throw new IOException("Called from a server"); + } + + /** + * Returns the headers that have been received during the operation. + * Modifying the object returned has no effect on the headers that are sent + * or retrieved. + * @return the headers received during this Operation + * @throws IOException if this Operation has been closed + */ + public HeaderSet getReceivedHeader() throws IOException { + ensureOpen(); + return requestHeader; + } + + /** + * Specifies the headers that should be sent in the next OBEX message that + * is sent. + * @param headers the headers to send in the next message + * @throws IOException if this Operation has been closed or the + * transaction has ended and no further messages will be exchanged + * @throws IllegalArgumentException if headers was not created + * by a call to ServerRequestHandler.createHeaderSet() + */ + public void sendHeaders(HeaderSet headers) throws IOException { + ensureOpen(); + + if (headers == null) { + throw new IOException("Headers may not be null"); + } + + int[] headerList = headers.getHeaderList(); + if (headerList != null) { + for (int i = 0; i < headerList.length; i++) { + replyHeader.setHeader(headerList[i], headers.getHeader(headerList[i])); + } + + } + } + + /** + * Retrieves the response code retrieved from the server. Response codes are + * defined in the ResponseCodes interface. + * @return the response code retrieved from the server + * @throws IOException if an error occurred in the transport layer during + * the transaction; if this method is called on a + * HeaderSet object created by calling + * createHeaderSet in a ClientSession + * object; if this is called from a server + */ + public int getResponseCode() throws IOException { + throw new IOException("Called from a server"); + } + + /** + * Always returns null + * @return null + */ + public String getEncoding() { + return null; + } + + /** + * Returns the type of content that the resource connected to is providing. + * E.g. if the connection is via HTTP, then the value of the content-type + * header field is returned. + * @return the content type of the resource that the URL references, or + * null if not known + */ + public String getType() { + try { + return (String)requestHeader.getHeader(HeaderSet.TYPE); + } catch (IOException e) { + return null; + } + } + + /** + * Returns the length of the content which is being provided. E.g. if the + * connection is via HTTP, then the value of the content-length header field + * is returned. + * @return the content length of the resource that this connection's URL + * references, or -1 if the content length is not known + */ + public long getLength() { + try { + Long temp = (Long)requestHeader.getHeader(HeaderSet.LENGTH); + + if (temp == null) { + return -1; + } else { + return temp.longValue(); + } + } catch (IOException e) { + return -1; + } + } + + public int getMaxPacketSize() { + return mMaxPacketLength - 6 - getHeaderLength(); + } + + public int getHeaderLength() { + long id = mListener.getConnectionId(); + if (id == -1) { + replyHeader.mConnectionID = null; + } else { + replyHeader.mConnectionID = ObexHelper.convertToByteArray(id); + } + + byte[] headerArray = ObexHelper.createHeader(replyHeader, false); + + return headerArray.length; + } + + /** + * Open and return an input stream for a connection. + * @return an input stream + * @throws IOException if an I/O error occurs + */ + public InputStream openInputStream() throws IOException { + ensureOpen(); + return mPrivateInput; + } + + /** + * Open and return a data input stream for a connection. + * @return an input stream + * @throws IOException if an I/O error occurs + */ + public DataInputStream openDataInputStream() throws IOException { + return new DataInputStream(openInputStream()); + } + + /** + * Open and return an output stream for a connection. + * @return an output stream + * @throws IOException if an I/O error occurs + */ + public OutputStream openOutputStream() throws IOException { + ensureOpen(); + + if (mPrivateOutputOpen) { + throw new IOException("no more input streams available, stream already opened"); + } + + if (!mRequestFinished) { + throw new IOException("no output streams available ,request not finished"); + } + + if (mPrivateOutput == null) { + mPrivateOutput = new PrivateOutputStream(this, getMaxPacketSize()); + } + mPrivateOutputOpen = true; + return mPrivateOutput; + } + + /** + * Open and return a data output stream for a connection. + * @return an output stream + * @throws IOException if an I/O error occurs + */ + public DataOutputStream openDataOutputStream() throws IOException { + return new DataOutputStream(openOutputStream()); + } + + /** + * Closes the connection and ends the transaction + * @throws IOException if the operation has already ended or is closed + */ + public void close() throws IOException { + ensureOpen(); + mClosed = true; + } + + /** + * Verifies that the connection is open and no exceptions should be thrown. + * @throws IOException if an exception needs to be thrown + */ + public void ensureOpen() throws IOException { + if (mExceptionString != null) { + throw new IOException(mExceptionString); + } + if (mClosed) { + throw new IOException("Operation has already ended"); + } + } + + /** + * Verifies that additional information may be sent. In other words, the + * operation is not done. + *

+ * Included to implement the BaseStream interface only. It does not do + * anything on the server side since the operation of the Operation object + * is not done until after the handler returns from its method. + * @throws IOException if the operation is completed + */ + public void ensureNotDone() throws IOException { + } + + /** + * Called when the output or input stream is closed. It does not do anything + * on the server side since the operation of the Operation object is not + * done until after the handler returns from its method. + * @param inStream true if the input stream is closed; + * false if the output stream is closed + * @throws IOException if an IO error occurs + */ + public void streamClosed(boolean inStream) throws IOException { + + } + + public void noBodyHeader(){ + mSendBodyHeader = false; + } + public void noEndofBody() { + mEndofBody = false; + } + + public boolean isAborted() { + try { + if (mInput.available() != 0) { + int headerID = mInput.read(); + int length = mInput.read(); + length = (length << 8) + mInput.read(); + if (length > 3) { + byte[] temp = new byte[length - 3]; + // First three bytes already read, compensating for this + int bytesReceived = mInput.read(temp); + while (bytesReceived != temp.length) { + bytesReceived += mInput.read(temp, bytesReceived, + temp.length - bytesReceived); + } + if (headerID == ObexHelper.OBEX_OPCODE_ABORT) { + return true; + } + } + } + } catch (IOException e) { + Log.e(TAG,"IOException = "+e.toString()); + } + return false; + } +} diff --git a/btobex/javax/btobex/ServerRequestHandler.java b/btobex/javax/btobex/ServerRequestHandler.java new file mode 100644 index 00000000000..d14302437cb --- /dev/null +++ b/btobex/javax/btobex/ServerRequestHandler.java @@ -0,0 +1,349 @@ +/* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * Copyright (c) 2008-2009, Motorola, Inc. + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * - Neither the name of the Motorola, Inc. nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package javax.btobex; + +/** + * The ServerRequestHandler class defines an event listener that + * will respond to OBEX requests made to the server. + *

+ * The onConnect(), onSetPath(), + * onDelete(), onGet(), and onPut() + * methods may return any response code defined in the + * ResponseCodes class except for OBEX_HTTP_CONTINUE. + * If OBEX_HTTP_CONTINUE or a value not defined in the + * ResponseCodes class is returned, the server implementation will + * send an OBEX_HTTP_INTERNAL_ERROR response to the client. + *

+ * Connection ID and Target Headers + *

+ * According to the IrOBEX specification, a packet may not contain a Connection + * ID and Target header. Since the Connection ID header is managed by the + * implementation, it will not send a Connection ID header, if a Connection ID + * was specified, in a packet that has a Target header. In other words, if an + * application adds a Target header to a HeaderSet object used in + * an OBEX operation and a Connection ID was specified, no Connection ID will be + * sent in the packet containing the Target header. + *

+ * CREATE-EMPTY Requests + *

+ * A CREATE-EMPTY request allows clients to create empty objects on the server. + * When a CREATE-EMPTY request is received, the onPut() method will + * be called by the implementation. To differentiate between a normal PUT + * request and a CREATE-EMPTY request, an application must open the + * InputStream from the Operation object passed to the + * onPut() method. For a PUT request, the application will be able + * to read Body data from this InputStream. For a CREATE-EMPTY + * request, there will be no Body data to read. Therefore, a call to + * InputStream.read() will return -1. + * @hide + */ +public class ServerRequestHandler { + + private long mConnectionId; + + /** + * Creates a ServerRequestHandler. + */ + protected ServerRequestHandler() { + /* + * A connection ID of -1 implies there is no conenction ID + */ + mConnectionId = -1; + } + + /** + * Sets the connection ID header to include in the reply packets. + * @param connectionId the connection ID to use; -1 if no connection ID + * should be sent + * @throws IllegalArgumentException if id is not in the range + * -1 to 232-1 + */ + public void setConnectionId(final long connectionId) { + if ((connectionId < -1) || (connectionId > 0xFFFFFFFFL)) { + throw new IllegalArgumentException("Illegal Connection ID"); + } + mConnectionId = connectionId; + } + + /** + * Retrieves the connection ID that is being used in the present connection. + * This method will return -1 if no connection ID is being used. + * @return the connection id being used or -1 if no connection ID is being + * used + */ + public long getConnectionId() { + return mConnectionId; + } + + /** + * Called when a CONNECT request is received. + *

+ * If this method is not implemented by the class that extends this class, + * onConnect() will always return an OBEX_HTTP_OK + * response code. + *

+ * The headers received in the request can be retrieved from the + * request argument. The headers that should be sent in the + * reply must be specified in the reply argument. + * @param request contains the headers sent by the client; + * request will never be null + * @param reply the headers that should be sent in the reply; + * reply will never be null + * @return a response code defined in ResponseCodes that will + * be returned to the client; if an invalid response code is + * provided, the OBEX_HTTP_INTERNAL_ERROR response code + * will be used + */ + public int onConnect(HeaderSet request, HeaderSet reply) { + return ResponseCodes.OBEX_HTTP_OK; + } + + /** + * Called when a DISCONNECT request is received. + *

+ * The headers received in the request can be retrieved from the + * request argument. The headers that should be sent in the + * reply must be specified in the reply argument. + * @param request contains the headers sent by the client; + * request will never be null + * @param reply the headers that should be sent in the reply; + * reply will never be null + */ + public void onDisconnect(HeaderSet request, HeaderSet reply) { + } + + /** + * Called when a SETPATH request is received. + *

+ * If this method is not implemented by the class that extends this class, + * onSetPath() will always return an + * OBEX_HTTP_NOT_IMPLEMENTED response code. + *

+ * The headers received in the request can be retrieved from the + * request argument. The headers that should be sent in the + * reply must be specified in the reply argument. + * @param request contains the headers sent by the client; + * request will never be null + * @param reply the headers that should be sent in the reply; + * reply will never be null + * @param backup true if the client requests that the server + * back up one directory before changing to the path described by + * name; false to apply the request to the + * present path + * @param create true if the path should be created if it does + * not already exist; false if the path should not be + * created if it does not exist and an error code should be returned + * @return a response code defined in ResponseCodes that will + * be returned to the client; if an invalid response code is + * provided, the OBEX_HTTP_INTERNAL_ERROR response code + * will be used + */ + public int onSetPath(HeaderSet request, HeaderSet reply, boolean backup, boolean create) { + + return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED; + } + + /** + * Called when a DELETE request is received. + *

+ * If this method is not implemented by the class that extends this class, + * onDelete() will always return an + * OBEX_HTTP_NOT_IMPLEMENTED response code. + *

+ * The headers received in the request can be retrieved from the + * request argument. The headers that should be sent in the + * reply must be specified in the reply argument. + * @param request contains the headers sent by the client; + * request will never be null + * @param reply the headers that should be sent in the reply; + * reply will never be null + * @return a response code defined in ResponseCodes that will + * be returned to the client; if an invalid response code is + * provided, the OBEX_HTTP_INTERNAL_ERROR response code + * will be used + */ + public int onDelete(HeaderSet request, HeaderSet reply) { + return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED; + } + + /** + * Called when a COPY request is received. + *

+ * If this method is not implemented by the class that extends this class, + * onCopy() will always return an + * OBEX_HTTP_NOT_IMPLEMENTED response code. + *

+ * The headers received in the request can be retrieved from the + * request argument. The headers that should be sent in the + * reply must be specified in the reply argument. + * @param request contains the headers sent by the client; + * request will never be null + * @param reply the headers that should be sent in the reply; + * reply will never be null + * @return a response code defined in ResponseCodes that will + * be returned to the client; if an invalid response code is + * provided, the OBEX_HTTP_INTERNAL_ERROR response code + * will be used + */ + public int onCopy(HeaderSet request, HeaderSet reply) { + return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED; + } + + /** + * Called when a RENAME request is received. + *

+ * If this method is not implemented by the class that extends this class, + * onRename() will always return an + * OBEX_HTTP_NOT_IMPLEMENTED response code. + *

+ * The headers received in the request can be retrieved from the + * request argument. The headers that should be sent in the + * reply must be specified in the reply argument. + * @param request contains the headers sent by the client; + * request will never be null + * @param reply the headers that should be sent in the reply; + * reply will never be null + * @return a response code defined in ResponseCodes that will + * be returned to the client; if an invalid response code is + * provided, the OBEX_HTTP_INTERNAL_ERROR response code + * will be used + */ + public int onRename(HeaderSet request, HeaderSet reply) { + return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED; + } + + /** + * Called when a SET Permission request is received. + *

+ * If this method is not implemented by the class that extends this class, + * onSetPermissions() will always return an + * OBEX_HTTP_NOT_IMPLEMENTED response code. + *

+ * The headers received in the request can be retrieved from the + * request argument. The headers that should be sent in the + * reply must be specified in the reply argument. + * @param request contains the headers sent by the client; + * request will never be null + * @param reply the headers that should be sent in the reply; + * reply will never be null + * @return a response code defined in ResponseCodes that will + * be returned to the client; if an invalid response code is + * provided, the OBEX_HTTP_INTERNAL_ERROR response code + * will be used + */ + public int onSetPermissions(HeaderSet request, HeaderSet reply) { + return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED; + } + + + /** + * Called when a ABORT request is received. + */ + public int onAbort(HeaderSet request, HeaderSet reply) { + return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED; + } + + /** + * Called when a PUT request is received. + *

+ * If this method is not implemented by the class that extends this class, + * onPut() will always return an + * OBEX_HTTP_NOT_IMPLEMENTED response code. + *

+ * If an ABORT request is received during the processing of a PUT request, + * op will be closed by the implementation. + * @param operation contains the headers sent by the client and allows new + * headers to be sent in the reply; op will never be + * null + * @return a response code defined in ResponseCodes that will + * be returned to the client; if an invalid response code is + * provided, the OBEX_HTTP_INTERNAL_ERROR response code + * will be used + */ + public int onPut(Operation operation) { + return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED; + } + + /** + * Called when a GET request is received. + *

+ * If this method is not implemented by the class that extends this class, + * onGet() will always return an + * OBEX_HTTP_NOT_IMPLEMENTED response code. + *

+ * If an ABORT request is received during the processing of a GET request, + * op will be closed by the implementation. + * @param operation contains the headers sent by the client and allows new + * headers to be sent in the reply; op will never be + * null + * @return a response code defined in ResponseCodes that will + * be returned to the client; if an invalid response code is + * provided, the OBEX_HTTP_INTERNAL_ERROR response code + * will be used + */ + public int onGet(Operation operation) { + return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED; + } + + /** + * Called when this object attempts to authenticate a client and the + * authentication request fails because the response digest in the + * authentication response header was wrong. + *

+ * If this method is not implemented by the class that extends this class, + * this method will do nothing. + * @param userName the user name returned in the authentication response; + * null if no user name was provided in the response + */ + public void onAuthenticationFailure(byte[] userName) { + } + + /** + * Called by ServerSession to update the status of current transaction + *

+ * If this method is not implemented by the class that extends this class, + * this method will do nothing. + */ + public void updateStatus(String message) { + } + + /** + * Called when session is closed. + *

+ * If this method is not implemented by the class that extends this class, + * this method will do nothing. + */ + public void onClose() { + } +} diff --git a/btobex/javax/btobex/ServerSession.java b/btobex/javax/btobex/ServerSession.java new file mode 100755 index 00000000000..4f7c29a373e --- /dev/null +++ b/btobex/javax/btobex/ServerSession.java @@ -0,0 +1,849 @@ +/* + * Copyright (c) 2014 The Linux Foundation. All rights reserved. + * Copyright (c) 2008-2009, Motorola, Inc. + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * - Neither the name of the Motorola, Inc. nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package javax.btobex; + +import android.util.Log; + +import java.io.InputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * This class in an implementation of the OBEX ServerSession. + * @hide + */ +public final class ServerSession extends ObexSession implements Runnable { + + private static final String TAG = "Obex ServerSession"; + + private static final boolean VERBOSE = ObexHelper.VERBOSE; + + private ObexTransport mTransport; + + private InputStream mInput; + + private OutputStream mOutput; + + private ServerRequestHandler mListener; + + private Thread mProcessThread; + + private int mMaxPacketLength; + + private boolean mClosed; + + public ObexHelper mSrmServer; + /** + * Creates new ServerSession. + * @param trans the connection to the client + * @param handler the event listener that will process requests + * @param auth the authenticator to use with this connection + * @throws IOException if an error occurred while opening the input and + * output streams + */ + public ServerSession(ObexTransport trans, ServerRequestHandler handler, Authenticator auth) + throws IOException { + mAuthenticator = auth; + mTransport = trans; + mInput = mTransport.openInputStream(); + mOutput = mTransport.openOutputStream(); + mListener = handler; + mMaxPacketLength = 256; + mSrmServer = new ObexHelper(); + mClosed = false; + mProcessThread = new Thread(this); + mProcessThread.start(); + } + + public void setMaxPacketSize(int size) { + if (VERBOSE) Log.v(TAG, "setMaxPacketSize" + size); + mMaxPacketLength = size; + } + + public int getMaxPacketSize() { + return mMaxPacketLength; + } + + /** + * Processes requests made to the server and forwards them to the + * appropriate event listener. + */ + public void run() { + try { + + boolean done = false; + while (!done && !mClosed) { + int requestType = mInput.read(); + if (VERBOSE) Log.v(TAG, "run requestType "+requestType); + switch (requestType) { + case ObexHelper.OBEX_OPCODE_CONNECT: + handleConnectRequest(); + break; + + case ObexHelper.OBEX_OPCODE_DISCONNECT: + handleDisconnectRequest(); + done = true; + break; + + case ObexHelper.OBEX_OPCODE_GET: + case ObexHelper.OBEX_OPCODE_GET_FINAL: + handleGetRequest(requestType); + break; + + case ObexHelper.OBEX_OPCODE_PUT: + case ObexHelper.OBEX_OPCODE_PUT_FINAL: + handlePutRequest(requestType); + break; + + case ObexHelper.OBEX_OPCODE_SETPATH: + handleSetPathRequest(); + break; + case ObexHelper.OBEX_OPCODE_ABORT: + handleAbortRequest(); + break; + case ObexHelper.OBEX_OPCODE_ACTION: + handleActionRequest(); + break; + case -1: + done = true; + break; + + default: + + /* + * Received a request type that is not recognized so I am + * just going to read the packet and send a not implemented + * to the client + */ + int length = mInput.read(); + length = (length << 8) + mInput.read(); + for (int i = 3; i < length; i++) { + mInput.read(); + } + sendResponse(ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED, null); + } + } + + } catch (NullPointerException e) { + Log.d(TAG, e.toString()); + } catch (Exception e) { + Log.d(TAG, e.toString()); + } + close(); + } + + /** + * Handles a ABORT request from a client. This method will read the rest of + * the request from the client. Assuming the request is valid, it will + * create a HeaderSet object to pass to the + * ServerRequestHandler object. After the handler processes the + * request, this method will create a reply message to send to the server. + * + * @throws IOException if an error occurred at the transport layer + */ + private void handleAbortRequest() throws IOException { + int code = ResponseCodes.OBEX_HTTP_OK; + HeaderSet request = new HeaderSet(); + HeaderSet reply = new HeaderSet(); + + int length = mInput.read(); + length = (length << 8) + mInput.read(); + if (length > mMaxPacketLength) { + code = ResponseCodes.OBEX_HTTP_REQ_TOO_LARGE; + } else { + for (int i = 3; i < length; i++) { + mInput.read(); + } + code = mListener.onAbort(request, reply); + Log.v(TAG, "onAbort request handler return value- " + code); + code = validateResponseCode(code); + } + sendResponse(code, null); + } + + /** + * Handles a ACTION request from a client. This method will read the rest of + * the request from the client. Assuming the request is valid, it will + * create a HeaderSet object to pass to the + * ServerRequestHandler object. After the handler processes the + * request, this method will create a reply message to send to the server. + * + * @throws IOException if an error occurred at the transport layer + */ + private void handleActionRequest() throws IOException { + int code = ResponseCodes.OBEX_HTTP_OK; + HeaderSet request = new HeaderSet(); + HeaderSet reply = new HeaderSet(); + int length = mInput.read(); + int bytesReceived; + + length = (length << 8) + mInput.read(); + + if (length > mMaxPacketLength) { + code = ResponseCodes.OBEX_HTTP_REQ_TOO_LARGE; + } else { + byte[] headers = new byte[length - 3]; + bytesReceived = mInput.read(headers); + + while (bytesReceived != headers.length) { + bytesReceived += mInput.read(headers, bytesReceived, headers.length + - bytesReceived); + } + if (VERBOSE) Log.v(TAG,"onAction headers.length = " + headers.length); + ObexHelper.updateHeaderSet(request, headers); + + Byte actionId = (Byte)request.getHeader(HeaderSet.ACTION_ID); + if (actionId == ObexHelper.OBEX_ACTION_COPY) { + code = mListener.onCopy(request, reply); + } else if (actionId == ObexHelper.OBEX_ACTION_MOVE_RENAME) { + code = mListener.onRename(request, reply); + } else if (actionId == ObexHelper.OBEX_ACTION_SET_PERM) { + code = mListener.onSetPermissions(request, reply); + } + if (VERBOSE) Log.v(TAG, "onAction request handler return value- " + code); + code = validateResponseCode(code); + } + sendResponse(code, null); + } + + /** + * Handles a PUT request from a client. This method will provide a + * ServerOperation object to the request handler. The + * ServerOperation object will handle the rest of the request. + * It will also send replies and receive requests until the final reply + * should be sent. When the final reply should be sent, this method will get + * the response code to use and send the reply. The + * ServerOperation object will always reply with a + * OBEX_HTTP_CONTINUE reply. It will only reply if further information is + * needed. + * @param type the type of request received; either 0x02 or 0x82 + * @throws IOException if an error occurred at the transport layer + */ + private void handlePutRequest(int type) throws IOException { + if (VERBOSE) Log.v(TAG, "handlePutRequest"); + + ServerOperation op = new ServerOperation(this, mInput, type, mMaxPacketLength, mListener); + try { + int response = -1; + + Byte srm = (Byte)op.requestHeader.getHeader(HeaderSet.SINGLE_RESPONSE_MODE); + if (srm == ObexHelper.OBEX_SRM_ENABLED) { + if (VERBOSE) Log.v(TAG, "handlePutRequest srm == ObexHelper.OBEX_SRM_ENABLED"); + if (mSrmServer.getLocalSrmCapability() == ObexHelper.SRM_CAPABLE) { + if (VERBOSE) Log.v(TAG, "ObexHelper.SRM_CAPABLE"); + op.replyHeader.setHeader(HeaderSet.SINGLE_RESPONSE_MODE, ObexHelper.OBEX_SRM_ENABLED); + if (mSrmServer.getLocalSrmpWait()) { + if (VERBOSE) Log.v(TAG, "handlePutRequest: Server SRMP header set to WAIT"); + op.replyHeader.setHeader(HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER, + ObexHelper.OBEX_SRM_PARAM_WAIT); + } + } else { + if (VERBOSE) Log.v(TAG, "ObexHelper.SRM_INCAPABLE"); + op.replyHeader.setHeader(HeaderSet.SINGLE_RESPONSE_MODE, + ObexHelper.OBEX_SRM_DISABLED); + } + } + + if ((op.finalBitSet) && !op.isValidBody()) { + response = validateResponseCode(mListener + .onDelete(op.requestHeader, op.replyHeader)); + } else { + response = validateResponseCode(mListener.onPut(op)); + } + if (response != ResponseCodes.OBEX_HTTP_OK && !op.isAborted) { + if (VERBOSE) Log.v(TAG, "handlePutRequest pre != HTTP_OK sendReply"); + op.sendReply(response, false, false); + } else if (!op.isAborted) { + String nameHeader = (String)op.replyHeader.getHeader(HeaderSet.NAME); + if (!op.finalBitSet && nameHeader != null) { + if (VERBOSE) Log.v(TAG, "handlePutRequest Saved: "+nameHeader); + //Donot Include NameHeader in CONTINUE + op.replyHeader.setHeader(HeaderSet.NAME, null); + } + // wait for the final bit + while (!op.finalBitSet) { + if (VERBOSE) Log.v(TAG, "handlePutRequest pre looped sendReply"); + op.sendReply(ResponseCodes.OBEX_HTTP_CONTINUE, op.mSingleResponseActive, false); + } + if (nameHeader != null) { + if (VERBOSE) Log.v(TAG, "handlePutRequest SETHeader: "+nameHeader); + //PUT NameHeader in Final Packet + op.replyHeader.setHeader(HeaderSet.NAME, nameHeader); + } + op.sendReply(response, false,false); + } + } catch (Exception e) { + /*To fix bugs in aborted cases, + *(client abort file transfer prior to the last packet which has the end of body header, + *internal error should not be sent because server has already replied with + *OK response in "sendReply") + */ + if (!op.isAborted) { + sendResponse(ResponseCodes.OBEX_HTTP_INTERNAL_ERROR, null); + } + } + } + + /** + * Handles a GET request from a client. This method will provide a + * ServerOperation object to the request handler. The + * ServerOperation object will handle the rest of the request. + * It will also send replies and receive requests until the final reply + * should be sent. When the final reply should be sent, this method will get + * the response code to use and send the reply. The + * ServerOperation object will always reply with a + * OBEX_HTTP_CONTINUE reply. It will only reply if further information is + * needed. + * @param type the type of request received; either 0x03 or 0x83 + * @throws IOException if an error occurred at the transport layer + */ + private void handleGetRequest(int type) throws IOException { + if (VERBOSE) Log.v(TAG, "handleGetRequest"); + + ServerOperation op = new ServerOperation(this, mInput, type, mMaxPacketLength, mListener); + try { + Byte srm = (Byte)op.requestHeader.getHeader(HeaderSet.SINGLE_RESPONSE_MODE); + if (VERBOSE) Log.v(TAG, "handleGetRequest srm status" + srm ); + if (srm == ObexHelper.OBEX_SRM_ENABLED) { + if (VERBOSE) Log.v(TAG, "handleGetRequest srm == ObexHelper.OBEX_SRM_ENABLED"); + if (mSrmServer.getLocalSrmCapability() == ObexHelper.SRM_CAPABLE) { + if (VERBOSE) Log.v(TAG, "ObexHelper.getLocalSrmCapability()" + + "=ObexHelper.SRM_CAPABLE"); + op.replyHeader.setHeader(HeaderSet.SINGLE_RESPONSE_MODE, + ObexHelper.OBEX_SRM_ENABLED); + if (mSrmServer.getLocalSrmpWait()) { + if (VERBOSE) Log.v(TAG, "GetRequest:Server SRMP header set to WAIT"); + op.replyHeader.setHeader(HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER, + ObexHelper.OBEX_SRM_PARAM_WAIT); + } + } else { + if (VERBOSE) Log.v(TAG, "ObexHelper.getLocalSrmCapability() == "+ + "ObexHelper.SRM_INCAPABLE"); + op.replyHeader.setHeader(HeaderSet.SINGLE_RESPONSE_MODE, + ObexHelper.OBEX_SRM_DISABLED); + } + } + + int response = validateResponseCode(mListener.onGet(op)); + + if (!op.isAborted) { + op.sendReply(response, false,false); + } else { + if(mSrmServer.getLocalSrmStatus() == ObexHelper.LOCAL_SRM_ENABLED) { + sendResponse(ResponseCodes.OBEX_HTTP_OK, null); + } + } + } catch (Exception e) { + sendResponse(ResponseCodes.OBEX_HTTP_INTERNAL_ERROR, null); + } + } + + /** + * Send standard response. + * @param code the response code to send + * @param header the headers to include in the response + * @throws IOException if an IO error occurs + */ + public void sendResponse(int code, byte[] header) throws IOException { + int totalLength = 3; + byte[] data = null; + if (VERBOSE) Log.v(TAG,"sendResponse code "+code+" header : "+header); + OutputStream op = mOutput; + if (op == null) { + return; + } + + if (header != null) { + totalLength += header.length; + if (VERBOSE) Log.v(TAG,"header != null totalLength = "+totalLength); + data = new byte[totalLength]; + data[0] = (byte)code; + data[1] = (byte)(totalLength >> 8); + data[2] = (byte)totalLength; + System.arraycopy(header, 0, data, 3, header.length); + } else { + data = new byte[totalLength]; + data[0] = (byte)code; + data[1] = (byte)0x00; + data[2] = (byte)totalLength; + } + op.write(data); + op.flush(); + } + + /** + * Handles a SETPATH request from a client. This method will read the rest + * of the request from the client. Assuming the request is valid, it will + * create a HeaderSet object to pass to the + * ServerRequestHandler object. After the handler processes the + * request, this method will create a reply message to send to the server + * with the response code provided. + * @throws IOException if an error occurred at the transport layer + */ + private void handleSetPathRequest() throws IOException { + int length; + int flags; + @SuppressWarnings("unused") + int constants; + int totalLength = 3; + byte[] head = null; + int code = -1; + int bytesReceived; + HeaderSet request = new HeaderSet(); + HeaderSet reply = new HeaderSet(); + + length = mInput.read(); + length = (length << 8) + mInput.read(); + flags = mInput.read(); + constants = mInput.read(); + + if (length > mMaxPacketLength) { + code = ResponseCodes.OBEX_HTTP_REQ_TOO_LARGE; + totalLength = 3; + } else { + if (length > 5) { + byte[] headers = new byte[length - 5]; + bytesReceived = mInput.read(headers); + + while (bytesReceived != headers.length) { + bytesReceived += mInput.read(headers, bytesReceived, headers.length + - bytesReceived); + } + + ObexHelper.updateHeaderSet(request, headers); + + if (mListener.getConnectionId() != -1 && request.mConnectionID != null) { + mListener.setConnectionId(ObexHelper.convertToLong(request.mConnectionID)); + } else { + mListener.setConnectionId(1); + } + // the Auth chan is initiated by the server, client sent back the authResp . + if (request.mAuthResp != null) { + if (!handleAuthResp(request.mAuthResp)) { + code = ResponseCodes.OBEX_HTTP_UNAUTHORIZED; + mListener.onAuthenticationFailure(ObexHelper.getTagValue((byte)0x01, + request.mAuthResp)); + } + request.mAuthResp = null; + } + } + + if (code != ResponseCodes.OBEX_HTTP_UNAUTHORIZED) { + // the Auth challenge is initiated by the client + // the server will send back the authResp to the client + if (request.mAuthChall != null) { + handleAuthChall(request); + reply.mAuthResp = new byte[request.mAuthResp.length]; + System.arraycopy(request.mAuthResp, 0, reply.mAuthResp, 0, + reply.mAuthResp.length); + request.mAuthChall = null; + request.mAuthResp = null; + } + boolean backup = false; + boolean create = true; + if (!((flags & 1) == 0)) { + backup = true; + } + if (!((flags & 2) == 0)) { + create = false; + } + + try { + code = mListener.onSetPath(request, reply, backup, create); + } catch (Exception e) { + sendResponse(ResponseCodes.OBEX_HTTP_INTERNAL_ERROR, null); + return; + } + + code = validateResponseCode(code); + + if (reply.nonce != null) { + mChallengeDigest = new byte[16]; + System.arraycopy(reply.nonce, 0, mChallengeDigest, 0, 16); + } else { + mChallengeDigest = null; + } + + long id = mListener.getConnectionId(); + if (id == -1) { + reply.mConnectionID = null; + } else { + reply.mConnectionID = ObexHelper.convertToByteArray(id); + } + + head = ObexHelper.createHeader(reply, false); + totalLength += head.length; + + if (totalLength > mMaxPacketLength) { + totalLength = 3; + head = null; + code = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + } + } + + // Compute Length of OBEX SETPATH packet + byte[] replyData = new byte[totalLength]; + replyData[0] = (byte)code; + replyData[1] = (byte)(totalLength >> 8); + replyData[2] = (byte)totalLength; + if (head != null) { + System.arraycopy(head, 0, replyData, 3, head.length); + } + /* + * Write the OBEX SETPATH packet to the server. Byte 0: response code + * Byte 1&2: Connect Packet Length Byte 3 to n: headers + */ + mOutput.write(replyData); + mOutput.flush(); + } + + /** + * Handles a disconnect request from a client. This method will read the + * rest of the request from the client. Assuming the request is valid, it + * will create a HeaderSet object to pass to the + * ServerRequestHandler object. After the handler processes the + * request, this method will create a reply message to send to the server. + * @throws IOException if an error occurred at the transport layer + */ + private void handleDisconnectRequest() throws IOException { + int length; + int code = ResponseCodes.OBEX_HTTP_OK; + int totalLength = 3; + byte[] head = null; + int bytesReceived; + HeaderSet request = new HeaderSet(); + HeaderSet reply = new HeaderSet(); + + length = mInput.read(); + length = (length << 8) + mInput.read(); + + if (length > mMaxPacketLength) { + code = ResponseCodes.OBEX_HTTP_REQ_TOO_LARGE; + totalLength = 3; + } else { + if (length > 3) { + byte[] headers = new byte[length - 3]; + bytesReceived = mInput.read(headers); + + while (bytesReceived != headers.length) { + bytesReceived += mInput.read(headers, bytesReceived, headers.length + - bytesReceived); + } + + ObexHelper.updateHeaderSet(request, headers); + } + + if (mListener.getConnectionId() != -1 && request.mConnectionID != null) { + mListener.setConnectionId(ObexHelper.convertToLong(request.mConnectionID)); + } else { + mListener.setConnectionId(1); + } + + if (request.mAuthResp != null) { + if (!handleAuthResp(request.mAuthResp)) { + code = ResponseCodes.OBEX_HTTP_UNAUTHORIZED; + mListener.onAuthenticationFailure(ObexHelper.getTagValue((byte)0x01, + request.mAuthResp)); + } + request.mAuthResp = null; + } + + if (code != ResponseCodes.OBEX_HTTP_UNAUTHORIZED) { + + if (request.mAuthChall != null) { + handleAuthChall(request); + request.mAuthChall = null; + } + + try { + mListener.onDisconnect(request, reply); + } catch (Exception e) { + sendResponse(ResponseCodes.OBEX_HTTP_INTERNAL_ERROR, null); + return; + } + + long id = mListener.getConnectionId(); + if (id == -1) { + reply.mConnectionID = null; + } else { + reply.mConnectionID = ObexHelper.convertToByteArray(id); + } + + head = ObexHelper.createHeader(reply, false); + totalLength += head.length; + + if (totalLength > mMaxPacketLength) { + totalLength = 3; + head = null; + code = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + } + } + + // Compute Length of OBEX CONNECT packet + byte[] replyData; + if (head != null) { + replyData = new byte[3 + head.length]; + } else { + replyData = new byte[3]; + } + replyData[0] = (byte)code; + replyData[1] = (byte)(totalLength >> 8); + replyData[2] = (byte)totalLength; + if (head != null) { + System.arraycopy(head, 0, replyData, 3, head.length); + } + /* + * Write the OBEX DISCONNECT packet to the server. Byte 0: response code + * Byte 1&2: Connect Packet Length Byte 3 to n: headers + */ + mOutput.write(replyData); + mOutput.flush(); + } + + /** + * Handles a connect request from a client. This method will read the rest + * of the request from the client. Assuming the request is valid, it will + * create a HeaderSet object to pass to the + * ServerRequestHandler object. After the handler processes the + * request, this method will create a reply message to send to the server + * with the response code provided. + * @throws IOException if an error occurred at the transport layer + */ + private void handleConnectRequest() throws IOException { + int packetLength; + @SuppressWarnings("unused") + int version; + @SuppressWarnings("unused") + int flags; + int totalLength = 7; + byte[] head = null; + int code = -1; + HeaderSet request = new HeaderSet(); + HeaderSet reply = new HeaderSet(); + int bytesReceived; + + /* + * Read in the length of the OBEX packet, OBEX version, flags, and max + * packet length + */ + packetLength = mInput.read(); + packetLength = (packetLength << 8) + mInput.read(); + version = mInput.read(); + flags = mInput.read(); + mMaxPacketLength = mInput.read(); + mMaxPacketLength = (mMaxPacketLength << 8) + mInput.read(); + + // should we check it? + if (mMaxPacketLength > ObexHelper.MAX_PACKET_SIZE_INT) { + mMaxPacketLength = ObexHelper.MAX_PACKET_SIZE_INT; + } + + if (packetLength > mMaxPacketLength) { + code = ResponseCodes.OBEX_HTTP_REQ_TOO_LARGE; + totalLength = 7; + } else { + if (packetLength > 7) { + byte[] headers = new byte[packetLength - 7]; + bytesReceived = mInput.read(headers); + + while (bytesReceived != headers.length) { + bytesReceived += mInput.read(headers, bytesReceived, headers.length + - bytesReceived); + } + + ObexHelper.updateHeaderSet(request, headers); + } + + if (mSrmServer.getLocalSrmCapability() == ObexHelper.SRM_CAPABLE) { + /* + * As per GOEP TS Spec the Server should ignore the SRM header sent in Connect + */ + Byte byteHeader = (Byte)request.getHeader(HeaderSet.SINGLE_RESPONSE_MODE); + if((byteHeader == ObexHelper.OBEX_SRM_SUPPORTED) || + (byteHeader == ObexHelper.OBEX_SRM_DISABLED) + || (byteHeader == ObexHelper.OBEX_SRM_ENABLED)) { + if (VERBOSE) Log.v(TAG, + "handleConnectRequest: SRM Header received in Connect.. Ignored"); + } + if (mSrmServer.getLocalSrmParamStatus()) { + if (VERBOSE) Log.v(TAG, "handleConnectRequest: Enabled the SRMP WAIT"); + mSrmServer.setLocalSrmpWait(ObexHelper.SRMP_ENABLED); + } else { + if (VERBOSE) Log.v(TAG, "handleConnectRequest: Disabled the SRMP WAIT"); + mSrmServer.setLocalSrmpWait(ObexHelper.SRMP_DISABLED); + } + } + + if (mListener.getConnectionId() != -1 && request.mConnectionID != null) { + mListener.setConnectionId(ObexHelper.convertToLong(request.mConnectionID)); + } else { + mListener.setConnectionId(1); + } + + if (request.mAuthResp != null) { + if (!handleAuthResp(request.mAuthResp)) { + code = ResponseCodes.OBEX_HTTP_UNAUTHORIZED; + mListener.onAuthenticationFailure(ObexHelper.getTagValue((byte)0x01, + request.mAuthResp)); + } + request.mAuthResp = null; + } + + if (code != ResponseCodes.OBEX_HTTP_UNAUTHORIZED) { + if (request.mAuthChall != null) { + handleAuthChall(request); + reply.mAuthResp = new byte[request.mAuthResp.length]; + System.arraycopy(request.mAuthResp, 0, reply.mAuthResp, 0, + reply.mAuthResp.length); + request.mAuthChall = null; + request.mAuthResp = null; + } + + try { + code = mListener.onConnect(request, reply); + code = validateResponseCode(code); + + if (reply.nonce != null) { + mChallengeDigest = new byte[16]; + System.arraycopy(reply.nonce, 0, mChallengeDigest, 0, 16); + } else { + mChallengeDigest = null; + } + long id = mListener.getConnectionId(); + if (id == -1) { + reply.mConnectionID = null; + } else { + reply.mConnectionID = ObexHelper.convertToByteArray(id); + } + + head = ObexHelper.createHeader(reply, false); + totalLength += head.length; + + if (totalLength > mMaxPacketLength) { + totalLength = 7; + head = null; + code = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + } catch (Exception e) { + e.printStackTrace(); + totalLength = 7; + head = null; + code = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + + } + } + + // Compute Length of OBEX CONNECT packet + byte[] length = ObexHelper.convertToByteArray(totalLength); + + /* + * Write the OBEX CONNECT packet to the server. Byte 0: response code + * Byte 1&2: Connect Packet Length Byte 3: OBEX Version Number + * (Presently, 0x10) Byte 4: Flags (For TCP 0x00) Byte 5&6: Max OBEX + * Packet Length Byte 7 to n: headers + */ + byte[] sendData = new byte[totalLength]; + sendData[0] = (byte)code; + sendData[1] = length[2]; + sendData[2] = length[3]; + sendData[3] = (byte)0x10; + sendData[4] = (byte)0x00; + sendData[5] = (byte)(mMaxPacketLength >> 8); + sendData[6] = (byte)(mMaxPacketLength & 0xFF); + + if (head != null) { + System.arraycopy(head, 0, sendData, 7, head.length); + } + + mOutput.write(sendData); + mOutput.flush(); + } + + /** + * Closes the server session - in detail close I/O streams and the + * underlying transport layer. Internal flag is also set so that later + * attempt to read/write will throw an exception. + */ + public synchronized void close() { + if (mListener != null) { + mListener.onClose(); + } + try { + mInput.close(); + mOutput.close(); + mTransport.close(); + mClosed = true; + } catch (Exception e) { + } + mTransport = null; + mInput = null; + mOutput = null; + mListener = null; + } + + /** + * Verifies that the response code is valid. If it is not valid, it will + * return the OBEX_HTTP_INTERNAL_ERROR response code. + * @param code the response code to check + * @return the valid response code or OBEX_HTTP_INTERNAL_ERROR + * if code is not valid + */ + private int validateResponseCode(int code) { + + if ((code >= ResponseCodes.OBEX_HTTP_OK) && (code <= ResponseCodes.OBEX_HTTP_PARTIAL)) { + return code; + } + if ((code >= ResponseCodes.OBEX_HTTP_MULT_CHOICE) + && (code <= ResponseCodes.OBEX_HTTP_USE_PROXY)) { + return code; + } + if ((code >= ResponseCodes.OBEX_HTTP_BAD_REQUEST) + && (code <= ResponseCodes.OBEX_HTTP_UNSUPPORTED_TYPE)) { + return code; + } + if ((code >= ResponseCodes.OBEX_HTTP_INTERNAL_ERROR) + && (code <= ResponseCodes.OBEX_HTTP_VERSION)) { + return code; + } + if ((code >= ResponseCodes.OBEX_DATABASE_FULL) + && (code <= ResponseCodes.OBEX_DATABASE_LOCKED)) { + return code; + } + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + +} diff --git a/btobex/javax/btobex/SessionNotifier.java b/btobex/javax/btobex/SessionNotifier.java new file mode 100644 index 00000000000..f9b9f774783 --- /dev/null +++ b/btobex/javax/btobex/SessionNotifier.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * Copyright (c) 2008-2009, Motorola, Inc. + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * - Neither the name of the Motorola, Inc. nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package javax.btobex; + +import java.io.IOException; + +/** + * The SessionNotifier interface defines a connection notifier for + * server-side OBEX connections. When a SessionNotifier is created + * and calls acceptAndOpen(), it will begin listening for clients + * to create a connection at the transport layer. When the transport layer + * connection is received, the acceptAndOpen() method will return a + * javax.microedition.io.Connection that is the connection to the + * client. The acceptAndOpen() method also takes a + * ServerRequestHandler argument that will process the requests + * from the client that connects to the server. + * @hide + */ +public interface SessionNotifier { + + /** + * Waits for a transport layer connection to be established and specifies + * the handler to handle the requests from the client. No authenticator is + * associated with this connection, therefore, it is implementation + * dependent as to how an authentication challenge and authentication + * response header will be received and processed. + *

+ *

Additional Note for OBEX over Bluetooth

If this method is called + * on a SessionNotifier object that does not have a + * ServiceRecord in the SDDB, the ServiceRecord + * for this object will be added to the SDDB. This method requests the BCC + * to put the local device in connectable mode so that it will respond to + * connection attempts by clients. + *

+ * The following checks are done to verify that the service record provided + * is valid. If any of these checks fail, then a + * ServiceRegistrationException is thrown. + *

    + *
  • ServiceClassIDList and ProtocolDescriptorList, the mandatory service + * attributes for a btgoep service record, must be present in + * the ServiceRecord associated with this notifier. + *
  • L2CAP, RFCOMM and OBEX must all be in the ProtocolDescriptorList + *
  • The ServiceRecord associated with this notifier must not + * have changed the RFCOMM server channel number + *
+ *

+ * This method will not ensure that ServiceRecord associated + * with this notifier is a completely valid service record. It is the + * responsibility of the application to ensure that the service record + * follows all of the applicable syntactic and semantic rules for service + * record correctness. + * @param handler the request handler that will respond to OBEX requests + * @return the connection to the client + * @throws IOException if an error occurs in the transport layer + * @throws NullPointerException if handler is null + */ + ObexSession acceptAndOpen(ServerRequestHandler handler) throws IOException; + + /** + * Waits for a transport layer connection to be established and specifies + * the handler to handle the requests from the client and the + * Authenticator to use to respond to authentication challenge + * and authentication response headers. + *

+ *

Additional Note for OBEX over Bluetooth

If this method is called + * on a SessionNotifier object that does not have a + * ServiceRecord in the SDDB, the ServiceRecord + * for this object will be added to the SDDB. This method requests the BCC + * to put the local device in connectable mode so that it will respond to + * connection attempts by clients. + *

+ * The following checks are done to verify that the service record provided + * is valid. If any of these checks fail, then a + * ServiceRegistrationException is thrown. + *

    + *
  • ServiceClassIDList and ProtocolDescriptorList, the mandatory service + * attributes for a btgoep service record, must be present in + * the ServiceRecord associated with this notifier. + *
  • L2CAP, RFCOMM and OBEX must all be in the ProtocolDescriptorList + *
  • The ServiceRecord associated with this notifier must not + * have changed the RFCOMM server channel number + *
+ *

+ * This method will not ensure that ServiceRecord associated + * with this notifier is a completely valid service record. It is the + * responsibility of the application to ensure that the service record + * follows all of the applicable syntactic and semantic rules for service + * record correctness. + * @param handler the request handler that will respond to OBEX requests + * @param auth the Authenticator to use with this connection; + * if null then no Authenticator will be + * used + * @return the connection to the client + * @throws IOException if an error occurs in the transport layer + * @throws NullPointerException if handler is null + */ + ObexSession acceptAndOpen(ServerRequestHandler handler, Authenticator auth) throws IOException; +} diff --git a/cmds/bootanimation/Android.mk b/cmds/bootanimation/Android.mk index d6ecbe31f5e..0d82b4ce09e 100644 --- a/cmds/bootanimation/Android.mk +++ b/cmds/bootanimation/Android.mk @@ -16,12 +16,34 @@ LOCAL_SHARED_LIBRARIES := \ libandroidfw \ libutils \ libbinder \ + libhardware \ libui \ libskia \ libEGL \ libGLESv1_CM \ libgui \ - libtinyalsa + libtinyalsa \ + libmedia + +ifeq ($(TARGET_CONTINUOUS_SPLASH_ENABLED),true) + LOCAL_CFLAGS += -DCONTINUOUS_SPLASH +endif + +ifeq ($(TARGET_BOOTANIMATION_PRELOAD),true) + LOCAL_CFLAGS += -DPRELOAD_BOOTANIMATION +endif + +ifeq ($(TARGET_BOOTANIMATION_TEXTURE_CACHE),true) + LOCAL_CFLAGS += -DNO_TEXTURE_CACHE=0 +endif + +ifeq ($(TARGET_BOOTANIMATION_TEXTURE_CACHE),false) + LOCAL_CFLAGS += -DNO_TEXTURE_CACHE=1 +endif + +ifeq ($(TARGET_BOOTANIMATION_USE_RGB565),true) + LOCAL_CFLAGS += -DUSE_565 +endif LOCAL_MODULE:= bootanimation diff --git a/cmds/bootanimation/BootAnimation.cpp b/cmds/bootanimation/BootAnimation.cpp index b2474f2f18b..c5c53e52c17 100644 --- a/cmds/bootanimation/BootAnimation.cpp +++ b/cmds/bootanimation/BootAnimation.cpp @@ -1,5 +1,6 @@ /* * Copyright (C) 2007 The Android Open Source Project + * Copyright (c) 2012-2014, The Linux Foundation. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +24,8 @@ #include #include #include +#include +#include #include @@ -49,12 +52,31 @@ #include #include +#include +#include +#include + +#include + #include "BootAnimation.h" #include "AudioPlayer.h" #define OEM_BOOTANIMATION_FILE "/oem/media/bootanimation.zip" #define SYSTEM_BOOTANIMATION_FILE "/system/media/bootanimation.zip" #define SYSTEM_ENCRYPTED_BOOTANIMATION_FILE "/system/media/bootanimation-encrypted.zip" +#define THEME_BOOTANIMATION_FILE "/data/system/theme/bootanimation.zip" + +#define OEM_SHUTDOWN_ANIMATION_FILE "/oem/media/shutdownanimation.zip" +#define SYSTEM_SHUTDOWN_ANIMATION_FILE "/system/media/shutdownanimation.zip" +#define SYSTEM_ENCRYPTED_SHUTDOWN_ANIMATION_FILE "/system/media/shutdownanimation-encrypted.zip" +#define THEME_SHUTDOWN_ANIMATION_FILE "/data/system/theme/shutdownanimation.zip" + +#define OEM_BOOT_MUSIC_FILE "/oem/media/boot.wav" +#define SYSTEM_BOOT_MUSIC_FILE "/system/media/boot.wav" + +#define OEM_SHUTDOWN_MUSIC_FILE "/oem/media/shutdown.wav" +#define SYSTEM_SHUTDOWN_MUSIC_FILE "/system/media/shutdown.wav" + #define EXIT_PROP_NAME "service.bootanim.exit" extern "C" int clock_nanosleep(clockid_t clock_id, int flags, @@ -67,6 +89,105 @@ static const int ANIM_ENTRY_NAME_MAX = 256; // --------------------------------------------------------------------------- +static pthread_mutex_t mp_lock; +static pthread_cond_t mp_cond; +static bool isMPlayerPrepared = false; +static bool isMPlayerCompleted = false; + +class MPlayerListener : public MediaPlayerListener +{ + void notify(int msg, int ext1, int ext2, const Parcel *obj) + { + switch (msg) { + case MEDIA_NOP: // interface test message + break; + case MEDIA_PREPARED: + pthread_mutex_lock(&mp_lock); + isMPlayerPrepared = true; + pthread_cond_signal(&mp_cond); + pthread_mutex_unlock(&mp_lock); + break; + case MEDIA_PLAYBACK_COMPLETE: + pthread_mutex_lock(&mp_lock); + isMPlayerCompleted = true; + pthread_cond_signal(&mp_cond); + pthread_mutex_unlock(&mp_lock); + break; + default: + break; + } + } +}; + +static void setPowerHint(int durationMs) { + int rc; + power_module_t *power_module = NULL; + + rc = hw_get_module(POWER_HARDWARE_MODULE_ID, + (const hw_module_t **)&power_module); + if (rc) { + ALOGE("%s: Failed to obtain reference to power module %s\n", + __func__, strerror(-rc)); + return; + } + + if (power_module && power_module->powerHint) { + power_module->powerHint(power_module, POWER_HINT_CPU_BOOST, + (void *)(static_cast(durationMs))); + } +} + +static long getFreeMemory(void) +{ + int fd = open("/proc/meminfo", O_RDONLY); + const char* const sums[] = { "MemFree:", "Cached:", NULL }; + const int sumsLen[] = { strlen("MemFree:"), strlen("Cached:"), 0 }; + int num = 2; + + if (fd < 0) { + ALOGW("Unable to open /proc/meminfo"); + return -1; + } + + char buffer[256]; + const int len = read(fd, buffer, sizeof(buffer)-1); + close(fd); + + if (len < 0) { + ALOGW("Unable to read /proc/meminfo"); + return -1; + } + buffer[len] = 0; + + size_t numFound = 0; + long mem = 0; + + char* p = buffer; + while (*p && numFound < num) { + int i = 0; + while (sums[i]) { + if (strncmp(p, sums[i], sumsLen[i]) == 0) { + p += sumsLen[i]; + while (*p == ' ') p++; + char* num = p; + while (*p >= '0' && *p <= '9') p++; + if (*p != 0) { + *p = 0; + p++; + if (*p == 0) p--; + } + mem += atoll(num); + numFound++; + break; + } + i++; + } + p++; + } + + return numFound > 0 ? mem : -1; +} + BootAnimation::BootAnimation() : Thread(false), mZip(NULL) { mSession = new SurfaceComposerClient(); @@ -160,26 +281,25 @@ status_t BootAnimation::initTexture(Texture* texture, AssetManager& assets, return NO_ERROR; } -status_t BootAnimation::initTexture(const Animation::Frame& frame) +status_t BootAnimation::initTexture(void* buffer, size_t len) { //StopWatch watch("blah"); SkBitmap bitmap; - SkMemoryStream stream(frame.map->getDataPtr(), frame.map->getDataLength()); + SkMemoryStream stream(buffer, len); SkImageDecoder* codec = SkImageDecoder::Factory(&stream); if (codec) { codec->setDitherImage(false); codec->decode(&stream, &bitmap, + #ifdef USE_565 + kRGB_565_SkColorType, + #else kN32_SkColorType, + #endif SkImageDecoder::kDecodePixels_Mode); delete codec; } - // FileMap memory is never released until application exit. - // Release it now as the texture is already loaded and the memory used for - // the packed resource can be released. - frame.map->release(); - // ensure we can call getPixels(). No need to call unlock, since the // bitmap will go out of scope when we return from this method. bitmap.lockPixels(); @@ -236,6 +356,18 @@ status_t BootAnimation::readyToRun() { status_t status = SurfaceComposerClient::getDisplayInfo(dtoken, &dinfo); if (status) return -1; + char value[PROPERTY_VALUE_MAX]; + property_get("persist.panel.orientation", value, "0"); + int orient = atoi(value) / 90; + + if(orient == eOrientation90 || orient == eOrientation270) { + int temp = dinfo.h; + dinfo.h = dinfo.w; + dinfo.w = temp; + } + + Rect destRect(dinfo.w, dinfo.h); + mSession->setDisplayProjection(dtoken, orient, destRect, destRect); // create the native surface sp control = session()->createSurface(String8("BootAnimation"), @@ -286,21 +418,57 @@ status_t BootAnimation::readyToRun() { char decrypt[PROPERTY_VALUE_MAX]; property_get("vold.decrypt", decrypt, ""); + // Use customized resources for boot and showdown animation + // instead of system predefined boot animation files. bool encryptedAnimation = atoi(decrypt) != 0 || !strcmp("trigger_restart_min_framework", decrypt); ZipFileRO* zipFile = NULL; if ((encryptedAnimation && - (access(SYSTEM_ENCRYPTED_BOOTANIMATION_FILE, R_OK) == 0) && - ((zipFile = ZipFileRO::open(SYSTEM_ENCRYPTED_BOOTANIMATION_FILE)) != NULL)) || + (access(getAnimationFileName(IMG_ENC), R_OK) == 0) && + ((zipFile = ZipFileRO::open(getAnimationFileName(IMG_ENC))) != NULL)) || + + ((access(getAnimationFileName(IMG_DATA), R_OK) == 0) && + ((zipFile = ZipFileRO::open(getAnimationFileName(IMG_DATA))) != NULL)) || - ((access(OEM_BOOTANIMATION_FILE, R_OK) == 0) && - ((zipFile = ZipFileRO::open(OEM_BOOTANIMATION_FILE)) != NULL)) || + ((access(getAnimationFileName(IMG_THEME), R_OK) == 0) && + ((zipFile = ZipFileRO::open(getAnimationFileName(IMG_THEME))) != NULL)) || - ((access(SYSTEM_BOOTANIMATION_FILE, R_OK) == 0) && - ((zipFile = ZipFileRO::open(SYSTEM_BOOTANIMATION_FILE)) != NULL))) { + ((access(getAnimationFileName(IMG_SYS), R_OK) == 0) && + ((zipFile = ZipFileRO::open(getAnimationFileName(IMG_SYS))) != NULL))) { mZip = zipFile; } +#ifdef PRELOAD_BOOTANIMATION + // Preload the bootanimation zip on memory, so we don't stutter + // when showing the animation + FILE* fd; + if (encryptedAnimation && access(SYSTEM_ENCRYPTED_BOOTANIMATION_FILE, R_OK) == 0) + fd = fopen(SYSTEM_ENCRYPTED_BOOTANIMATION_FILE, "r"); + else if (access(OEM_BOOTANIMATION_FILE, R_OK) == 0) + fd = fopen(OEM_BOOTANIMATION_FILE, "r"); + else if (access(SYSTEM_BOOTANIMATION_FILE, R_OK) == 0) + fd = fopen(SYSTEM_BOOTANIMATION_FILE, "r"); + else + return NO_ERROR; + + if (fd != NULL) { + // We could use readahead.. + // ... if bionic supported it :( + //readahead(fd, 0, INT_MAX); + void *crappyBuffer = malloc(2*1024*1024); + if (crappyBuffer != NULL) { + // Read all the zip + while (!feof(fd)) + fread(crappyBuffer, 1024, 2*1024, fd); + + free(crappyBuffer); + } else { + ALOGW("Unable to allocate memory to preload the animation"); + } + fclose(fd); + } +#endif + return NO_ERROR; } @@ -451,6 +619,7 @@ bool BootAnimation::readFile(const char* name, String8& outString) bool BootAnimation::movie() { + char value[PROPERTY_VALUE_MAX]; String8 desString; if (!readFile("desc.txt", desString)) { @@ -555,6 +724,7 @@ bool BootAnimation::movie() mZip->endIteration(cookie); +#ifndef CONTINUOUS_SPLASH // clear screen glShadeModel(GL_FLAT); glDisable(GL_DITHER); @@ -564,6 +734,7 @@ bool BootAnimation::movie() glClear(GL_COLOR_BUFFER_BIT); eglSwapBuffers(mDisplay, mSurface); +#endif glBindTexture(GL_TEXTURE_2D, 0); glEnable(GL_TEXTURE_2D); @@ -581,10 +752,45 @@ bool BootAnimation::movie() Region clearReg(Rect(mWidth, mHeight)); clearReg.subtractSelf(Rect(xc, yc, xc+animation.width, yc+animation.height)); + pthread_mutex_init(&mp_lock, NULL); + pthread_cond_init(&mp_cond, NULL); + + property_get("persist.sys.silent", value, "null"); + if (strncmp(value, "1", 1) != 0) { + playBackgroundMusic(); + } for (size_t i=0 ; i 48 * 1024 * 1024) ? 1 : 0; + #endif + + /*calculate if we need to runtime save memory + * condition: runtime free memory is less than the textures that will used. + * needSaveMem default to be false + */ + GLint mMaxTextureSize; + bool needSaveMem = false; + GLuint mTextureid; + glGetIntegerv(GL_MAX_TEXTURE_SIZE, &mMaxTextureSize); + //ALOGD("freemem:%ld, %d", getFreeMemory(), mMaxTextureSize); + if(getFreeMemory() < mMaxTextureSize * mMaxTextureSize * fcount / 1024 || noTextureCache) { + ALOGD("Use save memory method, maybe small fps in actual."); + needSaveMem = true; + glGenTextures(1, &mTextureid); + glBindTexture(GL_TEXTURE_2D, mTextureid); + glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + } + + setPowerHint(frameDuration * 1000 * fcount); for (int r=0 ; !part.count || rplayFile(part.audioFile); } + glBindTexture(GL_TEXTURE_2D, 0); glClearColor( part.backgroundColor[0], part.backgroundColor[1], @@ -606,17 +813,9 @@ bool BootAnimation::movie() const Animation::Frame& frame(part.frames[j]); nsecs_t lastFrame = systemTime(); - if (r > 0) { - glBindTexture(GL_TEXTURE_2D, frame.tid); - } else { - if (part.count != 1) { - glGenTextures(1, &frame.tid); - glBindTexture(GL_TEXTURE_2D, frame.tid); - glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - } - initTexture(frame); - } + initTexture( + frame.map->getDataPtr(), + frame.map->getDataLength()); if (!clearReg.isEmpty()) { Region::const_iterator head(clearReg.begin()); @@ -658,18 +857,153 @@ bool BootAnimation::movie() break; } - // free the textures for this part - if (part.count != 1) { - for (size_t j=0 ; j mp = new MediaPlayer(); + sp mListener = new MPlayerListener(); + if (mp != NULL) { + ALOGD("starting to play %s", fileName); + mp->setListener(mListener); + + if (mp->setDataSource(NULL, fileName, NULL) == NO_ERROR) { + mp->setAudioStreamType(AUDIO_STREAM_ENFORCED_AUDIBLE); + mp->prepare(); + } else { + ALOGE("failed to setDataSource for %s", fileName); + return NULL; + } + + //waiting for media player is prepared. + pthread_mutex_lock(&mp_lock); + while (!isMPlayerPrepared) { + pthread_cond_wait(&mp_cond, &mp_lock); + } + pthread_mutex_unlock(&mp_lock); + + audio_devices_t device = AudioSystem::getDevicesForStream(AUDIO_STREAM_ENFORCED_AUDIBLE); + AudioSystem::initStreamVolume(AUDIO_STREAM_ENFORCED_AUDIBLE,0,7); + AudioSystem::setStreamVolumeIndex(AUDIO_STREAM_ENFORCED_AUDIBLE, 7, device); + + AudioSystem::getStreamVolumeIndex(AUDIO_STREAM_ENFORCED_AUDIBLE, &index, device); + if (index != 0) { + ALOGD("playing %s", fileName); + mp->seekTo(0); + mp->start(); + } else { + ALOGW("current volume is zero."); + } + } + return NULL; +} // --------------------------------------------------------------------------- } diff --git a/cmds/bootanimation/BootAnimation.h b/cmds/bootanimation/BootAnimation.h index f968b255d37..a7bfadf4737 100644 --- a/cmds/bootanimation/BootAnimation.h +++ b/cmds/bootanimation/BootAnimation.h @@ -40,6 +40,12 @@ class SurfaceControl; class BootAnimation : public Thread, public IBinder::DeathRecipient { public: + enum { + eOrientationDefault = 0, + eOrientation90 = 1, + eOrientation180 = 2, + eOrientation270 = 3, + }; BootAnimation(); virtual ~BootAnimation(); @@ -82,17 +88,23 @@ class BootAnimation : public Thread, public IBinder::DeathRecipient }; status_t initTexture(Texture* texture, AssetManager& asset, const char* name); - status_t initTexture(const Animation::Frame& frame); + status_t initTexture(void* buffer, size_t len); bool android(); bool readFile(const char* name, String8& outString); bool movie(); + enum ImageID { IMG_DATA = 0, IMG_SYS = 1, IMG_ENC = 2, IMG_THEME = 3 }; + char *getAnimationFileName(ImageID image); + char *getBootRingtoneFileName(ImageID image); + void playBackgroundMusic(); + bool checkBootState(); void checkExit(); + void checkShowAndroid(); sp mSession; sp mAudioPlayer; AssetManager mAssets; - Texture mAndroid[2]; + Texture mAndroid[3]; int mWidth; int mHeight; EGLDisplay mDisplay; @@ -103,6 +115,7 @@ class BootAnimation : public Thread, public IBinder::DeathRecipient ZipFileRO *mZip; }; +static void* playMusic(void* arg); // --------------------------------------------------------------------------- }; // namespace android diff --git a/cmds/bootanimation/bootanimation_main.cpp b/cmds/bootanimation/bootanimation_main.cpp index 417e138b704..baf6c7df2df 100644 --- a/cmds/bootanimation/bootanimation_main.cpp +++ b/cmds/bootanimation/bootanimation_main.cpp @@ -46,8 +46,9 @@ int main(int argc, char** argv) property_get("debug.sf.nobootanimation", value, "0"); int noBootAnimation = atoi(value); ALOGI_IF(noBootAnimation, "boot animation disabled"); - if (!noBootAnimation) { + property_get("ro.alarm_boot", value, "false"); + if (!noBootAnimation && strcmp(value, "true")) { sp proc(ProcessState::self()); ProcessState::self()->startThreadPool(); diff --git a/cmds/idmap/create.cpp b/cmds/idmap/create.cpp index 593a1973669..2052f26384f 100644 --- a/cmds/idmap/create.cpp +++ b/cmds/idmap/create.cpp @@ -14,6 +14,11 @@ using namespace android; namespace { int get_zip_entry_crc(const char *zip_path, const char *entry_name, uint32_t *crc) { + if (crc == NULL) { + // As this function used to get the crc, but the address is NULL, do nothing. + return -1; + } + UniquePtr zip(ZipFileRO::open(zip_path)); if (zip.get() == NULL) { return -1; @@ -22,9 +27,12 @@ namespace { if (entry == NULL) { return -1; } - if (!zip->getEntryInfo(entry, NULL, NULL, NULL, NULL, NULL, (long*)crc)) { + long tmp; + if (!zip->getEntryInfo(entry, NULL, NULL, NULL, NULL, NULL, &tmp)) { return -1; } + // As the crc of the apk only contains 32bits, so the convert action is safe. + *crc = (uint32_t) tmp; zip->releaseEntry(entry); return 0; } @@ -149,26 +157,26 @@ namespace { } int create_idmap(const char *target_apk_path, const char *overlay_apk_path, - uint32_t **data, size_t *size) + const char *cache_path, uint32_t target_hash, uint32_t overlay_hash, uint32_t **data, + size_t *size) { uint32_t target_crc, overlay_crc; - if (get_zip_entry_crc(target_apk_path, AssetManager::RESOURCES_FILENAME, - &target_crc) == -1) { - return -1; - } - if (get_zip_entry_crc(overlay_apk_path, AssetManager::RESOURCES_FILENAME, - &overlay_crc) == -1) { - return -1; - } + + // In the original implementation, crc of the res tables are generated + // theme apks however do not need a restable, everything is in assets/ + // instead timestamps are used + target_crc = 0; + overlay_crc = 0; AssetManager am; - bool b = am.createIdmap(target_apk_path, overlay_apk_path, target_crc, overlay_crc, - data, size); + bool b = am.createIdmap(target_apk_path, overlay_apk_path, cache_path, target_crc, + overlay_crc, target_hash, overlay_hash, data, size); return b ? 0 : -1; } int create_and_write_idmap(const char *target_apk_path, const char *overlay_apk_path, - int fd, bool check_if_stale) + const char *cache_path, uint32_t target_hash, uint32_t overlay_hash, int fd, + bool check_if_stale) { if (check_if_stale) { if (!is_idmap_stale_fd(target_apk_path, overlay_apk_path, fd)) { @@ -180,7 +188,8 @@ namespace { uint32_t *data = NULL; size_t size; - if (create_idmap(target_apk_path, overlay_apk_path, &data, &size) == -1) { + if (create_idmap(target_apk_path, overlay_apk_path, cache_path, target_hash, overlay_hash, + &data, &size) == -1) { return -1; } @@ -195,6 +204,7 @@ namespace { } int idmap_create_path(const char *target_apk_path, const char *overlay_apk_path, + const char *cache_path, uint32_t target_hash, uint32_t overlay_hash, const char *idmap_path) { if (!is_idmap_stale_path(target_apk_path, overlay_apk_path, idmap_path)) { @@ -207,7 +217,8 @@ int idmap_create_path(const char *target_apk_path, const char *overlay_apk_path, return EXIT_FAILURE; } - int r = create_and_write_idmap(target_apk_path, overlay_apk_path, fd, false); + int r = create_and_write_idmap(target_apk_path, overlay_apk_path, cache_path, + target_hash, overlay_hash, fd, false); close(fd); if (r != 0) { unlink(idmap_path); @@ -215,8 +226,10 @@ int idmap_create_path(const char *target_apk_path, const char *overlay_apk_path, return r == 0 ? EXIT_SUCCESS : EXIT_FAILURE; } -int idmap_create_fd(const char *target_apk_path, const char *overlay_apk_path, int fd) +int idmap_create_fd(const char *target_apk_path, const char *overlay_apk_path, + const char *cache_path, uint32_t target_hash, uint32_t overlay_hash, int fd) { - return create_and_write_idmap(target_apk_path, overlay_apk_path, fd, true) == 0 ? + return create_and_write_idmap(target_apk_path, overlay_apk_path, cache_path, target_hash, + overlay_hash, fd, true) == 0 ? EXIT_SUCCESS : EXIT_FAILURE; } diff --git a/cmds/idmap/idmap.cpp b/cmds/idmap/idmap.cpp index 90cfa2c610f..cc3f231824a 100644 --- a/cmds/idmap/idmap.cpp +++ b/cmds/idmap/idmap.cpp @@ -66,29 +66,31 @@ EXAMPLES \n\ Display an idmap file: \n\ \n\ $ adb shell idmap --inspect /data/resource-cache/vendor@overlay@overlay.apk@idmap \n\ - SECTION ENTRY VALUE COMMENT \n\ - IDMAP HEADER magic 0x706d6469 \n\ - base crc 0xb65a383f \n\ - overlay crc 0x7b9675e8 \n\ - base path .......... /path/to/target.apk \n\ - overlay path .......... /path/to/overlay.apk \n\ - DATA HEADER target pkg 0x0000007f \n\ - types count 0x00000003 \n\ - DATA BLOCK target type 0x00000002 \n\ - overlay type 0x00000002 \n\ - entry count 0x00000001 \n\ - entry offset 0x00000000 \n\ - entry 0x00000000 drawable/drawable \n\ - DATA BLOCK target type 0x00000003 \n\ - overlay type 0x00000003 \n\ - entry count 0x00000001 \n\ - entry offset 0x00000000 \n\ - entry 0x00000000 xml/integer \n\ - DATA BLOCK target type 0x00000004 \n\ - overlay type 0x00000004 \n\ - entry count 0x00000001 \n\ - entry offset 0x00000000 \n\ - entry 0x00000000 raw/lorem_ipsum \n\ + SECTION ENTRY VALUE COMMENT \n\ + IDMAP HEADER magic 0x706d6469 \n\ + base crc 0xb65a383f \n\ + overlay crc 0x7b9675e8 \n\ + base mtime 0x1eb47d51 \n\ + overlay mtime 0x185f87a2 \n\ + base path .......... /path/to/target.apk \n\ + overlay path .......... /path/to/overlay.apk \n\ + DATA HEADER target pkg 0x0000007f \n\ + types count 0x00000003 \n\ + DATA BLOCK target type 0x00000002 \n\ + overlay type 0x00000002 \n\ + entry count 0x00000001 \n\ + entry offset 0x00000000 \n\ + entry 0x00000000 drawable/drawable \n\ + DATA BLOCK target type 0x00000003 \n\ + overlay type 0x00000003 \n\ + entry count 0x00000001 \n\ + entry offset 0x00000000 \n\ + entry 0x00000000 xml/integer \n\ + DATA BLOCK target type 0x00000004 \n\ + overlay type 0x00000004 \n\ + entry count 0x00000001 \n\ + entry offset 0x00000000 \n\ + entry 0x00000000 raw/lorem_ipsum \n\ \n\ In this example, the overlay package provides three alternative resource values:\n\ drawable/drawable, xml/integer, and raw/lorem_ipsum \n\ @@ -120,7 +122,8 @@ NOTES \n\ } int maybe_create_fd(const char *target_apk_path, const char *overlay_apk_path, - const char *idmap_str) + const char *cache_path, const char *idmap_str, const char *target_hash_str, + const char *overlay_hash_str) { // anyone (not just root or system) may do --fd -- the file has // already been opened by someone else on our behalf @@ -141,12 +144,16 @@ NOTES \n\ ALOGD("error: failed to read apk %s: %s\n", overlay_apk_path, strerror(errno)); return -1; } + int target_hash = strtol(target_hash_str, 0, 10); + int overlay_hash = strtol(overlay_hash_str, 0, 10); - return idmap_create_fd(target_apk_path, overlay_apk_path, idmap_fd); + return idmap_create_fd(target_apk_path, overlay_apk_path, cache_path, target_hash, + overlay_hash, idmap_fd); } int maybe_create_path(const char *target_apk_path, const char *overlay_apk_path, - const char *idmap_path) + const char *cache_path, const char *idmap_path, const char *target_hash_str, + const char *overlay_hash_str) { if (!verify_root_or_system()) { fprintf(stderr, "error: permission denied: not user root or user system\n"); @@ -163,7 +170,10 @@ NOTES \n\ return -1; } - return idmap_create_path(target_apk_path, overlay_apk_path, idmap_path); + int target_hash = strtol(target_hash_str, 0, 10); + int overlay_hash = strtol(overlay_hash_str, 0, 10); + return idmap_create_path(target_apk_path, overlay_apk_path, cache_path, target_hash, + overlay_hash, idmap_path); } int maybe_scan(const char *overlay_dir, const char *target_package_name, @@ -222,12 +232,12 @@ int main(int argc, char **argv) return 0; } - if (argc == 5 && !strcmp(argv[1], "--fd")) { - return maybe_create_fd(argv[2], argv[3], argv[4]); + if (argc == 8 && !strcmp(argv[1], "--fd")) { + return maybe_create_fd(argv[2], argv[3], argv[4], argv[5], argv[6], argv[7]); } - if (argc == 5 && !strcmp(argv[1], "--path")) { - return maybe_create_path(argv[2], argv[3], argv[4]); + if (argc == 8 && !strcmp(argv[1], "--path")) { + return maybe_create_path(argv[2], argv[3], argv[4], argv[5], argv[6], argv[7]); } if (argc == 6 && !strcmp(argv[1], "--scan")) { diff --git a/cmds/idmap/idmap.h b/cmds/idmap/idmap.h index f507dd8530a..6a9c5ef7881 100644 --- a/cmds/idmap/idmap.h +++ b/cmds/idmap/idmap.h @@ -19,9 +19,12 @@ #endif int idmap_create_path(const char *target_apk_path, const char *overlay_apk_path, + const char *cache_path, uint32_t target_hash, uint32_t overlay_hash, const char *idmap_path); -int idmap_create_fd(const char *target_apk_path, const char *overlay_apk_path, int fd); +int idmap_create_fd(const char *target_apk_path, const char *overlay_apk_path, + const char *cache_path, uint32_t target_hash, uint32_t overlay_hash, + int fd); // Regarding target_package_name: the idmap_scan implementation should // be able to extract this from the manifest in target_apk_path, diff --git a/cmds/idmap/inspect.cpp b/cmds/idmap/inspect.cpp index b9ac8a59de0..520a100ce72 100644 --- a/cmds/idmap/inspect.cpp +++ b/cmds/idmap/inspect.cpp @@ -200,6 +200,18 @@ namespace { } print("", "overlay crc", i, ""); + err = buf.nextUint32(&i); + if (err != NO_ERROR) { + return err; + } + print("", "base mtime", i, ""); + + err = buf.nextUint32(&i); + if (err != NO_ERROR) { + return err; + } + print("", "overlay mtime", i, ""); + err = buf.nextPath(path); if (err != NO_ERROR) { // printe done from IdmapBuffer::nextPath @@ -223,7 +235,8 @@ namespace { } status_t parse_data(IdmapBuffer& buf, const AssetManager& am) { - const uint32_t packageId = am.getResources().getBasePackageId(0); + const ResTable& rt = am.getResources(); + const uint32_t packageId = rt.getBasePackageId(rt.getBasePackageCount() - 1); uint16_t data16; status_t err = buf.nextUint16(&data16); diff --git a/cmds/idmap/scan.cpp b/cmds/idmap/scan.cpp index 1153f38a9c0..5c2761e49f4 100644 --- a/cmds/idmap/scan.cpp +++ b/cmds/idmap/scan.cpp @@ -187,6 +187,62 @@ namespace { dataMap->release(); return priority; } + + int idmap_scan(const char *overlay_dir, const char *target_package_name, + const char *target_apk_path, const char *idmap_dir, + SortedVector& overlayVector) + { + DIR *dir = opendir(overlay_dir); + if (dir == NULL) { + return EXIT_FAILURE; + } + + struct dirent *dirent; + while ((dirent = readdir(dir)) != NULL) { + struct stat st; + char overlay_apk_path[PATH_MAX + 1]; + snprintf(overlay_apk_path, PATH_MAX, "%s/%s", overlay_dir, dirent->d_name); + if (stat(overlay_apk_path, &st) < 0) { + continue; + } + if (!S_ISREG(st.st_mode) && !S_ISDIR(st.st_mode)) { + continue; + } + + if (S_ISDIR(st.st_mode)) { + String8 dir_name = String8(overlay_apk_path).getPathLeaf(); + if (dir_name == "." || dir_name == "..") { + // Skip the "." and ".." dir. + continue; + } + idmap_scan(overlay_apk_path, target_package_name, target_apk_path, idmap_dir, + overlayVector); + } else { + int priority = parse_apk(overlay_apk_path, target_package_name); + if (priority < 0) { + continue; + } + + String8 idmap_path(idmap_dir); + idmap_path.appendPath(flatten_path(overlay_apk_path + 1)); + idmap_path.append("@idmap"); + + if (idmap_create_path(target_apk_path, overlay_apk_path, NULL, 0, 0, + idmap_path.string()) != 0) { + ALOGE("error: failed to create idmap for target=%s overlay=%s idmap=%s\n", + target_apk_path, overlay_apk_path, idmap_path.string()); + continue; + } + + Overlay overlay(String8(overlay_apk_path), idmap_path, priority); + overlayVector.add(overlay); + } + } + + closedir(dir); + + return EXIT_SUCCESS; + } } int idmap_scan(const char *overlay_dir, const char *target_package_name, @@ -198,48 +254,13 @@ int idmap_scan(const char *overlay_dir, const char *target_package_name, return EXIT_FAILURE; } - DIR *dir = opendir(overlay_dir); - if (dir == NULL) { - return EXIT_FAILURE; - } - SortedVector overlayVector; - struct dirent *dirent; - while ((dirent = readdir(dir)) != NULL) { - struct stat st; - char overlay_apk_path[PATH_MAX + 1]; - snprintf(overlay_apk_path, PATH_MAX, "%s/%s", overlay_dir, dirent->d_name); - if (stat(overlay_apk_path, &st) < 0) { - continue; - } - if (!S_ISREG(st.st_mode)) { - continue; - } - - int priority = parse_apk(overlay_apk_path, target_package_name); - if (priority < 0) { - continue; - } - - String8 idmap_path(idmap_dir); - idmap_path.appendPath(flatten_path(overlay_apk_path + 1)); - idmap_path.append("@idmap"); - - if (idmap_create_path(target_apk_path, overlay_apk_path, idmap_path.string()) != 0) { - ALOGE("error: failed to create idmap for target=%s overlay=%s idmap=%s\n", - target_apk_path, overlay_apk_path, idmap_path.string()); - continue; - } - - Overlay overlay(String8(overlay_apk_path), idmap_path, priority); - overlayVector.add(overlay); - } - - closedir(dir); + int res = idmap_scan(overlay_dir, target_package_name, target_apk_path, idmap_dir, + overlayVector); - if (!writePackagesList(filename.string(), overlayVector)) { + if (res == EXIT_FAILURE || !writePackagesList(filename.string(), overlayVector)) { return EXIT_FAILURE; } - return EXIT_SUCCESS; + return res; } diff --git a/cmds/input/src/com/android/commands/input/Input.java b/cmds/input/src/com/android/commands/input/Input.java index 2a7c79bdfb5..9c7a7e47db9 100644 --- a/cmds/input/src/com/android/commands/input/Input.java +++ b/cmds/input/src/com/android/commands/input/Input.java @@ -47,6 +47,7 @@ public class Input { put("touchpad", InputDevice.SOURCE_TOUCHPAD); put("touchnavigation", InputDevice.SOURCE_TOUCH_NAVIGATION); put("joystick", InputDevice.SOURCE_JOYSTICK); + put("gesture", InputDevice.SOURCE_GESTURE_SENSOR); }}; @@ -234,6 +235,18 @@ private void injectKeyEvent(KeyEvent event) { InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH); } + private int getInputDeviceId(int inputSource) { + final int DEFAULT_DEVICE_ID = 0; + int[] devIds = InputDevice.getDeviceIds(); + for (int devId : devIds) { + InputDevice inputDev = InputDevice.getDevice(devId); + if (inputDev.supportsSource(inputSource)) { + return devId; + } + } + return DEFAULT_DEVICE_ID; + } + /** * Builds a MotionEvent and injects it into the event stream. * @@ -249,11 +262,10 @@ private void injectMotionEvent(int inputSource, int action, long when, float x, final int DEFAULT_META_STATE = 0; final float DEFAULT_PRECISION_X = 1.0f; final float DEFAULT_PRECISION_Y = 1.0f; - final int DEFAULT_DEVICE_ID = 0; final int DEFAULT_EDGE_FLAGS = 0; MotionEvent event = MotionEvent.obtain(when, when, action, x, y, pressure, DEFAULT_SIZE, - DEFAULT_META_STATE, DEFAULT_PRECISION_X, DEFAULT_PRECISION_Y, DEFAULT_DEVICE_ID, - DEFAULT_EDGE_FLAGS); + DEFAULT_META_STATE, DEFAULT_PRECISION_X, DEFAULT_PRECISION_Y, + getInputDeviceId(inputSource), DEFAULT_EDGE_FLAGS); event.setSource(inputSource); Log.i(TAG, "injectMotionEvent: " + event); InputManager.getInstance().injectInputEvent(event, diff --git a/cmds/media/src/com/android/commands/media/Media.java b/cmds/media/src/com/android/commands/media/Media.java index 6a8fb05da98..04d170e4e07 100644 --- a/cmds/media/src/com/android/commands/media/Media.java +++ b/cmds/media/src/com/android/commands/media/Media.java @@ -223,6 +223,26 @@ public void onVolumeInfoChanged(ParcelableVolumeInfo info) throws RemoteExceptio System.out.println("onVolumeInfoChanged " + info); } + @Override + public void onPlayItemResponse(boolean success) throws RemoteException { + System.out.println("onPlayItemResponse "); + } + + @Override + public void onUpdateNowPlayingEntries(long[] playList) throws RemoteException { + System.out.println("onUpdateNowPlayingEntries "); + } + + @Override + public void onUpdateFolderInfoBrowsedPlayer(String stringUri) throws RemoteException { + System.out.println("onUpdateFolderInfoBrowsedPlayer "); + } + + @Override + public void onUpdateNowPlayingContentChange() throws RemoteException { + System.out.println("onUpdateNowPlayingContentChange "); + } + void printUsageMessage() { try { System.out.println("V2Monitoring session " + mController.getTag() diff --git a/cmds/pm/src/com/android/commands/pm/Pm.java b/cmds/pm/src/com/android/commands/pm/Pm.java index 0a573ca9c73..7a01701f8dd 100644 --- a/cmds/pm/src/com/android/commands/pm/Pm.java +++ b/cmds/pm/src/com/android/commands/pm/Pm.java @@ -549,7 +549,10 @@ private String loadText(PackageItemInfo pii, int res, CharSequence nonLocalized) if (res != 0) { Resources r = getResources(pii); if (r != null) { - return r.getString(res); + try { + return r.getString(res); + } catch (Resources.NotFoundException e) { + } } } return null; diff --git a/cmds/screencap/screencap.cpp b/cmds/screencap/screencap.cpp index 6b2a0e2266b..0341315b78c 100644 --- a/cmds/screencap/screencap.cpp +++ b/cmds/screencap/screencap.cpp @@ -45,6 +45,7 @@ static void usage(const char* pname) "usage: %s [-hp] [-d display-id] [FILENAME]\n" " -h: this message\n" " -p: save the file as a png.\n" + " -j: save the file as a jpeg.\n" " -d: specify the display id to capture, default %d.\n" "If FILENAME ends with .png it will be saved as a png.\n" "If FILENAME is not given, the results will be printed to stdout.\n", @@ -92,13 +93,17 @@ int main(int argc, char** argv) const char* pname = argv[0]; bool png = false; + bool jpeg = false; int32_t displayId = DEFAULT_DISPLAY_ID; int c; - while ((c = getopt(argc, argv, "phd:")) != -1) { + while ((c = getopt(argc, argv, "pjhd:")) != -1) { switch (c) { case 'p': png = true; break; + case 'j': + jpeg = true; + break; case 'd': displayId = atoi(optarg); break; @@ -122,8 +127,14 @@ int main(int argc, char** argv) return 1; } const int len = strlen(fn); - if (len >= 4 && 0 == strcmp(fn+len-4, ".png")) { - png = true; + if (len >= 4) { + if (0 == strcmp(fn+len-4, ".png")) { + png = true; + } else if (0 == strcmp(fn+len-4, ".jpg")) { + jpeg = true; + } else if (len > 4 && 0 == strcmp(fn+len-5, ".jpeg")) { + jpeg = true; + } } } @@ -173,14 +184,15 @@ int main(int argc, char** argv) } if (base) { - if (png) { + if (png || jpeg) { const SkImageInfo info = SkImageInfo::Make(w, h, flinger2skia(f), kPremul_SkAlphaType); SkBitmap b; b.installPixels(info, const_cast(base), s*bytesPerPixel(f)); SkDynamicMemoryWStream stream; SkImageEncoder::EncodeStream(&stream, b, - SkImageEncoder::kPNG_Type, SkImageEncoder::kDefaultQuality); + png ? SkImageEncoder::kPNG_Type : SkImageEncoder::kJPEG_Type, + SkImageEncoder::kDefaultQuality); SkData* streamData = stream.copyToData(); write(fd, streamData->data(), streamData->size()); streamData->unref(); diff --git a/cmds/tm/Android.mk b/cmds/tm/Android.mk new file mode 100644 index 00000000000..34a41ddce9f --- /dev/null +++ b/cmds/tm/Android.mk @@ -0,0 +1,15 @@ +# Copyright 2015 The CyanogenMod Project +# +LOCAL_PATH:= $(call my-dir) + +include $(CLEAR_VARS) +LOCAL_SRC_FILES := $(call all-subdir-java-files) +LOCAL_MODULE := tm +include $(BUILD_JAVA_LIBRARY) + +include $(CLEAR_VARS) +LOCAL_MODULE := tm +LOCAL_SRC_FILES := tm +LOCAL_MODULE_CLASS := EXECUTABLES +LOCAL_MODULE_TAGS := optional +include $(BUILD_PREBUILT) diff --git a/cmds/tm/MODULE_LICENSE_APACHE2 b/cmds/tm/MODULE_LICENSE_APACHE2 new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cmds/tm/NOTICE b/cmds/tm/NOTICE new file mode 100644 index 00000000000..0820f6d9e21 --- /dev/null +++ b/cmds/tm/NOTICE @@ -0,0 +1,190 @@ + + Copyright (c) 2015, The CyanogenMod Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + + 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. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + diff --git a/cmds/tm/src/com/android/commands/tm/Tm.java b/cmds/tm/src/com/android/commands/tm/Tm.java new file mode 100644 index 00000000000..af1ac75e9e1 --- /dev/null +++ b/cmds/tm/src/com/android/commands/tm/Tm.java @@ -0,0 +1,201 @@ +/* +** +** Copyright 2015, The CyanogenMod 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 com.android.commands.tm; + +import android.content.pm.IPackageManager; +import android.content.pm.PackageInfo; +import android.content.pm.ParceledListSlice; +import android.content.pm.ThemeUtils; +import android.content.res.IThemeService; +import android.content.res.ThemeChangeRequest; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.UserHandle; +import android.util.AndroidException; +import com.android.internal.os.BaseCommand; + +import java.io.PrintStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class Tm extends BaseCommand { + private static final String SYSTEM_THEME = "system"; + + IThemeService mTs; + IPackageManager mPm; + + /** + * Command-line entry point. + * + * @param args The command-line arguments + */ + public static void main(String[] args) { + (new Tm()).run(args); + } + + public void onShowUsage(PrintStream out) { + List components = ThemeUtils.getAllComponents(); + StringBuilder sb = new StringBuilder(); + sb.append("usage: tm [subcommand] [options]\n"); + sb.append(" tm list\n"); + sb.append(" tm apply [-r] [-c [-c ] ...]\n"); + sb.append(" tm rebuild\n"); + sb.append(" tm process \n"); + sb.append("\n"); + sb.append("tm list: return a list of theme packages.\n"); + sb.append("\n"); + sb.append("tm apply: applies the components for the theme specified by PACKAGE_NAME.\n"); + sb.append(" -r: remove per app themes\n"); + sb.append(" [-c [-c ] ...]\n"); + sb.append(" if no components are specified all components will be applied.\n"); + sb.append(" Valid components are:\n"); + for (String component : components) { + sb.append(" "); + sb.append(component); + sb.append("\n"); + } + sb.append("\n"); + sb.append("tm rebuild: rebuilds the resource cache.\n"); + sb.append("\n"); + sb.append("tm process: processes the theme resources for the theme specified by " + + "PACKAGE_NAME.\n"); + + out.println(sb.toString()); + } + + public void onRun() throws Exception { + mTs = IThemeService.Stub.asInterface(ServiceManager.getService("themes")); + if (mTs == null) { + System.err.println(NO_SYSTEM_ERROR_CODE); + throw new AndroidException("Can't connect to theme service; is the system running?"); + } + + mPm = IPackageManager.Stub.asInterface(ServiceManager.getService("package")); + if (mPm == null) { + System.err.println(NO_SYSTEM_ERROR_CODE); + throw new AndroidException("Can't connect to package manager; is the system running?"); + } + + String op = nextArgRequired(); + + if (op.equals("list")) { + runListThemePackages(); + } else if (op.equals("apply")) { + runApplyTheme(); + } else if (op.equals("rebuild")) { + runRebuildResourceCache(); + } else if (op.equals("process")) { + runProcessTheme(); + } else { + showError("Error: unknown command '" + op + "'"); + return; + } + } + + private void runListThemePackages() throws Exception { + List packages = getInstalledPackages(mPm, 0, UserHandle.USER_OWNER); + + // there is always a "system" theme available + System.out.println("package:system [theme]"); + for (PackageInfo info : packages) { + if (info.isThemeApk || info.isLegacyIconPackApk) { + System.out.print("package:"); + System.out.print(info.packageName); + if (info.isThemeApk) { + System.out.println(" [theme]"); + } else { + System.out.println(" [icon pack]"); + } + } + } + } + + private void runApplyTheme() throws Exception { + String pkgName = nextArg(); + if (pkgName == null) { + System.err.println("Error: didn't specify theme package to apply"); + return; + } + if (!SYSTEM_THEME.equals(pkgName)) { + PackageInfo info = mPm.getPackageInfo(pkgName, 0, UserHandle.USER_OWNER); + if (info == null) { + System.err.println("Error: invalid package name"); + return; + } + if (!(info.isThemeApk || info.isLegacyIconPackApk)) { + System.err.println("Error: package is not a theme or icon pack"); + return; + } + } + + boolean removePerAppThemes = false; + + ThemeChangeRequest.Builder builder = new ThemeChangeRequest.Builder(); + String opt; + while ((opt=nextOption()) != null) { + if (opt.equals("-c")) { + builder.setComponent(nextArgRequired(), pkgName); + } else if (opt.equals("-r")) { + removePerAppThemes = true; + } + } + + // No components specified so let's just try and apply EVERYTHING! + Map componentMap = builder.build().getThemeComponentsMap(); + if (componentMap.size() == 0) { + List components = ThemeUtils.getAllComponents(); + for (String component : components) { + builder.setComponent(component, pkgName); + } + } + mTs.requestThemeChange(builder.build(), removePerAppThemes); + } + + private void runRebuildResourceCache() throws Exception { + mTs.rebuildResourceCache(); + } + + private void runProcessTheme() throws Exception { + String pkgName = nextArg(); + if (pkgName == null) { + System.err.println("Error: didn't specify theme package to apply"); + return; + } + PackageInfo info = mPm.getPackageInfo(pkgName, 0, UserHandle.USER_OWNER); + if (info == null) { + System.err.println("Error: invalid package name"); + return; + } + if (!info.isThemeApk) { + System.err.println("Error: package is not a theme"); + return; + } + + mTs.processThemeResources(pkgName); + } + + @SuppressWarnings("unchecked") + private List getInstalledPackages(IPackageManager pm, int flags, int userId) + throws RemoteException { + ParceledListSlice slice = pm.getInstalledPackages(flags, userId); + return slice.getList(); + } + +} diff --git a/cmds/tm/tm b/cmds/tm/tm new file mode 100755 index 00000000000..dc95b6ffc59 --- /dev/null +++ b/cmds/tm/tm @@ -0,0 +1,6 @@ +# Script to start "tm" on the device, which has a very rudimentary +# shell. +# +base=/system +export CLASSPATH=$base/framework/tm.jar +exec app_process $base/bin com.android.commands.tm.Tm "$@" diff --git a/core/core.iml b/core/core.iml new file mode 100644 index 00000000000..bce37bed605 --- /dev/null +++ b/core/core.iml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/java/android/accounts/ChooseTypeAndAccountActivity.java b/core/java/android/accounts/ChooseTypeAndAccountActivity.java index 82c2159d25f..c8c2dc11b2c 100644 --- a/core/java/android/accounts/ChooseTypeAndAccountActivity.java +++ b/core/java/android/accounts/ChooseTypeAndAccountActivity.java @@ -310,7 +310,7 @@ protected void onActivityResult(final int requestCode, final int resultCode, if (resultCode == RESULT_CANCELED) { // if canceling out of addAccount and the original state caused us to skip this, // finish this activity - if (mAccounts.isEmpty()) { + if (null == mAccounts || mAccounts.isEmpty()) { setResult(Activity.RESULT_CANCELED); finish(); } diff --git a/core/java/android/annotation/ChaosLab.java b/core/java/android/annotation/ChaosLab.java new file mode 100644 index 00000000000..7937e6b38fc --- /dev/null +++ b/core/java/android/annotation/ChaosLab.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2013 The ChameleonOS 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.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Annotation to be used when adding new features that will be + * tested out in the ChaOS Lab prior to be a permanent feature. + * + * Example: @ChaosLab(name="SomeSnazzyFeature", classification=Classification.NEW_CLASS) + * @hide + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface ChaosLab +{ + /** + * Name should be unique and consistent so it can be easily searched + */ + String name(); + + /** + * Various classifications for annotating what is being added or changed + */ + Classification classification(); + + /** + * Any additional information that can be useful. + */ + String notes() default ""; + + public static enum Classification + { + CHANGE_ACCESS, + CHANGE_CODE, + CHANGE_CODE_AND_ACCESS, + CHANGE_PARAMETER, + CHANGE_PARAMATER_AND_ACCESS, + CHANGE_BASE_CLASS, + NEW_CLASS, + NEW_FIELD, + NEW_METHOD; + } +} diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 9568897dca6..006d8defd39 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -28,11 +28,13 @@ import com.android.internal.app.WindowDecorActionBar; import com.android.internal.app.ToolbarActionBar; import com.android.internal.policy.PolicyManager; +import com.android.internal.widget.FloatingWindowView; import android.annotation.IntDef; import android.annotation.Nullable; import android.annotation.SystemApi; import android.app.admin.DevicePolicyManager; +import android.app.ActivityManager; import android.content.ComponentCallbacks2; import android.content.ComponentName; import android.content.ContentResolver; @@ -49,8 +51,10 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.database.Cursor; +import android.graphics.Color; import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.media.AudioManager; import android.media.session.MediaController; @@ -72,18 +76,22 @@ import android.util.EventLog; import android.util.Log; import android.util.PrintWriterPrinter; +import android.util.TypedValue; import android.util.Slog; import android.util.SparseArray; import android.view.ActionMode; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.ContextThemeWrapper; +import android.view.Gravity; +import android.view.IWindowManager; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.MotionEvent; +import android.view.ScaleGestureDetector; import android.view.View; import android.view.View.OnCreateContextMenuListener; import android.view.ViewGroup; @@ -94,6 +102,9 @@ import android.view.WindowManagerGlobal; import android.view.accessibility.AccessibilityEvent; import android.widget.AdapterView; +import android.widget.FrameLayout; + +import com.android.internal.util.aicp.ColorUtils; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -101,6 +112,7 @@ import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; /** * An activity is a single, focused thing that the user can do. Almost all @@ -680,6 +692,9 @@ public class Activity extends ContextThemeWrapper private static final String SAVED_DIALOG_KEY_PREFIX = "android:dialog_"; private static final String SAVED_DIALOG_ARGS_KEY_PREFIX = "android:dialog_args_"; + private static final String SAVED_WINDOW_LAYOUT_IDS_KEY = "android:savedWindowLayoutIds"; + private static final String SAVED_WINDOW_LAYOUT_TAG = "android:savedWindowLayout"; + private static class ManagedDialog { Dialog mDialog; Bundle mArgs; @@ -733,6 +748,7 @@ static final class NonConfigurationInstances { /*package*/ boolean mVisibleFromServer = false; /*package*/ boolean mVisibleFromClient = true; /*package*/ ActionBar mActionBar = null; + /*package*/ WindowDecorActionBar mDecorActionBar = null; private boolean mEnableDefaultActionBarUp; private VoiceInteractor mVoiceInteractor; @@ -797,6 +813,57 @@ private static final class ManagedCursor { SharedElementCallback mEnterTransitionListener = SharedElementCallback.NULL_CALLBACK; SharedElementCallback mExitTransitionListener = SharedElementCallback.NULL_CALLBACK; + /** Return the activity handler instance. */ + public Handler getHandler() { + return mHandler; + } + + private Rect mOriginalBounds; + private boolean mIsSplitView; + + private final int UNKNOWN = -10000; + private final int SNAP_NONE = 0; + private final int SNAP_LEFT = 1; + private final int SNAP_TOP = 2; + private final int SNAP_RIGHT = 3; + private final int SNAP_BOTTOM = 4; + private final int MOVE_MAX_RANGE = 10; + + private Runnable mRunnable; + private Runnable mLayoutRunnable; + private int mRange = 100; + private int mSensitivity = 50; + private int mSnap = SNAP_NONE; + private int[] mSnapParam = new int[3]; // w,h,g + private int[] mOldParam = new int[2]; // w,h + private boolean mSnapped; + private int[] mOldLayout; + private int[] mLastLayout; + private boolean mTimeoutRunning; + private boolean mTimeoutDone; + private boolean mRestorePosition; + private boolean mChangedPreviousRange; + private float[] mPreviousRange = new float[2]; + + private Float screenX ; + private Float screenY ; + private Float viewX ; + private Float viewY ; + private Float leftFromScreen ; + private Float topFromScreen ; + private int mCurrentScreenHeight; + private int mCurrentScreenWidth; + private int mPreviousOrientation; + private int mAppMinimumWidth; + private int mAppMinimumHeight; + private int mAppFloatViewWidth; + private int mAppFloatViewHeight; + private boolean mChangedFlags = false; + private boolean isAlreadyAttachToWindow = false; + public boolean mIsFullscreenApp = false; + private ScaleGestureDetector mScaleGestureDetector; + private FloatingWindowView mFloatingWindowView; + /** Return the intent that started this activity. */ public Intent getIntent() { return mIntent; @@ -939,6 +1006,9 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { mVoiceInteractor.attachActivity(this); } mCalled = true; + mPreviousOrientation = getResources().getConfiguration().orientation; + mScaleGestureDetector = new ScaleGestureDetector(getApplicationContext(), mScaleGestureListener); + mScaleGestureDetector.setQuickScaleEnabled(false); } /** @@ -977,6 +1047,7 @@ public void onCreate(@Nullable Bundle savedInstanceState, final void performRestoreInstanceState(Bundle savedInstanceState) { onRestoreInstanceState(savedInstanceState); restoreManagedDialogs(savedInstanceState); + restoreManagedWindowLayout(savedInstanceState); } /** @@ -1085,6 +1156,54 @@ private void restoreManagedDialogs(Bundle savedInstanceState) { } } + private void restoreManagedWindowLayout(Bundle savedInstanceState) { + final Bundle b = savedInstanceState.getBundle(SAVED_WINDOW_LAYOUT_TAG); + if (b == null) { + return; + } + + if (mWindow == null) { + return; + } + + if (!mWindow.mIsFloatingWindow) { + return; + } + + final int[] ids = b.getIntArray(SAVED_WINDOW_LAYOUT_IDS_KEY); + final int x = ids[0]; + final int y = ids[1]; + final int width = ids[2]; + final int height = ids[3]; + mSnap = ids[4]; + WindowManager.LayoutParams params = mWindow.getAttributes(); + params.x = x; + params.y = y; + switch (mSnap) { + case SNAP_LEFT: + params.width = (mCurrentScreenWidth / 2); + params.height = ViewGroup.LayoutParams.MATCH_PARENT; + break; + case SNAP_RIGHT: + params.width = (mCurrentScreenWidth / 2); + params.height = ViewGroup.LayoutParams.MATCH_PARENT; + break; + case SNAP_TOP: + params.width = ViewGroup.LayoutParams.MATCH_PARENT; + params.height = (mCurrentScreenHeight / 2); + break; + case SNAP_BOTTOM: + params.width = ViewGroup.LayoutParams.MATCH_PARENT; + params.height = (mCurrentScreenHeight / 2); + break; + case SNAP_NONE: + params.width = width; + params.height = height; + break; + } + mWindow.setAttributes(params); + } + private Dialog createDialog(Integer dialogId, Bundle state, Bundle args) { final Dialog dialog = onCreateDialog(dialogId, args); if (dialog == null) { @@ -1156,6 +1275,7 @@ public void onPostCreate(@Nullable Bundle savedInstanceState, */ protected void onStart() { if (DEBUG_LIFECYCLE) Slog.v(TAG, "onStart " + this); + setupFloatingActionBar(false); mCalled = true; if (!mLoadersStarted) { @@ -1171,6 +1291,87 @@ protected void onStart() { getApplication().dispatchActivityStarted(this); } + private void setupFloatingActionBar(boolean reload) { + if (mWindow == null) { + return; + } + + if (!mWindow.mIsFloatingWindow) { + return; + } + + if (mWindow.peekDecorView() == null) { + return; + } + + mWindow.getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_IMMERSIVE); + + FrameLayout decorFloatingView = (FrameLayout) mWindow.peekDecorView().getRootView(); + if (decorFloatingView == null) { + return; + } + if (!reload) { + decorFloatingView.setFitsSystemWindows(true); + mFloatingWindowView = new FloatingWindowView(this, getActionBarHeight(true)); + decorFloatingView.addView(mFloatingWindowView, -1, FloatingWindowView.getParams()); + decorFloatingView.setTagInternal(android.R.id.extractArea, mFloatingWindowView); + changeTitleBarColor(); + } else { + mFloatingWindowView = (FloatingWindowView) decorFloatingView.getTag(android.R.id.extractArea); + changeTitleBarColor(); + decorFloatingView.bringChildToFront(mFloatingWindowView); + } + } + + private void changeFloatingWindowColor(int bg_color, int ic_color) { + mFloatingWindowView.setFloatingBackgroundColor(bg_color); + mFloatingWindowView.setFloatingColorFilter(ic_color); + } + + + private void changeTitleBarColor() { + if (mFloatingWindowView != null) { + try { + final PackageManager pm = getPackageManager(); + // Getting current task packagename + final ActivityManager am = (ActivityManager) getSystemService(Activity.ACTIVITY_SERVICE); + List tasks = am.getRunningTasks(1); + ActivityManager.RunningTaskInfo task = tasks.get(0); // current task + ComponentName rootActivity = task.baseActivity; + String appPackageName = rootActivity.getPackageName(); + // Get theme from packagename + final Resources res = pm.getResourcesForApplication(appPackageName); + final int[] attrs = new int[] { + res.getIdentifier("colorPrimary", "attr", appPackageName), + android.R.attr.colorPrimary + }; + + final Resources.Theme theme = res.newTheme(); + final ComponentName cn = pm.getLaunchIntentForPackage(appPackageName).getComponent(); + theme.applyStyle(pm.getActivityInfo(cn, 0).theme, false); + // Obtain the colorPrimary color from the attrs + TypedArray a = theme.obtainStyledAttributes(attrs); + // Do something with the color + final int colorPrimary = a.getColor(0, a.getColor(1, Color.WHITE)); + // Make sure you recycle the TypedArray + a.recycle(); + a = null; + int iconTint; + + if (ColorUtils.isBrightColor(colorPrimary)) { + iconTint = Color.BLACK; + } else { + iconTint = Color.WHITE; + } + changeFloatingWindowColor(colorPrimary, iconTint); + } catch (final NameNotFoundException e) { + e.printStackTrace(); + } + } + } + /** * Called after {@link #onStop} when the current activity is being * re-displayed to the user (the user has navigated back to it). It will @@ -1238,6 +1439,7 @@ protected void onPostResume() { final Window win = getWindow(); if (win != null) win.makeActive(); if (mActionBar != null) mActionBar.setShowHideAnimationEnabled(true); + setupFloatingActionBar(true); mCalled = true; } @@ -1298,6 +1500,7 @@ final void performSaveInstanceState(Bundle outState) { onSaveInstanceState(outState); saveManagedDialogs(outState); mActivityTransitionState.saveState(outState); + saveManagedWindowLayout(outState); if (DEBUG_LIFECYCLE) Slog.v(TAG, "onSaveInstanceState " + this + ": " + outState); } @@ -1424,6 +1627,27 @@ private void saveManagedDialogs(Bundle outState) { outState.putBundle(SAVED_DIALOGS_TAG, dialogState); } + private void saveManagedWindowLayout(Bundle outState) { + if (mWindow == null) { + return; + } + + if (!mWindow.mIsFloatingWindow) { + return; + } + + Bundle windowLayoutState = new Bundle(); + + int[] ids = new int[5]; + WindowManager.LayoutParams params = mWindow.getAttributes(); + ids[0] = params.x; + ids[1] = params.y; + ids[2] = params.width; + ids[3] = params.height; + ids[4] = mSnap; + windowLayoutState.putIntArray(SAVED_WINDOW_LAYOUT_IDS_KEY, ids); + outState.putBundle(SAVED_WINDOW_LAYOUT_TAG, windowLayoutState); + } /** * Called as part of the activity lifecycle when an activity is going into @@ -1604,8 +1828,8 @@ protected void onStop() { */ protected void onDestroy() { if (DEBUG_LIFECYCLE) Slog.v(TAG, "onDestroy " + this); + sendAppEndBroadcast(); mCalled = true; - // dismiss any dialogs we are managing. if (mManagedDialogs != null) { final int numDialogs = mManagedDialogs.size(); @@ -1686,6 +1910,51 @@ public void onConfigurationChanged(Configuration newConfig) { if (mWindow != null) { // Pass the configuration changed event to the window mWindow.onConfigurationChanged(newConfig); + if (mWindow.mIsFloatingWindow) { + refreshAppLayoutSize(); + Configuration config = getResources().getConfiguration(); + if (config.orientation != mPreviousOrientation) { + mWindow.setGravity(Gravity.LEFT | Gravity.TOP); + if (!isUnSnap()) { + requestChangingFlagsLayout(); + } + WindowManager.LayoutParams params = mWindow.getAttributes(); + switch (mSnap) { + case SNAP_LEFT: + params.width = (mCurrentScreenWidth / 2); + params.height = ViewGroup.LayoutParams.MATCH_PARENT; + params.x = 0; + params.y = 0; + break; + case SNAP_RIGHT: + params.width = (mCurrentScreenWidth / 2); + params.height = ViewGroup.LayoutParams.MATCH_PARENT; + params.x = (mCurrentScreenWidth / 2); + params.y = 0; + break; + case SNAP_TOP: + params.width = ViewGroup.LayoutParams.MATCH_PARENT; + params.height = (mCurrentScreenHeight / 2); + params.x = 0; + params.y = 0; + break; + case SNAP_BOTTOM: + params.width = ViewGroup.LayoutParams.MATCH_PARENT; + params.height = (mCurrentScreenHeight / 2); + params.x = 0; + params.y = (mCurrentScreenHeight / 2); + break; + case SNAP_NONE: + int width = params.width; + int height = params.height; + params.width = height; + params.height = width; + break; + } + mWindow.setAttributes(params); + mPreviousOrientation = config.orientation; + } + } } if (mActionBar != null) { @@ -2736,8 +3005,46 @@ public boolean dispatchKeyShortcutEvent(KeyEvent event) { * @return boolean Return true if this event was consumed. */ public boolean dispatchTouchEvent(MotionEvent ev) { - if (ev.getAction() == MotionEvent.ACTION_DOWN) { - onUserInteraction(); + if (mWindow.mIsFloatingWindow) { + if (isUnSnap()) { + mScaleGestureDetector.onTouchEvent(ev); + } + int actionBarHeight = getActionBarHeight(false); + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: + setTouchViewDown(ev.getX(), ev.getY()); + onUserInteraction(); + updateFocusApp(); + if (viewY < actionBarHeight) { + if (!mChangedPreviousRange) { + setPreviousTouchRange(ev.getRawX(), ev.getRawY()); + mChangedPreviousRange = true; + } + } + break; + case MotionEvent.ACTION_MOVE: + if (viewY < actionBarHeight) { + changeFlagsLayoutParams(); + setTouchViewMove(ev.getRawX(), ev.getRawY()); + if (mRestorePosition && moveRangeAboveLimit(ev)) { + restoreOldPosition(); + } + showSnap((int) ev.getRawX(), (int) ev.getRawY()); + } + break; + case MotionEvent.ACTION_UP: + if (viewY < actionBarHeight) { + mChangedFlags = false; + finishSnap(isValidSnap() && mTimeoutDone); + discardTimeout(); + mChangedPreviousRange = false; + } + break; + } + } else { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + onUserInteraction(); + } } if (getWindow().superDispatchTouchEvent(ev)) { return true; @@ -2745,6 +3052,502 @@ public boolean dispatchTouchEvent(MotionEvent ev) { return onTouchEvent(ev); } + /** + * @hide + */ + public void setTouchViewDown(float x , float y) { + viewX = x; + viewY = y; + } + + /** + * @hide + */ + public void setPreviousTouchRange(float x , float y) { + mPreviousRange[0] = x; + mPreviousRange[1] = y; + } + + /** + * @hide + */ + public void setTouchViewMove(float x , float y) { + screenX = x; + screenY = y; + leftFromScreen = (screenX - viewX); + topFromScreen = (screenY - viewY); + initLayoutParams(leftFromScreen, topFromScreen); + } + + /** + * @hide + */ + public void setChangedPreviousRange(boolean needed) { + mChangedPreviousRange = needed; + } + + /** + * @hide + */ + public boolean getChangedPreviousRange() { + return mChangedPreviousRange; + } + + private int getActionBarHeight(boolean smaller) { + ActionBar actionBar = getActionBar(); + int actionBarHeight = (actionBar != null) ? actionBar.getHeight() + : getAppDimensionPixel(smaller); + return (smaller ? getAppDimensionPixel(smaller) : actionBarHeight); + } + + /** + * @hide + */ + public void updateFocusApp() { + IWindowManager wm = (IWindowManager) WindowManagerGlobal.getWindowManagerService(); + try { + wm.notifyFloatActivityTouched(mToken, false); + } catch (RemoteException e) { + Log.e(TAG, "Cannot notify activity touched", e); + } + } + + private void initLayoutParams(float x , float y) { + WindowManager.LayoutParams param = mWindow.getAttributes(); + param.x = (int) x; + param.y = (int) y; + mWindow.setAttributes(param); + } + + /** + * @hide + */ + public void changeFlagsLayoutParams() { + mWindow.setGravity(Gravity.LEFT | Gravity.TOP); + if (!mChangedFlags) { + mChangedFlags = true; + requestChangingFlagsLayout(); + } + } + + private void requestChangingFlagsLayout() { + mWindow.setCloseOnTouchOutside(false); + mWindow.addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL); + mWindow.addFlags(WindowManager.LayoutParams.FLAG_SPLIT_TOUCH); + if (ActivityManager.isHighEndGfx()) { + mWindow.addFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED); + } + } + + /** + * @hide + */ + public void setChangedFlags(boolean changed) { + mChangedFlags = changed; + } + + /** + * @hide + */ + public boolean moveRangeAboveLimit(MotionEvent event) { + final float x = event.getRawX(); + final float y = event.getRawY(); + + boolean returnValue = false; + if (Math.abs(mPreviousRange[0] - x) > MOVE_MAX_RANGE) { + returnValue = true; + } else if (Math.abs(mPreviousRange[1] - y) > MOVE_MAX_RANGE) { + returnValue = true; + } + return returnValue; + } + + /** + * @hide + */ + public void showSnap(int x, int y) { + initSnappable(x, y); + calculateSnap(); + if (isValidSnap()) { + setupTimeout(); + } + } + + /** + * @hide + */ + public void finishSnap(boolean apply) { + if (apply) { + if (saveOldPosition()) { + mRestorePosition = true; + } + WindowManager.LayoutParams lpp = mWindow.getAttributes(); + lpp.width = mSnapParam[0]; + lpp.height = mSnapParam[1]; + int gravity = mSnapParam[2]; + lpp.x = (gravity == Gravity.RIGHT) ? (mCurrentScreenWidth / 2) : 0; + lpp.y = (gravity == Gravity.BOTTOM) ? (mCurrentScreenHeight / 2) : 0; + mWindow.setAttributes(lpp); + } else { + mSnap = SNAP_NONE; + } + } + + /** + * @hide + */ + public boolean isValidSnap() { + return (mSnapParam[0] != UNKNOWN) && + (mSnapParam[1] != UNKNOWN) && + (mSnapParam[2] != UNKNOWN); + } + + private boolean saveOldPosition() { + if (mRestorePosition) return true; + if (mSnapped) { + return (isUnSnap() || mTimeoutRunning); + } + mSnapped = true; + final WindowManager.LayoutParams params = mWindow.getAttributes(); + int[] layout = { params.x, params.y, params.width, params.height }; + mOldLayout = layout; + return true; + } + + private boolean isUnSnap() { + return (mSnap == SNAP_NONE); + } + + /** + * @hide + */ + public boolean restoreOldPosition() { + if (!mSnapped) return false; + restoreOldPositionWithoutRefresh(); + return true; + } + + private void restoreOldPositionWithoutRefresh() { + if (!mSnapped) return; + WindowManager.LayoutParams params = mWindow.getAttributes(); + params.x = mOldLayout[0]; + params.y = mOldLayout[1]; + params.width = mOldLayout[2]; + params.height = mOldLayout[3]; + mWindow.setAttributes(params); + mSnapped = false; + mRestorePosition = false; + } + + /** + * @hide + */ + public boolean getRestorePosition() { + return mRestorePosition; + } + + private boolean initSnappable(int x, int y) { + if ((Math.abs(mOldParam[0] - x) > mSensitivity) || + (Math.abs(mOldParam[1] - y) > mSensitivity)) { + mOldParam[0] = x; + mOldParam[1] = y; + return false; + } + mOldParam[0] = x; + mOldParam[1] = y; + + if (x < mRange) { + mSnap = SNAP_LEFT; + } else if (x > (mCurrentScreenWidth - mRange)) { + mSnap = SNAP_RIGHT; + } else if (y < mRange) { + mSnap = SNAP_TOP; + } else if (y > (mCurrentScreenHeight - mRange)) { + mSnap = SNAP_BOTTOM; + } else { + mSnap = SNAP_NONE; + return false; + } + return true; + } + + private void refreshAppLayoutSize() { + final IWindowManager wm = (IWindowManager) WindowManagerGlobal.getWindowManagerService(); + + try { + Rect windowFloatViewBounds = wm.getFloatViewRect(); + mAppFloatViewWidth = (windowFloatViewBounds.right - windowFloatViewBounds.left); + mAppFloatViewHeight = (windowFloatViewBounds.bottom - windowFloatViewBounds.top); + + Rect windowCurrentScreenBounds = wm.getAppFullscreenViewRect(); + mCurrentScreenWidth = (windowCurrentScreenBounds.right - windowCurrentScreenBounds.left); + mCurrentScreenHeight = (windowCurrentScreenBounds.bottom - windowCurrentScreenBounds.top); + + Rect windowMinimumBounds = wm.getAppMinimumViewRect(); + mAppMinimumWidth = (windowMinimumBounds.right - windowMinimumBounds.left); + mAppMinimumHeight = (windowMinimumBounds.bottom - windowMinimumBounds.top); + + } catch (RemoteException e) { + Log.e(TAG, "Could not perform get size view layout", e); + } + } + + private void refreshAppFloatViewSize() { + final IWindowManager wm = (IWindowManager) WindowManagerGlobal.getWindowManagerService(); + + try { + Rect windowBounds = wm.getFloatViewRect(); + mAppFloatViewWidth = (windowBounds.right - windowBounds.left); + mAppFloatViewHeight = (windowBounds.bottom - windowBounds.top); + } catch (RemoteException e) { + Log.e(TAG, "Could not perform float view layout", e); + } + } + + private void refreshCurrentScreenSize() { + final IWindowManager wm = (IWindowManager) WindowManagerGlobal.getWindowManagerService(); + + try { + Rect windowBounds = wm.getAppFullscreenViewRect(); + mCurrentScreenWidth = (windowBounds.right - windowBounds.left); + mCurrentScreenHeight = (windowBounds.bottom - windowBounds.top); + } catch (RemoteException e) { + Log.e(TAG, "Could not perform get app fullscreen view layout", e); + } + } + + /** + * @hide + */ + public void forceSnap(int side) { + if (side == SNAP_NONE) { + restoreOldPosition(); + return; + } + if (mSnapped) { + restoreOldPositionWithoutRefresh(); + } + mSnap = side; + calculateSnap(); + finishSnap(true); + } + + private void calculateSnap() { + switch (mSnap) { + case SNAP_LEFT: + mSnapParam[0] = (mCurrentScreenWidth / 2); + mSnapParam[1] = ViewGroup.LayoutParams.MATCH_PARENT; + mSnapParam[2] = Gravity.TOP | Gravity.LEFT; + break; + case SNAP_RIGHT: + mSnapParam[0] = (mCurrentScreenWidth / 2); + mSnapParam[1] = ViewGroup.LayoutParams.MATCH_PARENT; + mSnapParam[2] = Gravity.RIGHT; + break; + case SNAP_TOP: + mSnapParam[0] = ViewGroup.LayoutParams.MATCH_PARENT; + mSnapParam[1] = (mCurrentScreenHeight / 2); + mSnapParam[2] = Gravity.TOP; + break; + case SNAP_BOTTOM: + mSnapParam[0] = ViewGroup.LayoutParams.MATCH_PARENT; + mSnapParam[1] = (mCurrentScreenHeight / 2); + mSnapParam[2] = Gravity.BOTTOM; + break; + case SNAP_NONE: + mSnapParam[0] = UNKNOWN; + mSnapParam[1] = UNKNOWN; + mSnapParam[2] = UNKNOWN; + break; + } + } + + /** + * @hide + */ + public boolean getTimeoutDone() { + return mTimeoutDone; + } + + /** + * @hide + */ + public void discardTimeout() { + mTimeoutDone = false; + mTimeoutRunning = false; + mHandler.removeCallbacks(mRunnable); + } + + private void setupTimeout() { + if (mTimeoutRunning) return; + if (mRunnable == null) { + mRunnable = new Runnable() { + @Override + public void run() { + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + mTimeoutRunning = false; + mTimeoutDone = true; + } + }, 250); + } + }; + } + mTimeoutRunning = true; + mHandler.postDelayed(mRunnable, 750); + } + + private int getAppDimensionPixel(boolean smaller) { + float scale = getResources().getDisplayMetrics().density; + int sized = smaller ? 32 : 48; + int pixel = (int) (sized * scale + 0.5f); + return pixel; + } + + private void refreshAppMinimumSize() { + final IWindowManager wm = (IWindowManager) WindowManagerGlobal.getWindowManagerService(); + + try { + Rect windowBounds = wm.getAppMinimumViewRect(); + mAppMinimumWidth = (windowBounds.right - windowBounds.left); + mAppMinimumHeight = (windowBounds.bottom - windowBounds.top); + } catch (RemoteException e) { + Log.e(TAG, "Could not perform get app minimum view layout", e); + } + } + + /** + * @hide + */ + public void sendAppLaunchBroadcast() { + Intent appIntent = new Intent(Intent.ACTION_ACTIVITY_LAUNCH_DETECTOR); + appIntent.putExtra("packagename", getPackageName()); + appIntent.putExtra("packagetoken", mToken); + appIntent.addFlags( + Intent.FLAG_RECEIVER_REGISTERED_ONLY | Intent.FLAG_RECEIVER_FOREGROUND); + sendBroadcastAsUser(appIntent, UserHandle.CURRENT_OR_SELF); + moveTaskToBack(true); + } + + private void sendAppEndBroadcast() { + Intent endIntent = new Intent(Intent.ACTION_ACTIVITY_END_DETECTOR); + endIntent.putExtra("packagename", getPackageName()); + endIntent.addFlags( + Intent.FLAG_RECEIVER_REGISTERED_ONLY | Intent.FLAG_RECEIVER_FOREGROUND); + sendBroadcastAsUser(endIntent, UserHandle.CURRENT_OR_SELF); + } + + private void setProgressLayoutApp() { + if (mLayoutRunnable == null) { + mLayoutRunnable = new Runnable() { + @Override + public void run() { + setCurrentLayoutApp(); + } + }; + } + mHandler.postDelayed(mLayoutRunnable, 500); + } + + private void discardLayoutProgress() { + mHandler.removeCallbacks(mLayoutRunnable); + } + + private void setCurrentLayoutApp() { + if (isUnSnap()) { + final WindowManager.LayoutParams params = mWindow.getAttributes(); + final int width = params.width; + final int height = params.height; + if (width >= mCurrentScreenWidth) { + params.width = ViewGroup.LayoutParams.MATCH_PARENT; + } else if (width <= mAppMinimumWidth) { + params.width = mAppMinimumWidth; + } + if (height >= mCurrentScreenHeight) { + params.height = ViewGroup.LayoutParams.MATCH_PARENT; + } else if (height <= mAppMinimumHeight) { + params.height = mAppMinimumHeight; + } + mWindow.setAttributes(params); + } + mHandler.removeCallbacks(mLayoutRunnable); + } + + /** + * @hide + */ + public void setFullscreenApp() { + if (mIsFullscreenApp) { + return; + } + WindowManager.LayoutParams params = mWindow.getAttributes(); + int[] layout = { params.x, params.y, params.width, params.height }; + mLastLayout = layout; + params.width = ViewGroup.LayoutParams.MATCH_PARENT; + params.height = ViewGroup.LayoutParams.MATCH_PARENT; + mWindow.getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_IMMERSIVE); + mWindow.setAttributes(params); + mIsFullscreenApp = true; + } + + /** + * @hide + */ + public void restorePreviousLayoutApp() { + if (!mIsFullscreenApp) { + return; + } + WindowManager.LayoutParams params = mWindow.getAttributes(); + params.x = mLastLayout[0]; + params.y = mLastLayout[1]; + params.width = mLastLayout[2]; + params.height = mLastLayout[3]; + mWindow.getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_IMMERSIVE); + mWindow.setAttributes(params); + mIsFullscreenApp = false; + } + + private final ScaleGestureDetector.OnScaleGestureListener mScaleGestureListener + = new ScaleGestureDetector.SimpleOnScaleGestureListener() { + + private float lastSpanX; + private float lastSpanY; + private WindowManager.LayoutParams params; + + @Override + public boolean onScaleBegin(ScaleGestureDetector scaleGestureDetector) { + discardLayoutProgress(); + lastSpanX = scaleGestureDetector.getCurrentSpanX(); + lastSpanY = scaleGestureDetector.getCurrentSpanY(); + params = mWindow.getAttributes(); + return true; + } + + @Override + public boolean onScale(ScaleGestureDetector scaleGestureDetector) { + float spanX = scaleGestureDetector.getCurrentSpanX(); + float spanY = scaleGestureDetector.getCurrentSpanY(); + float newWidth = spanX / lastSpanX * (float) params.width; + float newHeight = spanY / lastSpanY * (float) params.height; + params.width = (int) newWidth; + params.height = (int) newHeight; + mWindow.setAttributes(params); + lastSpanX = spanX; + lastSpanY = spanY; + return true; + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + setProgressLayoutApp(); + } + }; + /** * Called to process trackball events. You can override this to * intercept all trackball events before they are dispatched to the @@ -4651,6 +5454,10 @@ private void finish(boolean finishTask) { } } + public void finishFloating() { + mMainThread.performFinishFloating(); + } + /** * Call this when your activity is done and should be closed. The * ActivityResult is propagated back to whoever launched you via @@ -5929,7 +6736,10 @@ final void attach(Context context, ActivityThread aThread, mFragments.attachActivity(this, mContainer, null); - mWindow = PolicyManager.makeNewWindow(this); + if (makeNewWindow(context, intent, info)) { + parent = null; + } + mWindow.setCallback(this); mWindow.setOnWindowDismissedCallback(this); mWindow.getLayoutInflater().setPrivateFactory(this); @@ -5974,6 +6784,64 @@ final void attach(Context context, ActivityThread aThread, mCurrentConfig = config; } + private boolean makeNewWindow(Context context, Intent intent, ActivityInfo info) { + boolean floating = (intent.getFlags() & Intent.FLAG_FLOATING_WINDOW) == Intent.FLAG_FLOATING_WINDOW; + if (intent != null && floating) { + + TypedArray styleArray = context.obtainStyledAttributes(info.theme, com.android.internal.R.styleable.Window); + TypedValue backgroundValue = styleArray.peekValue(com.android.internal.R.styleable.Window_windowBackground); + + // Apps that have no title don't need no title bar + TypedValue outValue = new TypedValue(); + boolean result = styleArray.getValue(com.android.internal.R.styleable.Window_windowNoTitle, outValue); + + if (backgroundValue != null && backgroundValue.toString().contains("light")) { + context.getTheme().applyStyle(com.android.internal.R.style.Theme_DeviceDefault_FloatingWindowLight, true); + } else { + context.getTheme().applyStyle(com.android.internal.R.style.Theme_DeviceDefault_FloatingWindow, true); + } + + // Create our new window + mWindow = PolicyManager.makeNewWindow(this); + mWindow.mIsFloatingWindow = true; + if (!isAlreadyAttachToWindow) { + isAlreadyAttachToWindow = true; + mWindow.setCloseOnTouchOutsideIfNotSet(true); + mWindow.setGravity(Gravity.CENTER); + // Scale it + scaleFloatingWindow(); + } + + WindowManager.LayoutParams params = mWindow.getAttributes(); + params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; + if (android.os.Process.myUid() == android.os.Process.SYSTEM_UID) { + mWindow.setFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND, + WindowManager.LayoutParams.FLAG_DIM_BEHIND); + params.alpha = 1f; + params.dimAmount = 0.25f; + } + mWindow.setAttributes(params); + + refreshAppLayoutSize(); + return true; + } else { + mWindow = PolicyManager.makeNewWindow(this); + return false; + } + } + + private void scaleFloatingWindow() { + final IWindowManager wm = (IWindowManager) WindowManagerGlobal.getWindowManagerService(); + + try { + Rect windowBounds = wm.getFloatViewRect(); + mWindow.setLayout(windowBounds.right - windowBounds.left, + windowBounds.bottom - windowBounds.top); + } catch (RemoteException e) { + Log.e(TAG, "Could not perform float view layout", e); + } + } + /** @hide */ public final IBinder getActivityToken() { return mParent != null ? mParent.getActivityToken() : mToken; diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index 7a636dbdb59..831ba9f4f18 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -38,6 +38,7 @@ import android.content.pm.IPackageDataObserver; import android.content.pm.PackageManager; import android.content.pm.UserInfo; +import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Color; @@ -56,6 +57,7 @@ import android.util.DisplayMetrics; import android.util.Size; import android.util.Slog; + import org.xmlpull.v1.XmlSerializer; import java.io.FileDescriptor; @@ -256,6 +258,9 @@ public class ActivityManager { /** @hide User operation call: given user id is the current user, can't be stopped. */ public static final int USER_OP_IS_CURRENT = -2; + /** @hide Process does not exist. */ + public static final int PROCESS_STATE_NONEXISTENT = -1; + /** @hide Process is a persistent system process. */ public static final int PROCESS_STATE_PERSISTENT = 0; @@ -308,6 +313,15 @@ public class ActivityManager { Point mAppTaskThumbnailSize; + // Memoize these to reduce SystemProperty lookups + private static boolean _isHighEndGfx, _isLowRamDeviceStatic, _testHarness; + private static int _vmHeapClass, _vmHeapGrowth; + private static boolean _isLowRamDeviceStaticInit = false; + private static boolean _highEndGfxInit = false; + private static boolean _staticGetLargeMemoryClassInit = false; + private static boolean _staticGetMemoryClassInit = false; + private static boolean _isRunningInTestHarnessInit = false; + /*package*/ ActivityManager(Context context, Handler handler) { mContext = context; mHandler = handler; @@ -422,18 +436,23 @@ public void setPackageAskScreenCompat(String packageName, boolean ask) { public int getMemoryClass() { return staticGetMemoryClass(); } - + /** @hide */ static public int staticGetMemoryClass() { // Really brain dead right now -- just take this from the configured // vm heap size, and assume it is in megabytes and thus ends with "m". - String vmHeapSize = SystemProperties.get("dalvik.vm.heapgrowthlimit", ""); - if (vmHeapSize != null && !"".equals(vmHeapSize)) { - return Integer.parseInt(vmHeapSize.substring(0, vmHeapSize.length()-1)); + if (!_staticGetMemoryClassInit) { + String vmHeapSize = SystemProperties.get("dalvik.vm.heapgrowthlimit", ""); + if (vmHeapSize != null && !"".equals(vmHeapSize)) { + _vmHeapGrowth = Integer.parseInt(vmHeapSize.substring(0, vmHeapSize.length()-1)); + _staticGetMemoryClassInit = true; + } + } else { + return _vmHeapGrowth; } return staticGetLargeMemoryClass(); } - + /** * Return the approximate per-application memory class of the current * device when an application is running with a large heap. This is the @@ -450,13 +469,17 @@ static public int staticGetMemoryClass() { public int getLargeMemoryClass() { return staticGetLargeMemoryClass(); } - + /** @hide */ static public int staticGetLargeMemoryClass() { // Really brain dead right now -- just take this from the configured // vm heap size, and assume it is in megabytes and thus ends with "m". - String vmHeapSize = SystemProperties.get("dalvik.vm.heapsize", "16m"); - return Integer.parseInt(vmHeapSize.substring(0, vmHeapSize.length() - 1)); + if (!_staticGetLargeMemoryClassInit) { + String vmHeapSize = SystemProperties.get("dalvik.vm.heapsize", "16m"); + _vmHeapClass = Integer.parseInt(vmHeapSize.substring(0, vmHeapSize.length()-1)); + _staticGetLargeMemoryClassInit = true; + } + return _vmHeapClass; } /** @@ -472,7 +495,11 @@ public boolean isLowRamDevice() { /** @hide */ public static boolean isLowRamDeviceStatic() { - return "true".equals(SystemProperties.get("ro.config.low_ram", "false")); + if (!_isLowRamDeviceStaticInit) { + _isLowRamDeviceStatic = "true".equals(SystemProperties.get("ro.config.low_ram", "false")); + _isLowRamDeviceStaticInit = true; + } + return _isLowRamDeviceStatic; } /** @@ -482,8 +509,20 @@ public static boolean isLowRamDeviceStatic() { * @hide */ static public boolean isHighEndGfx() { - return !isLowRamDeviceStatic() && - !Resources.getSystem().getBoolean(com.android.internal.R.bool.config_avoidGfxAccel); + if (!_highEndGfxInit) { + _isHighEndGfx = !isLowRamDeviceStatic() && + !Resources.getSystem().getBoolean(com.android.internal.R.bool.config_avoidGfxAccel) || + isForcedHighEndGfx(); + _highEndGfxInit = true; + } + return _isHighEndGfx; + } + + /** + * @hide + */ + public static boolean isForcedHighEndGfx() { + return SystemProperties.getBoolean("persist.sys.force_highendgfx", false); } /** @@ -769,7 +808,7 @@ public static class RecentTaskInfo implements Parcelable { * The true identifier of this task, valid even if it is not running. */ public int persistentId; - + /** * The original Intent used to launch the task. You can use this * Intent to re-launch the task (if it is no longer running) or bring @@ -924,6 +963,11 @@ private RecentTaskInfo(Parcel source) { */ public static final int RECENT_IGNORE_HOME_STACK_TASKS = 0x0008; + /** + * @hide + */ + public static final int RECENT_DO_NOT_COUNT_EXCLUDED = 0x0010; + /** *

Return a list of the tasks that the user has recently launched, with * the most recent being first and older ones after in order. @@ -951,7 +995,7 @@ private RecentTaskInfo(Parcel source) { * user has started and the maximum number the system can remember. * @param flags Information about what to return. May be any combination * of {@link #RECENT_WITH_EXCLUDED} and {@link #RECENT_IGNORE_UNAVAILABLE}. - * + * * @return Returns a list of RecentTaskInfo records describing each of * the recent tasks. */ @@ -1083,7 +1127,7 @@ public void readFromParcel(Parcel source) { numActivities = source.readInt(); numRunning = source.readInt(); } - + public static final Creator CREATOR = new Creator() { public RunningTaskInfo createFromParcel(Parcel source) { return new RunningTaskInfo(source); @@ -1406,100 +1450,100 @@ public static class RunningServiceInfo implements Parcelable { * If non-zero, this is the process the service is running in. */ public int pid; - + /** * The UID that owns this service. */ public int uid; - + /** * The name of the process this service runs in. */ public String process; - + /** * Set to true if the service has asked to run as a foreground process. */ public boolean foreground; - + /** * The time when the service was first made active, either by someone * starting or binding to it. This * is in units of {@link android.os.SystemClock#elapsedRealtime()}. */ public long activeSince; - + /** * Set to true if this service has been explicitly started. */ public boolean started; - + /** * Number of clients connected to the service. */ public int clientCount; - + /** * Number of times the service's process has crashed while the service * is running. */ public int crashCount; - + /** * The time when there was last activity in the service (either * explicit requests to start it or clients binding to it). This * is in units of {@link android.os.SystemClock#uptimeMillis()}. */ public long lastActivityTime; - + /** * If non-zero, this service is not currently running, but scheduled to * restart at the given time. */ public long restarting; - + /** * Bit for {@link #flags}: set if this service has been * explicitly started. */ public static final int FLAG_STARTED = 1<<0; - + /** * Bit for {@link #flags}: set if the service has asked to * run as a foreground process. */ public static final int FLAG_FOREGROUND = 1<<1; - + /** * Bit for {@link #flags): set if the service is running in a * core system process. */ public static final int FLAG_SYSTEM_PROCESS = 1<<2; - + /** * Bit for {@link #flags): set if the service is running in a * persistent process. */ public static final int FLAG_PERSISTENT_PROCESS = 1<<3; - + /** * Running flags. */ public int flags; - + /** * For special services that are bound to by system code, this is * the package that holds the binding. */ public String clientPackage; - + /** * For special services that are bound to by system code, this is * a string resource providing a user-visible label for who the * client is. */ public int clientLabel; - + public RunningServiceInfo() { } @@ -1540,7 +1584,7 @@ public void readFromParcel(Parcel source) { clientPackage = source.readString(); clientLabel = source.readInt(); } - + public static final Creator CREATOR = new Creator() { public RunningServiceInfo createFromParcel(Parcel source) { return new RunningServiceInfo(source); @@ -1564,7 +1608,7 @@ private RunningServiceInfo(Parcel source) { * @param maxNum The maximum number of entries to return in the list. The * actual number returned may be smaller, depending on how many services * are running. - * + * * @return Returns a list of RunningServiceInfo records describing each of * the running tasks. */ @@ -1578,7 +1622,7 @@ public List getRunningServices(int maxNum) return null; } } - + /** * Returns a PendingIntent you can start to show a control panel for the * given running service. If the service does not have a control panel, @@ -1594,7 +1638,7 @@ public PendingIntent getRunningServiceControlPanel(ComponentName service) return null; } } - + /** * Information you can retrieve about the available memory through * {@link ActivityManager#getMemoryInfo}. @@ -1621,7 +1665,7 @@ public static class MemoryInfo implements Parcelable { * processes. */ public long threshold; - + /** * Set to true if the system considers itself to currently be in a low * memory situation. @@ -1654,7 +1698,7 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeLong(visibleAppThreshold); dest.writeLong(foregroundAppThreshold); } - + public void readFromParcel(Parcel source) { availMem = source.readLong(); totalMem = source.readLong(); @@ -1778,7 +1822,7 @@ public String toString() { */ public boolean clearApplicationUserData(String packageName, IPackageDataObserver observer) { try { - return ActivityManagerNative.getDefault().clearApplicationUserData(packageName, + return ActivityManagerNative.getDefault().clearApplicationUserData(packageName, observer, UserHandle.myUserId()); } catch (RemoteException e) { return false; @@ -1819,7 +1863,7 @@ public static class ProcessErrorStateInfo implements Parcelable { * The process name in which the crash or error occurred. */ public String processName; - + /** * The pid of this process; 0 if none */ @@ -1831,7 +1875,7 @@ public static class ProcessErrorStateInfo implements Parcelable { * the same uid). */ public int uid; - + /** * The activity name associated with the error, if known. May be null. */ @@ -1887,8 +1931,8 @@ public void readFromParcel(Parcel source) { longMsg = source.readString(); stackTrace = source.readString(); } - - public static final Creator CREATOR = + + public static final Creator CREATOR = new Creator() { public ProcessErrorStateInfo createFromParcel(Parcel source) { return new ProcessErrorStateInfo(source); @@ -1902,11 +1946,11 @@ private ProcessErrorStateInfo(Parcel source) { readFromParcel(source); } } - + /** - * Returns a list of any processes that are currently in an error condition. The result + * Returns a list of any processes that are currently in an error condition. The result * will be null if all processes are running properly at this time. - * + * * @return Returns a list of ProcessErrorStateInfo records, or null if there are no * current error conditions (it will not return an empty list). This list ordering is not * specified. @@ -1922,7 +1966,7 @@ public List getProcessesInErrorState() { /** * Information you can retrieve about a running process. */ - public static class RunningAppProcessInfo implements Parcelable { + public static class RunningAppProcessInfo implements Parcelable { /** * The name of the process that this object is associated with */ @@ -1932,17 +1976,17 @@ public static class RunningAppProcessInfo implements Parcelable { * The pid of this process; 0 if none */ public int pid; - + /** * The user id of this process. */ public int uid; - + /** * All packages that have been loaded into the process. */ public String pkgList[]; - + /** * Constant for {@link #flags}: this is an app that is unable to * correctly save its state when going to the background, @@ -1950,7 +1994,7 @@ public static class RunningAppProcessInfo implements Parcelable { * @hide */ public static final int FLAG_CANT_SAVE_STATE = 1<<0; - + /** * Constant for {@link #flags}: this process is associated with a * persistent system app. @@ -1984,21 +2028,21 @@ public static class RunningAppProcessInfo implements Parcelable { * foreground UI. */ public static final int IMPORTANCE_FOREGROUND = 100; - + /** * Constant for {@link #importance}: this process is running something * that is actively visible to the user, though not in the immediate * foreground. */ public static final int IMPORTANCE_VISIBLE = 200; - + /** * Constant for {@link #importance}: this process is running something * that is considered to be actively perceptible to the user. An * example would be an application performing background music playback. */ public static final int IMPORTANCE_PERCEPTIBLE = 130; - + /** * Constant for {@link #importance}: this process is running an * application that can not save its state, and thus can't be killed @@ -2006,19 +2050,19 @@ public static class RunningAppProcessInfo implements Parcelable { * @hide */ public static final int IMPORTANCE_CANT_SAVE_STATE = 170; - + /** * Constant for {@link #importance}: this process is contains services * that should remain running. */ public static final int IMPORTANCE_SERVICE = 300; - + /** * Constant for {@link #importance}: this process process contains * background code that is expendable. */ public static final int IMPORTANCE_BACKGROUND = 400; - + /** * Constant for {@link #importance}: this process is empty of any * actively running code. @@ -2056,7 +2100,7 @@ public static int procStateToImportance(int procState) { * smaller than "less important" values. */ public int importance; - + /** * An additional ordering within a particular {@link #importance} * category, providing finer-grained information about the relative @@ -2073,7 +2117,7 @@ public static int procStateToImportance(int procState) { * been specified for the reason for this level. */ public static final int REASON_UNKNOWN = 0; - + /** * Constant for {@link #importanceReasonCode}: one of the application's * content providers is being used by another process. The pid of @@ -2082,7 +2126,7 @@ public static int procStateToImportance(int procState) { * {@link #importanceReasonComponent}. */ public static final int REASON_PROVIDER_IN_USE = 1; - + /** * Constant for {@link #importanceReasonCode}: one of the application's * content providers is being used by another process. The pid of @@ -2091,25 +2135,25 @@ public static int procStateToImportance(int procState) { * {@link #importanceReasonComponent}. */ public static final int REASON_SERVICE_IN_USE = 2; - + /** * The reason for {@link #importance}, if any. */ public int importanceReasonCode; - + /** * For the specified values of {@link #importanceReasonCode}, this * is the process ID of the other process that is a client of this * process. This will be 0 if no other process is using this one. */ public int importanceReasonPid; - + /** * For the specified values of {@link #importanceReasonCode}, this * is the name of the component that is being used in this process. */ public ComponentName importanceReasonComponent; - + /** * When {@link #importanceReasonPid} is non-0, this is the importance * of the other pid. @hide @@ -2127,7 +2171,7 @@ public RunningAppProcessInfo() { importanceReasonCode = REASON_UNKNOWN; processState = PROCESS_STATE_IMPORTANT_FOREGROUND; } - + public RunningAppProcessInfo(String pProcessName, int pPid, String pArr[]) { processName = pProcessName; pid = pPid; @@ -2170,7 +2214,7 @@ public void readFromParcel(Parcel source) { processState = source.readInt(); } - public static final Creator CREATOR = + public static final Creator CREATOR = new Creator() { public RunningAppProcessInfo createFromParcel(Parcel source) { return new RunningAppProcessInfo(source); @@ -2184,7 +2228,7 @@ private RunningAppProcessInfo(Parcel source) { readFromParcel(source); } } - + /** * Returns a list of application processes installed on external media * that are running on the device. @@ -2203,6 +2247,16 @@ public List getRunningExternalApplications() { return null; } } + /** + * @hide + */ + public Configuration getConfiguration() { + try { + return ActivityManagerNative.getDefault().getConfiguration(); + } catch (RemoteException e) { + return null; + } + } /** * Returns a list of application processes that are running on the device. @@ -2258,7 +2312,7 @@ public Debug.MemoryInfo[] getProcessMemoryInfo(int[] pids) { return null; } } - + /** * @deprecated This is now just a wrapper for * {@link #killBackgroundProcesses(String)}; the previous behavior here @@ -2270,17 +2324,17 @@ public Debug.MemoryInfo[] getProcessMemoryInfo(int[] pids) { public void restartPackage(String packageName) { killBackgroundProcesses(packageName); } - + /** * Have the system immediately kill all background processes associated * with the given package. This is the same as the kernel killing those * processes to reclaim memory; the system will take care of restarting * these processes in the future as needed. - * + * *

You must hold the permission * {@link android.Manifest.permission#KILL_BACKGROUND_PROCESSES} to be able to * call this method. - * + * * @param packageName The name of the package whose processes are to * be killed. */ @@ -2291,7 +2345,7 @@ public void killBackgroundProcesses(String packageName) { } catch (RemoteException e) { } } - + /** * Have the system perform a force stop of everything associated with * the given application package. All processes that share its uid @@ -2299,14 +2353,14 @@ public void killBackgroundProcesses(String packageName) { * removed, etc. In addition, a {@link Intent#ACTION_PACKAGE_RESTARTED} * broadcast will be sent, so that any of its registered alarms can * be stopped, notifications removed, etc. - * + * *

You must hold the permission * {@link android.Manifest.permission#FORCE_STOP_PACKAGES} to be able to * call this method. - * + * * @param packageName The name of the package to be stopped. * @param userId The user for which the running package is to be stopped. - * + * * @hide This is not available to third party applications due to * it allowing them to break other applications by stopping their * services, removing their alarms, etc. @@ -2431,7 +2485,11 @@ public static boolean isUserAMonkey() { * Returns "true" if device is running in a test harness. */ public static boolean isRunningInTestHarness() { - return SystemProperties.getBoolean("ro.test_harness", false); + if (!_isRunningInTestHarnessInit) { + _testHarness = SystemProperties.getBoolean("ro.test_harness", false); + _isRunningInTestHarnessInit = true; + } + return _testHarness; } /** @@ -2569,7 +2627,7 @@ public static int getCurrentUser() { } /** - * @param userid the user's id. Zero indicates the default user + * @param userid the user's id. Zero indicates the default user * @hide */ public boolean switchUser(int userid) { @@ -2783,4 +2841,17 @@ public void setExcludeFromRecents(boolean exclude) { } } } + + /** + * @throws SecurityException Throws SecurityException if the caller does + * not hold the {@link android.Manifest.permission#CHANGE_CONFIGURATION} permission. + * + * @hide + */ + public void updateConfiguration(Configuration values) throws SecurityException { + try { + ActivityManagerNative.getDefault().updateConfiguration(values); + } catch (RemoteException e) { + } + } } diff --git a/core/java/android/app/ActivityManagerNative.java b/core/java/android/app/ActivityManagerNative.java index e8d08b8ccc2..637de6f03e2 100644 --- a/core/java/android/app/ActivityManagerNative.java +++ b/core/java/android/app/ActivityManagerNative.java @@ -17,7 +17,6 @@ package android.app; import android.app.ActivityManager.StackInfo; -import android.app.ProfilerInfo; import android.content.ComponentName; import android.content.IIntentReceiver; import android.content.IIntentSender; @@ -47,6 +46,7 @@ import android.os.ServiceManager; import android.os.StrictMode; import android.service.voice.IVoiceInteractionSession; +import android.os.SystemClock; import android.text.TextUtils; import android.util.Log; import android.util.Singleton; @@ -466,8 +466,9 @@ public boolean onTransact(int code, Parcel data, Parcel reply, int flags) String resultData = data.readString(); Bundle resultExtras = data.readBundle(); boolean resultAbort = data.readInt() != 0; + int intentFlags = data.readInt(); if (who != null) { - finishReceiver(who, resultCode, resultData, resultExtras, resultAbort); + finishReceiver(who, resultCode, resultData, resultExtras, resultAbort, intentFlags); } reply.writeNoException(); return true; @@ -551,6 +552,15 @@ public boolean onTransact(int code, Parcel data, Parcel reply, int flags) return true; } + case GET_CALLING_PACKAGE_FOR_BROADCAST_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + boolean foreground = data.readInt() == 1 ? true : false; + String res = getCallingPackageForBroadcast(foreground); + reply.writeNoException(); + reply.writeString(res); + return true; + } + case GET_CALLING_ACTIVITY_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); IBinder token = data.readStrongBinder(); @@ -793,6 +803,16 @@ public boolean onTransact(int code, Parcel data, Parcel reply, int flags) return true; } + case GET_ACTIVITY_FOR_TASK_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + int task = data.readInt(); + boolean onlyRoot = data.readInt() != 0; + IBinder res = getActivityForTask(task, onlyRoot); + reply.writeNoException(); + reply.writeStrongBinder(res); + return true; + } + case GET_CONTENT_PROVIDER_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); IBinder b = data.readStrongBinder(); @@ -2209,17 +2229,12 @@ public boolean onTransact(int code, Parcel data, Parcel reply, int flags) return true; } - case GET_ACTIVITY_CONTAINER_TRANSACTION: { + case GET_ACTIVITY_DISPLAY_ID_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); IBinder activityToken = data.readStrongBinder(); - IActivityContainer activityContainer = getEnclosingActivityContainer(activityToken); + int displayId = getActivityDisplayId(activityToken); reply.writeNoException(); - if (activityContainer != null) { - reply.writeInt(1); - reply.writeStrongBinder(activityContainer.asBinder()); - } else { - reply.writeInt(0); - } + reply.writeInt(displayId); return true; } @@ -2396,6 +2411,7 @@ protected IActivityManager create() { class ActivityManagerProxy implements IActivityManager { + static final String TAG_TIMELINE = "Timeline"; public ActivityManagerProxy(IBinder remote) { mRemote = remote; @@ -2411,6 +2427,13 @@ public int startActivity(IApplicationThread caller, String callingPackage, Inten int startFlags, ProfilerInfo profilerInfo, Bundle options) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); + + if (intent.getComponent() != null) { + Log.i(TAG_TIMELINE, "Timeline: Activity_launch_request id:" + + intent.getComponent().getPackageName() + " time:" + + SystemClock.uptimeMillis()); + } + data.writeInterfaceToken(IActivityManager.descriptor); data.writeStrongBinder(caller != null ? caller.asBinder() : null); data.writeString(callingPackage); @@ -2848,7 +2871,8 @@ public void unbroadcastIntent(IApplicationThread caller, Intent intent, int user data.recycle(); reply.recycle(); } - public void finishReceiver(IBinder who, int resultCode, String resultData, Bundle map, boolean abortBroadcast) throws RemoteException + public void finishReceiver(IBinder who, int resultCode, String resultData, Bundle map, + boolean abortBroadcast, int flags) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); @@ -2858,6 +2882,7 @@ public void finishReceiver(IBinder who, int resultCode, String resultData, Bundl data.writeString(resultData); data.writeBundle(map); data.writeInt(abortBroadcast ? 1 : 0); + data.writeInt(flags); mRemote.transact(FINISH_RECEIVER_TRANSACTION, data, reply, IBinder.FLAG_ONEWAY); reply.readException(); data.recycle(); @@ -2877,6 +2902,8 @@ public void attachApplication(IApplicationThread app) throws RemoteException public void activityIdle(IBinder token, Configuration config, boolean stopProfiling) throws RemoteException { + Log.i(TAG_TIMELINE, "Timeline: Activity_idle id: " + token + " time:" + + SystemClock.uptimeMillis()); Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); @@ -2965,6 +2992,19 @@ public String getCallingPackage(IBinder token) throws RemoteException reply.recycle(); return res; } + public String getCallingPackageForBroadcast(boolean foreground) throws RemoteException + { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeInt(foreground ? 1 : 0); + mRemote.transact(GET_CALLING_PACKAGE_FOR_BROADCAST_TRANSACTION, data, reply, 0); + reply.readException(); + String res = reply.readString(); + data.recycle(); + reply.recycle(); + return res; + } public ComponentName getCallingActivity(IBinder token) throws RemoteException { Parcel data = Parcel.obtain(); @@ -3293,6 +3333,21 @@ public void registerTaskStackListener(ITaskStackListener listener) throws Remote data.recycle(); reply.recycle(); } + + public IBinder getActivityForTask(int task, boolean onlyRoot) throws RemoteException + { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeInt(task); + data.writeInt(onlyRoot ? 1 : 0); + mRemote.transact(GET_ACTIVITY_FOR_TASK_TRANSACTION, data, reply, 0); + reply.readException(); + IBinder res = reply.readStrongBinder(); + data.recycle(); + reply.recycle(); + return res; + } public int getTaskForActivity(IBinder token, boolean onlyRoot) throws RemoteException { Parcel data = Parcel.obtain(); @@ -5238,26 +5293,21 @@ public void deleteActivityContainer(IActivityContainer activityContainer) reply.recycle(); } - public IActivityContainer getEnclosingActivityContainer(IBinder activityToken) - throws RemoteException { + @Override + public int getActivityDisplayId(IBinder activityToken) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); data.writeStrongBinder(activityToken); - mRemote.transact(GET_ACTIVITY_CONTAINER_TRANSACTION, data, reply, 0); + mRemote.transact(GET_ACTIVITY_DISPLAY_ID_TRANSACTION, data, reply, 0); reply.readException(); - final int result = reply.readInt(); - final IActivityContainer res; - if (result == 1) { - res = IActivityContainer.Stub.asInterface(reply.readStrongBinder()); - } else { - res = null; - } + final int displayId = reply.readInt(); data.recycle(); reply.recycle(); - return res; + return displayId; } + @Override public IBinder getHomeActivityToken() throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index f2be45c463a..afa0984b584 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -43,6 +43,7 @@ import android.database.sqlite.SQLiteDebug.DbStats; import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.Typeface; import android.hardware.display.DisplayManagerGlobal; import android.net.ConnectivityManager; import android.net.IConnectivityManager; @@ -75,6 +76,7 @@ import android.os.Trace; import android.os.UserHandle; import android.provider.Settings; +import android.text.TextUtils; import android.util.AndroidRuntimeException; import android.util.ArrayMap; import android.util.DisplayMetrics; @@ -87,6 +89,7 @@ import android.util.SuperNotCalledException; import android.view.Display; import android.view.HardwareRenderer; +import android.view.InflateException; import android.view.IWindowManager; import android.view.IWindowSessionCallback; import android.view.View; @@ -124,7 +127,6 @@ import java.util.Map; import java.util.Objects; import java.util.TimeZone; -import java.util.regex.Pattern; import libcore.io.DropBox; import libcore.io.EventLogger; @@ -163,7 +165,6 @@ public final class ActivityThread { private static final boolean DEBUG_MEMORY_TRIM = false; private static final boolean DEBUG_PROVIDER = false; private static final long MIN_TIME_BETWEEN_GCS = 5*1000; - private static final Pattern PATTERN_SEMICOLON = Pattern.compile(";"); private static final int SQLITE_MEM_RELEASED_EVENT_LOG_TAG = 75003; private static final int LOG_ON_PAUSE_CALLED = 30021; private static final int LOG_ON_RESUME_CALLED = 30022; @@ -258,18 +259,21 @@ public int hashCode() { } } + static final class AcquiringProviderRecord { + IActivityManager.ContentProviderHolder holder; + boolean acquiring = true; + int requests = 1; + } + // The lock of mProviderMap protects the following variables. - final ArrayMap mProviderMap - = new ArrayMap(); - final ArrayMap mProviderRefCountMap - = new ArrayMap(); - final ArrayMap mLocalProviders - = new ArrayMap(); - final ArrayMap mLocalProvidersByName - = new ArrayMap(); + final ArrayMap mProviderMap = new ArrayMap<>(); + final ArrayMap mAcquiringProviderMap = new ArrayMap<>(); + final ArrayMap mProviderRefCountMap = new ArrayMap<>(); + final ArrayMap mLocalProviders = new ArrayMap<>(); + final ArrayMap mLocalProvidersByName = new ArrayMap<>(); final ArrayMap> mOnPauseListeners - = new ArrayMap>(); + = new ArrayMap<>(); final GcIdler mGcIdler = new GcIdler(); boolean mGcIdlerScheduled = false; @@ -346,7 +350,7 @@ public String toString() { } } - final class ProviderClientRecord { + static final class ProviderClientRecord { final String[] mNames; final IContentProvider mProvider; final ContentProvider mLocalProvider; @@ -374,7 +378,7 @@ static final class ReceiverData extends BroadcastReceiver.PendingResult { public ReceiverData(Intent intent, int resultCode, String resultData, Bundle resultExtras, boolean ordered, boolean sticky, IBinder token, int sendingUser) { super(resultCode, resultData, resultExtras, TYPE_COMPONENT, ordered, sticky, - token, sendingUser); + token, sendingUser, intent.getFlags()); this.intent = intent; } @@ -1086,14 +1090,29 @@ public void dumpGfxInfo(FileDescriptor fd, String[] args) { WindowManagerGlobal.getInstance().dumpGfxInfo(fd); } - @Override - public void dumpDbInfo(FileDescriptor fd, String[] args) { + private void dumpDatabaseInfo(FileDescriptor fd, String[] args) { PrintWriter pw = new FastPrintWriter(new FileOutputStream(fd)); PrintWriterPrinter printer = new PrintWriterPrinter(pw); SQLiteDebug.dump(printer, args); pw.flush(); } + @Override + public void dumpDbInfo(final FileDescriptor fd, final String[] args) { + if (mSystemThread) { + // Ensure this invocation is asynchronous to prevent + // writer waiting due to buffer cannot be consumed. + AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() { + @Override + public void run() { + dumpDatabaseInfo(fd, args); + } + }); + } else { + dumpDatabaseInfo(fd, args); + } + } + @Override public void unstableProviderDied(IBinder provider) { sendMessage(H.UNSTABLE_PROVIDER_DIED, provider); @@ -1646,9 +1665,20 @@ Configuration applyConfigCompatMainThread(int displayDensity, Configuration conf */ Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs, String[] libDirs, int displayId, Configuration overrideConfiguration, - LoadedApk pkgInfo) { + LoadedApk pkgInfo, Context context, String pkgName) { return mResourcesManager.getTopLevelResources(resDir, splitResDirs, overlayDirs, libDirs, - displayId, overrideConfiguration, pkgInfo.getCompatibilityInfo(), null); + displayId, pkgName, overrideConfiguration, pkgInfo.getCompatibilityInfo(), null, + context, pkgInfo.getApplicationInfo().isThemeable); + } + + /** + * Creates the top level resources for the given package. + */ + Resources getTopLevelThemedResources(String resDir, int displayId, LoadedApk pkgInfo, + String pkgName, String themePkgName) { + return mResourcesManager.getTopLevelThemedResources(resDir, displayId, pkgName, + themePkgName, pkgInfo.getCompatibilityInfo(), null, + pkgInfo.getApplicationInfo().isThemeable); } final Handler getHandler() { @@ -1756,8 +1786,7 @@ private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compat ref = mResourcePackages.get(aInfo.packageName); } LoadedApk packageInfo = ref != null ? ref.get() : null; - if (packageInfo == null || (packageInfo.mResources != null - && !packageInfo.mResources.getAssets().isUpToDate())) { + if (packageInfo == null) { if (localLOGV) Slog.v(TAG, (includeCode ? "Loading code package " : "Loading resource-only package ") + aInfo.packageName + " (in " + (mBoundApplication != null @@ -1781,6 +1810,10 @@ private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compat new WeakReference(packageInfo)); } } + if (packageInfo.mResources != null + && !packageInfo.mResources.getAssets().isUpToDate()) { + packageInfo.mResources = null; + } return packageInfo; } } @@ -2153,6 +2186,18 @@ public final Activity getActivity(IBinder token) { return mActivities.get(token).activity; } + protected void performFinishFloating() { + synchronized (mPackages) { + Activity a = null; + for (ActivityClientRecord ar : mActivities.values()) { + a = ar.activity; + if (a != null && !a.mFinished && a.getWindow() != null && a.getWindow().mIsFloatingWindow) { + a.finish(); + } + } + } + } + public final void sendActivityResult( IBinder token, String id, int requestCode, int resultCode, Intent data) { @@ -2339,10 +2384,7 @@ private Context createBaseContextForActivity(ActivityClientRecord r, final DisplayManagerGlobal dm = DisplayManagerGlobal.getInstance(); try { - IActivityContainer container = - ActivityManagerNative.getDefault().getEnclosingActivityContainer(r.token); - final int displayId = - container == null ? Display.DEFAULT_DISPLAY : container.getDisplayId(); + final int displayId = ActivityManagerNative.getDefault().getActivityDisplayId(r.token); if (displayId > Display.DEFAULT_DISPLAY) { Display display = dm.getRealDisplay(displayId, r.token); baseContext = appContext.createDisplayContext(display); @@ -2985,12 +3027,12 @@ public final ActivityClientRecord performResumeActivity(IBinder token, r.state = null; r.persistentState = null; } catch (Exception e) { - if (!mInstrumentation.onException(r.activity, e)) { + /*if (!mInstrumentation.onException(r.activity, e)) { throw new RuntimeException( "Unable to resume activity " + r.intent.getComponent().toShortString() + ": " + e.toString(), e); - } + }*/ } } return r; @@ -4126,8 +4168,12 @@ static void freeTextLayoutCachesIfNeeded(int configDiff) { if (configDiff != 0) { // Ask text layout engine to free its caches if there is a locale change boolean hasLocaleConfigChange = ((configDiff & ActivityInfo.CONFIG_LOCALE) != 0); - if (hasLocaleConfigChange) { + boolean hasFontConfigChange = ((configDiff & ActivityInfo.CONFIG_THEME_FONT) != 0); + if (hasLocaleConfigChange || hasFontConfigChange) { Canvas.freeTextLayoutCaches(); + if (hasFontConfigChange) { + Typeface.recreateDefaults(); + } if (DEBUG_CONFIGURATION) Slog.v(TAG, "Cleared TextLayout Caches"); } } @@ -4184,6 +4230,13 @@ static final void handleDumpHeap(boolean managed, DumpHeapData dhd) { } catch (IOException e) { Slog.w(TAG, "Managed heap dump failed on path " + dhd.path + " -- can the process access this path?"); + } catch (RuntimeException e) { + // Throw exception for non-system process for notifying unable dump reason to do error handle. + ActivityThread am = currentActivityThread(); + if (am == null || !am.mSystemThread) { + throw new RuntimeException("Unable to dump heap on path " + dhd.path + + ": " + e.toString(), e); + } } finally { try { dhd.fd.close(); @@ -4199,22 +4252,24 @@ static final void handleDumpHeap(boolean managed, DumpHeapData dhd) { final void handleDispatchPackageBroadcast(int cmd, String[] packages) { boolean hasPkgInfo = false; if (packages != null) { - for (int i=packages.length-1; i>=0; i--) { - //Slog.i(TAG, "Cleaning old package: " + packages[i]); - if (!hasPkgInfo) { - WeakReference ref; - ref = mPackages.get(packages[i]); - if (ref != null && ref.get() != null) { - hasPkgInfo = true; - } else { - ref = mResourcePackages.get(packages[i]); + synchronized (mResourcesManager) { + for (int i=packages.length-1; i>=0; i--) { + //Slog.i(TAG, "Cleaning old package: " + packages[i]); + if (!hasPkgInfo) { + WeakReference ref; + ref = mPackages.get(packages[i]); if (ref != null && ref.get() != null) { hasPkgInfo = true; + } else { + ref = mResourcePackages.get(packages[i]); + if (ref != null && ref.get() != null) { + hasPkgInfo = true; + } } } + mPackages.remove(packages[i]); + mResourcePackages.remove(packages[i]); } - mPackages.remove(packages[i]); - mResourcePackages.remove(packages[i]); } } ApplicationPackageManager.handlePackageBroadcast(cmd, packages, @@ -4285,7 +4340,7 @@ private void updateDefaultDensity() { + DisplayMetrics.DENSITY_DEVICE + " to " + mCurDefaultDisplayDpi); DisplayMetrics.DENSITY_DEVICE = mCurDefaultDisplayDpi; - Bitmap.setDefaultDensity(DisplayMetrics.DENSITY_DEFAULT); + Bitmap.setDefaultDensity(DisplayMetrics.DENSITY_DEVICE); } } @@ -4371,15 +4426,22 @@ private void handleBindApplication(AppBindData data) { if (cacheDir != null) { // Provide a usable directory for temporary files System.setProperty("java.io.tmpdir", cacheDir.getAbsolutePath()); - - setupGraphicsSupport(data.info, cacheDir); } else { - Log.e(TAG, "Unable to setupGraphicsSupport due to missing cache directory"); + Log.v(TAG, "Unable to initialize \"java.io.tmpdir\" property due to missing cache directory"); + } + + // Use codeCacheDir to store generated/compiled graphics code + final File codeCacheDir = appContext.getCodeCacheDir(); + if (codeCacheDir != null) { + setupGraphicsSupport(data.info, codeCacheDir); + } else { + Log.e(TAG, "Unable to setupGraphicsSupport due to missing code-cache directory"); } } - final boolean is24Hr = "24".equals(mCoreSettings.getString(Settings.System.TIME_12_24)); + final boolean is24Hr = android.text.format.DateFormat.is24HourFormat( + mCoreSettings.getString(Settings.System.TIME_12_24), data.config.locale); DateFormat.set24HourTimePref(is24Hr); View.mDebugViewAttributes = @@ -4611,22 +4673,57 @@ private void installContentProviders( public final IContentProvider acquireProvider( Context c, String auth, int userId, boolean stable) { - final IContentProvider provider = acquireExistingProvider(c, auth, userId, stable); + final ProviderKey key = new ProviderKey(auth, userId); + final IContentProvider provider = acquireExistingProvider(c, key, stable); if (provider != null) { return provider; } + AcquiringProviderRecord r; + boolean first = false; + synchronized (mAcquiringProviderMap) { + r = mAcquiringProviderMap.get(key); + if (r == null) { + r = new AcquiringProviderRecord(); + mAcquiringProviderMap.put(key, r); + first = true; + } else { + r.requests++; + } + } - // There is a possible race here. Another thread may try to acquire - // the same provider at the same time. When this happens, we want to ensure - // that the first one wins. - // Note that we cannot hold the lock while acquiring and installing the - // provider since it might take a long time to run and it could also potentially - // be re-entrant in the case where the provider is in the same process. IActivityManager.ContentProviderHolder holder = null; - try { - holder = ActivityManagerNative.getDefault().getContentProvider( - getApplicationThread(), auth, userId, stable); - } catch (RemoteException ex) { + if (first) { + // Multiple threads may try to acquire the same provider at the same time. + // When this happens, we only let the first one really gets provider. + // Other threads just wait for its result. + // Note that we cannot hold the lock while acquiring and installing the + // provider since it might take a long time to run and it could also potentially + // be re-entrant in the case where the provider is in the same process. + try { + holder = ActivityManagerNative.getDefault().getContentProvider( + getApplicationThread(), auth, userId, stable); + } catch (RemoteException ex) { + } + synchronized (r) { + r.holder = holder; + r.acquiring = false; + r.notifyAll(); + } + } else { + synchronized (r) { + while (r.acquiring) { + try { + r.wait(); + } catch (InterruptedException e) { + } + } + holder = r.holder; + } + } + synchronized (mAcquiringProviderMap) { + if (--r.requests == 0) { + mAcquiringProviderMap.remove(key); + } } if (holder == null) { Slog.e(TAG, "Failed to find provider info for " + auth); @@ -4710,8 +4807,12 @@ private final void incProviderRefLocked(ProviderRefCount prc, boolean stable) { public final IContentProvider acquireExistingProvider( Context c, String auth, int userId, boolean stable) { + return acquireExistingProvider(c, new ProviderKey(auth, userId), stable); + } + + final IContentProvider acquireExistingProvider( + Context c, ProviderKey key, boolean stable) { synchronized (mProviderMap) { - final ProviderKey key = new ProviderKey(auth, userId); final ProviderClientRecord pr = mProviderMap.get(key); if (pr == null) { return null; @@ -4722,7 +4823,7 @@ public final IContentProvider acquireExistingProvider( if (!jBinder.isBinderAlive()) { // The hosting process of the provider has died; we can't // use this one. - Log.i(TAG, "Acquiring provider " + auth + " for user " + userId + Log.i(TAG, "Acquiring provider " + key.authority + " for user " + key.userId + ": existing object's process dead"); handleUnstableProviderDiedLocked(jBinder, true); return null; @@ -4921,7 +5022,7 @@ final void appNotRespondingViaProvider(IBinder provider) { private ProviderClientRecord installProviderAuthoritiesLocked(IContentProvider provider, ContentProvider localProvider, IActivityManager.ContentProviderHolder holder) { - final String auths[] = PATTERN_SEMICOLON.split(holder.info.authority); + final String auths[] = holder.info.authority.split(";"); final int userId = UserHandle.getUserId(holder.info.applicationInfo.uid); final ProviderClientRecord pcr = new ProviderClientRecord( @@ -5044,18 +5145,12 @@ private IActivityManager.ContentProviderHolder installProvider(Context context, if (DEBUG_PROVIDER) { Slog.v(TAG, "installProvider: lost the race, updating ref count"); } - // We need to transfer our new reference to the existing - // ref count, releasing the old one... but only if - // release is needed (that is, it is not running in the - // system process). + // The provider has already been installed, so we need + // to increase reference count to the existing one, but + // only if release is needed (that is, it is not running + // in the system process or local to the process). if (!noReleaseNeeded) { incProviderRefLocked(prc, stable); - try { - ActivityManagerNative.getDefault().removeContentProvider( - holder.connection, stable); - } catch (RemoteException e) { - //do nothing content provider object is dead any way - } } } else { ProviderClientRecord client = installProviderAuthoritiesLocked( diff --git a/core/java/android/app/AlarmManager.java b/core/java/android/app/AlarmManager.java index 2c596e5c37c..32600d9add2 100644 --- a/core/java/android/app/AlarmManager.java +++ b/core/java/android/app/AlarmManager.java @@ -100,6 +100,13 @@ public class AlarmManager */ public static final int ELAPSED_REALTIME = 3; + /** @hide + * Alarm time in {@link System#currentTimeMillis System.currentTimeMillis()} + * (wall clock time in UTC), which will wake up the device when + * it goes off. And it will power on the devices when it shuts down. + */ + public static final int RTC_POWEROFF_WAKEUP = 4; + /** * Broadcast Action: Sent after the value returned by * {@link #getNextAlarmClock()} has changed. @@ -629,7 +636,7 @@ public long getTriggerTime() { } /** - * Returns an intent intent that can be used to show or edit details of the alarm clock in + * Returns an intent that can be used to show or edit details of the alarm clock in * the application that scheduled it. * *

Beware that any application can retrieve and send this intent, diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 95870cf9675..04775f0cf58 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -1,4 +1,7 @@ /* + * Copyright (c) 2013-2014, The Linux Foundation. All rights reserved. + * Not a Contribution. + * * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -26,6 +29,7 @@ import android.os.Parcelable; import android.os.Process; import android.os.RemoteException; +import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.util.ArrayMap; @@ -68,6 +72,10 @@ public class AppOpsManager { * will do this for you). */ + /** {@hide */ + public static final String ACTION_SU_SESSION_CHANGED = + "android.intent.action.SU_SESSION_CHANGED"; + final Context mContext; final IAppOpsService mService; final ArrayMap mModeWatchers @@ -102,9 +110,18 @@ public class AppOpsManager { */ public static final int MODE_DEFAULT = 3; + /** + * @hide Result from {@link #checkOp}, {@link #noteOp}, {@link #startOp}: + * AppOps Service should show a dialog box on screen to get user + * permission. + */ + public static final int MODE_ASK = 4; + // when adding one of these: // - increment _NUM_OP - // - add rows to sOpToSwitch, sOpToString, sOpNames, sOpPerms, sOpDefaultMode + // - add rows to sOpToSwitch, sOpToString, sOpNames, sOpPerms, sOpDefaultMode, sOpDefaultStrictMode, + // sOpToOpString, sOpStrictMode. + // - add descriptive strings to frameworks/base/core/res/res/values/config.xml // - add descriptive strings to Settings/res/values/arrays.xml // - add the op to the appropriate template in AppOpsState.OpsTemplate (settings app) @@ -207,7 +224,35 @@ public class AppOpsManager { /** @hide Activate a VPN connection without user intervention. */ public static final int OP_ACTIVATE_VPN = 47; /** @hide */ - public static final int _NUM_OP = 48; + public static final int OP_WIFI_CHANGE = 48; + /** @hide */ + public static final int OP_BLUETOOTH_CHANGE = 49; + /** @hide */ + public static final int OP_SEND_MMS = 50; + /** @hide */ + public static final int OP_READ_MMS = 51; + /** @hide */ + public static final int OP_WRITE_MMS = 52; + /** @hide */ + public static final int OP_BOOT_COMPLETED = 53; + /** @hide */ + public static final int OP_NFC_CHANGE = 54; + /** @hide */ + public static final int OP_DELETE_SMS = 55; + /** @hide */ + public static final int OP_DELETE_MMS = 56; + /** @hide */ + public static final int OP_DELETE_CONTACTS = 57; + /** @hide */ + public static final int OP_DELETE_CALL_LOG = 58; + /** @hide */ + public static final int OP_DATA_CONNECT_CHANGE = 59; + /** @hide */ + public static final int OP_ALARM_WAKEUP = 60; + /** @hide */ + public static final int OP_SU = 61; + /** @hide */ + public static final int _NUM_OP = 62; /** Access to coarse location information. */ public static final String OPSTR_COARSE_LOCATION = @@ -228,6 +273,119 @@ public class AppOpsManager { @SystemApi public static final String OPSTR_ACTIVATE_VPN = "android:activate_vpn"; + private static final String OPSTR_GPS = + "android:gps"; + private static final String OPSTR_VIBRATE = + "android:vibrate"; + private static final String OPSTR_READ_CONTACTS = + "android:read_contacts"; + private static final String OPSTR_WRITE_CONTACTS = + "android:write_contacts"; + private static final String OPSTR_READ_CALL_LOG = + "android:read_call_log"; + private static final String OPSTR_WRITE_CALL_LOG = + "android:write_call_log"; + private static final String OPSTR_READ_CALENDAR = + "android:read_calendar"; + private static final String OPSTR_WRITE_CALENDAR = + "android:write_calendar"; + private static final String OPSTR_WIFI_SCAN = + "android:wifi_scan"; + private static final String OPSTR_POST_NOTIFICATION = + "android:post_notification"; + private static final String OPSTR_NEIGHBORING_CELLS = + "android:neighboring_cells"; + private static final String OPSTR_CALL_PHONE = + "android:call_phone"; + private static final String OPSTR_READ_SMS = + "android:read_sms"; + private static final String OPSTR_WRITE_SMS = + "android:write_sms"; + private static final String OPSTR_RECEIVE_SMS = + "android:receive_sms"; + private static final String OPSTR_RECEIVE_EMERGECY_SMS = + "android:receive_emergecy_sms"; + private static final String OPSTR_RECEIVE_MMS = + "android:receive_mms"; + private static final String OPSTR_RECEIVE_WAP_PUSH = + "android:receive_wap_push"; + private static final String OPSTR_SEND_SMS = + "android:send_sms"; + private static final String OPSTR_READ_ICC_SMS = + "android:read_icc_sms"; + private static final String OPSTR_WRITE_ICC_SMS = + "android:write_icc_sms"; + private static final String OPSTR_WRITE_SETTINGS = + "android:write_settings"; + private static final String OPSTR_SYSTEM_ALERT_WINDOW = + "android:system_alert_window"; + private static final String OPSTR_ACCESS_NOTIFICATIONS = + "android:access_notifications"; + private static final String OPSTR_CAMERA = + "android:camera"; + private static final String OPSTR_RECORD_AUDIO = + "android:record_audio"; + private static final String OPSTR_PLAY_AUDIO = + "android:play_audio"; + private static final String OPSTR_READ_CLIPBOARD = + "android:read_clipboard"; + private static final String OPSTR_WRITE_CLIPBOARD = + "android:write_clipboard"; + private static final String OPSTR_TAKE_MEDIA_BUTTONS = + "android:take_media_buttons"; + private static final String OPSTR_TAKE_AUDIO_FOCUS = + "android:take_audio_focus"; + private static final String OPSTR_AUDIO_MASTER_VOLUME = + "android:audio_master_volume"; + private static final String OPSTR_AUDIO_VOICE_VOLUME = + "android:audio_voice_volume"; + private static final String OPSTR_AUDIO_RING_VOLUME = + "android:audio_ring_volume"; + private static final String OPSTR_AUDIO_MEDIA_VOLUME = + "android:audio_media_volume"; + private static final String OPSTR_AUDIO_ALARM_VOLUME = + "android:audio_alarm_volume"; + private static final String OPSTR_AUDIO_NOTIFICATION_VOLUME = + "android:audio_notification_volume"; + private static final String OPSTR_AUDIO_BLUETOOTH_VOLUME = + "android:audio_bluetooth_volume"; + private static final String OPSTR_WAKE_LOCK = + "android:wake_lock"; + private static final String OPSTR_MUTE_MICROPHONE = + "android:mute_microphone"; + private static final String OPSTR_TOAST_WINDOW = + "android:toast_window"; + private static final String OPSTR_PROJECT_MEDIA = + "android:project_media"; + private static final String OPSTR_WIFI_CHANGE = + "android:wifi_change"; + private static final String OPSTR_BLUETOOTH_CHANGE = + "android:bluetooth_change"; + private static final String OPSTR_SEND_MMS = + "android:send_mms"; + private static final String OPSTR_READ_MMS = + "android:read_mms"; + private static final String OPSTR_WRITE_MMS = + "android:write_mms"; + private static final String OPSTR_BOOT_COMPLETED = + "android:boot_completed"; + private static final String OPSTR_NFC_CHANGE = + "android:nfc_change"; + private static final String OPSTR_DELETE_SMS = + "android:delete_sms"; + private static final String OPSTR_DELETE_MMS = + "android:delete_mms"; + private static final String OPSTR_DELETE_CONTACTS = + "android:delete_contacts"; + private static final String OPSTR_DELETE_CALL_LOG = + "android:delete_call_log"; + private static final String OPSTR_DATA_CONNECT_CHANGE = + "android:data_connect_change"; + private static final String OPSTR_ALARM_WAKEUP = + "android:alarm_wakeup"; + private static final String OPSTR_SU = + "android:su"; + /** * This maps each operation to the operation that serves as the * switch to determine whether it is allowed. Generally this is @@ -285,6 +443,20 @@ public class AppOpsManager { OP_TOAST_WINDOW, OP_PROJECT_MEDIA, OP_ACTIVATE_VPN, + OP_WIFI_CHANGE, + OP_BLUETOOTH_CHANGE, + OP_SEND_MMS, + OP_READ_MMS, + OP_WRITE_MMS, + OP_BOOT_COMPLETED, + OP_NFC_CHANGE, + OP_DELETE_SMS, + OP_DELETE_MMS, + OP_DELETE_CONTACTS, + OP_DELETE_CALL_LOG, + OP_DATA_CONNECT_CHANGE, + OP_ALARM_WAKEUP, + OP_SU }; /** @@ -340,6 +512,89 @@ public class AppOpsManager { null, null, OPSTR_ACTIVATE_VPN, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + OPSTR_SU, + }; + + /** + * This maps each operation to the public string constant for it. + * If it doesn't have a public string constant, it maps to null. + */ + private static String[] sOpToOpString = new String[] { + OPSTR_COARSE_LOCATION, + OPSTR_FINE_LOCATION, + OPSTR_GPS, + OPSTR_VIBRATE, + OPSTR_READ_CONTACTS, + OPSTR_WRITE_CONTACTS, + OPSTR_READ_CALL_LOG, + OPSTR_WRITE_CALL_LOG, + OPSTR_READ_CALENDAR, + OPSTR_WRITE_CALENDAR, + OPSTR_WIFI_SCAN, + OPSTR_POST_NOTIFICATION, + OPSTR_NEIGHBORING_CELLS, + OPSTR_CALL_PHONE, + OPSTR_READ_SMS, + OPSTR_WRITE_SMS, + OPSTR_RECEIVE_SMS, + OPSTR_RECEIVE_EMERGECY_SMS, + OPSTR_RECEIVE_MMS, + OPSTR_RECEIVE_WAP_PUSH, + OPSTR_SEND_SMS, + OPSTR_READ_ICC_SMS, + OPSTR_WRITE_ICC_SMS, + OPSTR_WRITE_SETTINGS, + OPSTR_SYSTEM_ALERT_WINDOW, + OPSTR_ACCESS_NOTIFICATIONS, + OPSTR_CAMERA, + OPSTR_RECORD_AUDIO, + OPSTR_PLAY_AUDIO, + OPSTR_READ_CLIPBOARD, + OPSTR_WRITE_CLIPBOARD, + OPSTR_TAKE_MEDIA_BUTTONS, + OPSTR_TAKE_AUDIO_FOCUS, + OPSTR_AUDIO_MASTER_VOLUME, + OPSTR_AUDIO_VOICE_VOLUME, + OPSTR_AUDIO_RING_VOLUME, + OPSTR_AUDIO_MEDIA_VOLUME, + OPSTR_AUDIO_ALARM_VOLUME, + OPSTR_AUDIO_NOTIFICATION_VOLUME, + OPSTR_AUDIO_BLUETOOTH_VOLUME, + OPSTR_WAKE_LOCK, + OPSTR_MONITOR_LOCATION, + OPSTR_MONITOR_HIGH_POWER_LOCATION, + OPSTR_GET_USAGE_STATS, + OPSTR_MUTE_MICROPHONE, + OPSTR_TOAST_WINDOW, + OPSTR_PROJECT_MEDIA, + OPSTR_ACTIVATE_VPN, + OPSTR_WIFI_CHANGE, + OPSTR_BLUETOOTH_CHANGE, + OPSTR_SEND_MMS, + OPSTR_READ_MMS, + OPSTR_WRITE_MMS, + OPSTR_BOOT_COMPLETED, + OPSTR_NFC_CHANGE, + OPSTR_DELETE_SMS, + OPSTR_DELETE_MMS, + OPSTR_DELETE_CONTACTS, + OPSTR_DELETE_CALL_LOG, + OPSTR_DATA_CONNECT_CHANGE, + OPSTR_ALARM_WAKEUP, + OPSTR_SU, }; /** @@ -395,6 +650,20 @@ public class AppOpsManager { "TOAST_WINDOW", "PROJECT_MEDIA", "ACTIVATE_VPN", + "WIFI_CHANGE", + "BLUETOOTH_CHANGE", + "SEND_MMS", + "READ_MMS", + "WRITE_MMS", + "BOOT_COMPLETED", + "NFC_CHANGE", + "DELETE_SMS", + "DELETE_MMS", + "DELETE_CONTACTS", + "DELETE_CALL_LOG", + "DATA_CONNECT_CHANGE", + "ALARM_WAKEUP", + "SU", }; /** @@ -450,6 +719,20 @@ public class AppOpsManager { null, // no permission for displaying toasts null, // no permission for projecting media null, // no permission for activating vpn + android.Manifest.permission.CHANGE_WIFI_STATE, + android.Manifest.permission.BLUETOOTH, + android.Manifest.permission.SEND_SMS, + android.Manifest.permission.READ_SMS, + android.Manifest.permission.WRITE_SMS, + android.Manifest.permission.RECEIVE_BOOT_COMPLETED, + android.Manifest.permission.NFC, + android.Manifest.permission.WRITE_SMS, + android.Manifest.permission.WRITE_SMS, + android.Manifest.permission.WRITE_CONTACTS, + android.Manifest.permission.WRITE_CALL_LOG, + android.Manifest.permission.MODIFY_PHONE_STATE, + null, // OP_ALARM_WAKEUP + null, }; /** @@ -506,6 +789,20 @@ public class AppOpsManager { UserManager.DISALLOW_CREATE_WINDOWS, // TOAST_WINDOW null, //PROJECT_MEDIA UserManager.DISALLOW_CONFIG_VPN, // ACTIVATE_VPN + null, //WIFI_CHANGE + null, //BLUETOOTH_CHANGE + null, //SEND_MMS + null, //READ_MMS + null, //WRITE_MMS + null, //BOOT_COMPLETED + null, //NFC_CHANGE + null, //DELETE_SMS + null, //DELETE_MMS + null, //DELETE_CONTACTS + null, //DELETE_CALL_LOG + null, //DATA_CONNECT_CHANGE + null, //ALARM_WAKEUP + UserManager.DISALLOW_SU, //SU TODO: this should really be investigated. }; /** @@ -561,6 +858,20 @@ public class AppOpsManager { true, //TOAST_WINDOW false, //PROJECT_MEDIA false, //ACTIVATE_VPN + false, // WIFI_CHANGE + false, // BLUETOOTH_CHANGE + false, // SEND_MMS + false, // READ_MMS + false, // WRITE_MMS + false, // BOOT_COMPLETED + false, // NFC_CHANGE + false, //DELETE_SMS + false, //DELETE_MMS + false, //DELETE_CONTACTS + false, //DELETE_CALL_LOG + false, //DATA_CONNECT_CHANGE + true, //ALARM_WAKEUP + false, //SU }; /** @@ -615,6 +926,157 @@ public class AppOpsManager { AppOpsManager.MODE_ALLOWED, AppOpsManager.MODE_IGNORED, // OP_PROJECT_MEDIA AppOpsManager.MODE_IGNORED, // OP_ACTIVATE_VPN + AppOpsManager.MODE_ALLOWED, + AppOpsManager.MODE_ALLOWED, + AppOpsManager.MODE_ALLOWED, + AppOpsManager.MODE_ALLOWED, + AppOpsManager.MODE_ALLOWED, + AppOpsManager.MODE_ALLOWED, + AppOpsManager.MODE_ALLOWED, + AppOpsManager.MODE_ALLOWED, + AppOpsManager.MODE_ALLOWED, + AppOpsManager.MODE_ALLOWED, + AppOpsManager.MODE_ALLOWED, + AppOpsManager.MODE_ALLOWED, + AppOpsManager.MODE_ALLOWED, // OP_ALARM_WAKEUP + AppOpsManager.MODE_ASK, // OP_SU + }; + + /** + * This specifies the default mode for each strict operation. + */ + + private static int[] sOpDefaultStrictMode = new int[] { + AppOpsManager.MODE_ASK, // OP_COARSE_LOCATION + AppOpsManager.MODE_ASK, // OP_FINE_LOCATION + AppOpsManager.MODE_ASK, // OP_GPS + AppOpsManager.MODE_ALLOWED, // OP_VIBRATE + AppOpsManager.MODE_ASK, // OP_READ_CONTACTS + AppOpsManager.MODE_ASK, // OP_WRITE_CONTACTS + AppOpsManager.MODE_ASK, // OP_READ_CALL_LOG + AppOpsManager.MODE_ASK, // OP_WRITE_CALL_LOG + AppOpsManager.MODE_ALLOWED, // OP_READ_CALENDAR + AppOpsManager.MODE_ALLOWED, // OP_WRITE_CALENDAR + AppOpsManager.MODE_ASK, // OP_WIFI_SCAN + AppOpsManager.MODE_ALLOWED, // OP_POST_NOTIFICATION + AppOpsManager.MODE_ALLOWED, // OP_NEIGHBORING_CELLS + AppOpsManager.MODE_ASK, // OP_CALL_PHONE + AppOpsManager.MODE_ASK, // OP_READ_SMS + AppOpsManager.MODE_ASK, // OP_WRITE_SMS + AppOpsManager.MODE_ALLOWED, // OP_RECEIVE_SMS + AppOpsManager.MODE_ALLOWED, // OP_RECEIVE_EMERGECY_SMS + AppOpsManager.MODE_ALLOWED, // OP_RECEIVE_MMS + AppOpsManager.MODE_ALLOWED, // OP_RECEIVE_WAP_PUSH + AppOpsManager.MODE_ASK, // OP_SEND_SMS + AppOpsManager.MODE_ALLOWED, // OP_READ_ICC_SMS + AppOpsManager.MODE_ALLOWED, // OP_WRITE_ICC_SMS + AppOpsManager.MODE_ALLOWED, // OP_WRITE_SETTINGS + AppOpsManager.MODE_ALLOWED, // OP_SYSTEM_ALERT_WINDOW + AppOpsManager.MODE_ALLOWED, // OP_ACCESS_NOTIFICATIONS + AppOpsManager.MODE_ASK, // OP_CAMERA + AppOpsManager.MODE_ASK, // OP_RECORD_AUDIO + AppOpsManager.MODE_ALLOWED, // OP_PLAY_AUDIO + AppOpsManager.MODE_ALLOWED, // OP_READ_CLIPBOARD + AppOpsManager.MODE_ALLOWED, // OP_WRITE_CLIPBOARD + AppOpsManager.MODE_ALLOWED, // OP_TAKE_MEDIA_BUTTONS + AppOpsManager.MODE_ALLOWED, // OP_TAKE_AUDIO_FOCUS + AppOpsManager.MODE_ALLOWED, // OP_AUDIO_MASTER_VOLUME + AppOpsManager.MODE_ALLOWED, // OP_AUDIO_VOICE_VOLUME + AppOpsManager.MODE_ALLOWED, // OP_AUDIO_RING_VOLUME + AppOpsManager.MODE_ALLOWED, // OP_AUDIO_MEDIA_VOLUME + AppOpsManager.MODE_ALLOWED, // OP_AUDIO_ALARM_VOLUME + AppOpsManager.MODE_ALLOWED, // OP_AUDIO_NOTIFICATION_VOLUME + AppOpsManager.MODE_ALLOWED, // OP_AUDIO_BLUETOOTH_VOLUME + AppOpsManager.MODE_ALLOWED, // OP_WAKE_LOCK + AppOpsManager.MODE_ALLOWED, // OP_MONITOR_LOCATION + AppOpsManager.MODE_ASK, // OP_MONITOR_HIGH_POWER_LOCATION + AppOpsManager.MODE_DEFAULT, // OP_GET_USAGE_STATS + AppOpsManager.MODE_ALLOWED, // OP_MUTE_MICROPHONE + AppOpsManager.MODE_ALLOWED, // OP_TOAST_WINDOW + AppOpsManager.MODE_IGNORED, // OP_PROJECT_MEDIA + AppOpsManager.MODE_IGNORED, // OP_ACTIVATE_VPN + AppOpsManager.MODE_ASK, // OP_WIFI_CHANGE + AppOpsManager.MODE_ASK, // OP_BLUETOOTH_CHANGE + AppOpsManager.MODE_ASK, // OP_SEND_MMS + AppOpsManager.MODE_ASK, // OP_READ_MMS + AppOpsManager.MODE_ASK, // OP_WRITE_MMS + AppOpsManager.MODE_ALLOWED, // OP_BOOT_COMPLETED + AppOpsManager.MODE_ASK, // OP_NFC_CHANGE + AppOpsManager.MODE_ASK, // OP_DELETE_SMS + AppOpsManager.MODE_ASK, // OP_DELETE_MMS + AppOpsManager.MODE_ASK, // OP_DELETE_CONTACTS + AppOpsManager.MODE_ASK, // OP_DELETE_CALL_LOG + AppOpsManager.MODE_ASK, // OP_DATA_CONNECT_CHANGE + AppOpsManager.MODE_ALLOWED, // OP_ALARM_WAKEUP + AppOpsManager.MODE_ASK, // OP_SU + }; + + /** + * This specifies if operation is in strict mode. + */ + private final static boolean[] sOpStrictMode = new boolean[] { + true, // OP_COARSE_LOCATION + true, // OP_FINE_LOCATION + true, // OP_GPS + false, // OP_VIBRATE + true, // OP_READ_CONTACTS + true, // OP_WRITE_CONTACTS + true, // OP_READ_CALL_LOG + true, // OP_WRITE_CALL_LOG + false, // OP_READ_CALENDAR + false, // OP_WRITE_CALENDAR + true, // OP_WIFI_SCAN + false, // OP_POST_NOTIFICATION + false, // OP_NEIGHBORING_CELLS + true, // OP_CALL_PHONE + true, // OP_READ_SMS + true, // OP_WRITE_SMS + false, // OP_RECEIVE_SMS + false, // OP_RECEIVE_EMERGECY_SMS + false, // OP_RECEIVE_MMS + false, // OP_RECEIVE_WAP_PUSH + true, // OP_SEND_SMS + false, // OP_READ_ICC_SMS + false, // OP_WRITE_ICC_SMS + false, // OP_WRITE_SETTINGS + false, // OP_SYSTEM_ALERT_WINDOW + false, // OP_ACCESS_NOTIFICATIONS + true, // OP_CAMERA + true, // OP_RECORD_AUDIO + false, // OP_PLAY_AUDIO + false, // OP_READ_CLIPBOARD + false, // OP_WRITE_CLIPBOARD + false, // OP_TAKE_MEDIA_BUTTONS + false, // OP_TAKE_AUDIO_FOCUS + false, // OP_AUDIO_MASTER_VOLUME + false, // OP_AUDIO_VOICE_VOLUME + false, // OP_AUDIO_RING_VOLUME + false, // OP_AUDIO_MEDIA_VOLUME + false, // OP_AUDIO_ALARM_VOLUME + false, // OP_AUDIO_NOTIFICATION_VOLUME + false, // OP_AUDIO_BLUETOOTH_VOLUME + false, // OP_WAKE_LOCK + false, // OP_MONITOR_LOCATION + true, // OP_MONITOR_HIGH_POWER_LOCATION + false, // OP_GET_USAGE_STATS + false, // OP_MUTE_MICROPHONE + false, // OP_TOAST_WINDOW + false, // OP_PROJECT_MEDIA + false, // OP_ACTIVATE_VPN + true, // OP_WIFI_CHANGE + true, // OP_BLUETOOTH_CHANGE + true, // OP_SEND_MMS + true, // OP_READ_MMS + true, // OP_WRITE_MMS + false, // OP_BOOT_COMPLETED + true, // OP_NFC_CHANGE + true, // OP_DELETE_SMS + true, // OP_DELETE_MMS + true, // OP_DELETE_CONTACTS + true, // OP_DELETE_CALL_LOG + true, // OP_DATA_CONNECT_CHANGE + false, // OP_ALARM_WAKEUP + true, // OP_SU }; /** @@ -673,9 +1135,26 @@ public class AppOpsManager { false, false, false, + false, // OP_WIFI_CHANGE + false, // OP_BLUETOOTH_CHANGE + false, // OP_SEND_MMS + false, // OP_READ_MMS + false, // OP_WRITE_MMS + false, // OP_BOOT_COMPLETED + false, // OP_NFC_CHANGE + false, // OP_DELETE_SMS + false, // OP_DELETE_MMS + false, // OP_DELETE_CONTACTS + false, // OP_DELETE_CALL_LOG + false, // OP_DATA_CONNECT_CHANGE + false, // OP_ALARM_WAKEUP + false, // OP_SU }; private static HashMap sOpStrToOp = new HashMap(); + private static HashMap sOpStringToOp = new HashMap(); + + private static HashMap sNameToOp = new HashMap(); static { if (sOpToSwitch.length != _NUM_OP) { @@ -686,6 +1165,10 @@ public class AppOpsManager { throw new IllegalStateException("sOpToString length " + sOpToString.length + " should be " + _NUM_OP); } + if (sOpToOpString.length != _NUM_OP) { + throw new IllegalStateException("sOpToOpString length " + sOpToOpString.length + + " should be " + _NUM_OP); + } if (sOpNames.length != _NUM_OP) { throw new IllegalStateException("sOpNames length " + sOpNames.length + " should be " + _NUM_OP); @@ -698,6 +1181,10 @@ public class AppOpsManager { throw new IllegalStateException("sOpDefaultMode length " + sOpDefaultMode.length + " should be " + _NUM_OP); } + if (sOpDefaultStrictMode.length != _NUM_OP) { + throw new IllegalStateException("sOpDefaultStrictMode length " + sOpDefaultStrictMode.length + + " should be " + _NUM_OP); + } if (sOpDisableReset.length != _NUM_OP) { throw new IllegalStateException("sOpDisableReset length " + sOpDisableReset.length + " should be " + _NUM_OP); @@ -710,10 +1197,20 @@ public class AppOpsManager { throw new IllegalStateException("sOpAllowSYstemRestrictionsBypass length " + sOpRestrictions.length + " should be " + _NUM_OP); } + if (sOpStrictMode.length != _NUM_OP) { + throw new IllegalStateException("sOpStrictMode length " + + sOpStrictMode.length + " should be " + _NUM_OP); + } for (int i=0; i<_NUM_OP; i++) { if (sOpToString[i] != null) { sOpStrToOp.put(sOpToString[i], i); } + if (sOpToOpString[i] != null) { + sOpStringToOp.put(sOpToOpString[i], i); + } + } + for (int i=0; i<_NUM_OP; i++) { + sNameToOp.put(sOpNames[i], i); } } @@ -746,6 +1243,15 @@ public static int strDebugOpToOp(String op) { throw new IllegalArgumentException("Unknown operation string: " + op); } + /** + * Map a non-localized name for the operation back to the Op number + * @hide + */ + public static int nameToOp(String name) { + Integer val = sNameToOp.get(name); + return val != null ? val : OP_NONE; + } + /** * Retrieve the permission associated with an operation, or null if there is not one. * @hide @@ -775,7 +1281,9 @@ public static boolean opAllowSystemBypassRestriction(int op) { * Retrieve the default mode for the operation. * @hide */ - public static int opToDefaultMode(int op) { + public static int opToDefaultMode(int op, boolean isStrict) { + if (isStrict) + return sOpDefaultStrictMode[op]; return sOpDefaultMode[op]; } @@ -860,13 +1368,18 @@ public static class OpEntry implements Parcelable { private final long mTime; private final long mRejectTime; private final int mDuration; + private final int mAllowedCount; + private final int mIgnoredCount; - public OpEntry(int op, int mode, long time, long rejectTime, int duration) { + public OpEntry(int op, int mode, long time, long rejectTime, int duration, + int allowedCount, int ignoredCount) { mOp = op; mMode = mode; mTime = time; mRejectTime = rejectTime; mDuration = duration; + mAllowedCount = allowedCount; + mIgnoredCount = ignoredCount; } public int getOp() { @@ -893,6 +1406,14 @@ public int getDuration() { return mDuration == -1 ? (int)(System.currentTimeMillis()-mTime) : mDuration; } + public int getAllowedCount() { + return mAllowedCount; + } + + public int getIgnoredCount() { + return mIgnoredCount; + } + @Override public int describeContents() { return 0; @@ -905,6 +1426,8 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeLong(mTime); dest.writeLong(mRejectTime); dest.writeInt(mDuration); + dest.writeInt(mAllowedCount); + dest.writeInt(mIgnoredCount); } OpEntry(Parcel source) { @@ -913,6 +1436,8 @@ public void writeToParcel(Parcel dest, int flags) { mTime = source.readLong(); mRejectTime = source.readLong(); mDuration = source.readInt(); + mAllowedCount = source.readInt(); + mIgnoredCount = source.readInt(); } public static final Creator CREATOR = new Creator() { @@ -1391,4 +1916,75 @@ public void finishOp(int op, int uid, String packageName) { public void finishOp(int op) { finishOp(op, Process.myUid(), mContext.getOpPackageName()); } + + /** @hide */ + public static boolean isStrictEnable() { + return SystemProperties.getBoolean("persist.sys.strict_op_enable", false); + } + + /** + * Check if op in strict mode + * @hide + */ + public static boolean isStrictOp(int code) { + return sOpStrictMode[code]; + } + + + /** @hide */ + public static int stringToMode(String permission) { + if ("allowed".equalsIgnoreCase(permission)) { + return AppOpsManager.MODE_ALLOWED; + } else if ("ignored".equalsIgnoreCase(permission)) { + return AppOpsManager.MODE_IGNORED; + } else if ("ask".equalsIgnoreCase(permission)) { + return AppOpsManager.MODE_ASK; + } + return AppOpsManager.MODE_ERRORED; + } + + /** @hide */ + public static int stringOpToOp (String op) { + Integer val = sOpStringToOp.get(op); + if (val == null) { + val = OP_NONE; + } + return val; + } + + /** @hide */ + public boolean isControlAllowed(int op, String packageName) { + boolean isShow = true; + try { + isShow = mService.isControlAllowed(op, packageName); + } catch (RemoteException e) { + } + return isShow; + } + + /** @hide */ + public boolean getPrivacyGuardSettingForPackage(int uid, String packageName) { + try { + return mService.getPrivacyGuardSettingForPackage(uid, packageName); + } catch (RemoteException e) { + } + return false; + } + + /** @hide */ + public void setPrivacyGuardSettingForPackage(int uid, String packageName, + boolean state) { + try { + mService.setPrivacyGuardSettingForPackage(uid, packageName, state); + } catch (RemoteException e) { + } + } + + /** @hide */ + public void resetCounters() { + try { + mService.resetCounters(); + } catch (RemoteException e) { + } + } } diff --git a/core/java/android/app/ApplicationErrorReport.java b/core/java/android/app/ApplicationErrorReport.java index 6c2511eef35..8692336439f 100644 --- a/core/java/android/app/ApplicationErrorReport.java +++ b/core/java/android/app/ApplicationErrorReport.java @@ -235,10 +235,13 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeString(processName); dest.writeLong(time); dest.writeInt(systemApp ? 1 : 0); + dest.writeInt(crashInfo != null ? 1 : 0); switch (type) { case TYPE_CRASH: - crashInfo.writeToParcel(dest, flags); + if (crashInfo != null) { + crashInfo.writeToParcel(dest, flags); + } break; case TYPE_ANR: anrInfo.writeToParcel(dest, flags); @@ -259,10 +262,11 @@ public void readFromParcel(Parcel in) { processName = in.readString(); time = in.readLong(); systemApp = in.readInt() == 1; + boolean hasCrashInfo = in.readInt() == 1; switch (type) { case TYPE_CRASH: - crashInfo = new CrashInfo(in); + crashInfo = hasCrashInfo ? new CrashInfo(in) : null; anrInfo = null; batteryInfo = null; runningServiceInfo = null; diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index 967e97e5af6..1a4e2acbeb3 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -18,6 +18,7 @@ import android.content.ComponentName; import android.content.ContentResolver; +import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.IntentSender; @@ -925,12 +926,13 @@ public CharSequence getUserBadgedLabel(CharSequence label, UserHandle user) { if (app.packageName.equals("system")) { return mContext.mMainThread.getSystemContext().getResources(); } + final boolean sameUid = (app.uid == Process.myUid()); Resources r = mContext.mMainThread.getTopLevelResources( sameUid ? app.sourceDir : app.publicSourceDir, sameUid ? app.splitSourceDirs : app.splitPublicSourceDirs, app.resourceDirs, app.sharedLibraryFiles, Display.DEFAULT_DISPLAY, - null, mContext.mPackageInfo); + null, mContext.mPackageInfo, mContext, app.packageName); if (r != null) { return r; } @@ -965,6 +967,48 @@ public Resources getResourcesForApplicationAsUser(String appPackageName, int use throw new NameNotFoundException("Package " + appPackageName + " doesn't exist"); } + /** @hide */ + @Override public Resources getThemedResourcesForApplication( + ApplicationInfo app, String themePkgName) throws NameNotFoundException { + if (app.packageName.equals("system")) { + return mContext.mMainThread.getSystemContext().getResources(); + } + + Resources r = mContext.mMainThread.getTopLevelThemedResources( + app.uid == Process.myUid() ? app.sourceDir : app.publicSourceDir, + Display.DEFAULT_DISPLAY, mContext.mPackageInfo, app.packageName, themePkgName); + if (r != null) { + return r; + } + throw new NameNotFoundException("Unable to open " + app.publicSourceDir); + } + + /** @hide */ + @Override public Resources getThemedResourcesForApplication( + String appPackageName, String themePkgName) throws NameNotFoundException { + return getThemedResourcesForApplication( + getApplicationInfo(appPackageName, 0), themePkgName); + } + + /** @hide */ + @Override + public Resources getThemedResourcesForApplicationAsUser(String appPackageName, + String themePackageName, int userId) throws NameNotFoundException { + if (userId < 0) { + throw new IllegalArgumentException( + "Call does not support special user #" + userId); + } + try { + ApplicationInfo ai = mPM.getApplicationInfo(appPackageName, 0, userId); + if (ai != null) { + return getThemedResourcesForApplication(ai, themePackageName); + } + } catch (RemoteException e) { + throw new RuntimeException("Package manager has died", e); + } + throw new NameNotFoundException("Package " + appPackageName + " doesn't exist"); + } + int mCachedSafeMode = -1; @Override public boolean isSafeMode() { try { @@ -1621,6 +1665,15 @@ public boolean isUpgrade() { } } + @Override + public void setComponentProtectedSetting(ComponentName componentName, boolean newState) { + try { + mPM.setComponentProtectedSetting(componentName, newState, mContext.getUserId()); + } catch (RemoteException re) { + Log.e(TAG, "Failed to set component protected setting", re); + } + } + @Override public PackageInstaller getPackageInstaller() { synchronized (mLock) { @@ -1768,9 +1821,11 @@ private int getBadgeResIdForUser(int userHandle) { private UserInfo getUserIfProfile(int userHandle) { List userProfiles = getUserManager().getProfiles(UserHandle.myUserId()); - for (UserInfo user : userProfiles) { - if (user.id == userHandle) { - return user; + if (userProfiles != null) { + for (UserInfo user : userProfiles) { + if (user.id == userHandle) { + return user; + } } } return null; @@ -1784,4 +1839,30 @@ private UserInfo getUserIfProfile(int userHandle) { = new ArrayMap>(); private static ArrayMap> sStringCache = new ArrayMap>(); + + /** + * @hide + */ + @Override + public void updateIconMaps(String pkgName) { + try { + mPM.updateIconMapping(pkgName); + } catch (RemoteException re) { + Log.e(TAG, "Failed to update icon maps", re); + } + } + + /** + * @hide + */ + @Override + public int processThemeResources(String themePkgName) { + try { + return mPM.processThemeResources(themePkgName); + } catch (RemoteException e) { + Log.e(TAG, "Unable to process theme resources for " + themePkgName, e); + } + + return 0; + } } diff --git a/core/java/android/app/ComposedIconInfo.aidl b/core/java/android/app/ComposedIconInfo.aidl new file mode 100644 index 00000000000..8a1bab52d8c --- /dev/null +++ b/core/java/android/app/ComposedIconInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2014 The CyanogenMod 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.app; + +/** @hide */ +parcelable ComposedIconInfo; diff --git a/core/java/android/app/ComposedIconInfo.java b/core/java/android/app/ComposedIconInfo.java new file mode 100644 index 00000000000..8a8ad034fe6 --- /dev/null +++ b/core/java/android/app/ComposedIconInfo.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2014 The CyanogenMod 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.app; + +import android.os.Parcel; +import android.os.Parcelable; + +/** @hide */ +public class ComposedIconInfo implements Parcelable { + public int iconUpon, iconMask; + public int[] iconBacks; + public float iconScale; + public float iconRotation; + // value used to provide some randomization to the angle of rotation + public float iconRotationVariance; + public float iconTranslationX; + public float iconTranslationY; + public int iconDensity; + public int iconSize; + public float[] colorFilter; + + // Palettized background items + public int iconPaletteBack; + public SwatchType swatchType; + public int[] defaultSwatchColors; + + public ComposedIconInfo() { + super(); + iconPaletteBack = 0; + swatchType = SwatchType.None; + iconRotation = 0; + iconTranslationX = 0; + iconTranslationY = 0; + } + + private ComposedIconInfo(Parcel source) { + iconScale = source.readFloat(); + iconRotation = source.readFloat(); + iconRotationVariance = source.readFloat(); + iconTranslationX = source.readFloat(); + iconTranslationY = source.readFloat(); + iconDensity = source.readInt(); + iconSize = source.readInt(); + int backCount = source.readInt(); + if (backCount > 0) { + iconBacks = new int[backCount]; + for (int i = 0; i < backCount; i++) { + iconBacks[i] = source.readInt(); + } + } + iconMask = source.readInt(); + iconUpon = source.readInt(); + int colorFilterSize = source.readInt(); + if (colorFilterSize > 0) { + colorFilter = new float[colorFilterSize]; + for (int i = 0; i < colorFilterSize; i++) { + colorFilter[i] = source.readFloat(); + } + } + iconPaletteBack = source.readInt(); + swatchType = SwatchType.values()[source.readInt()]; + int numDefaultColors = source.readInt(); + if (numDefaultColors > 0) { + defaultSwatchColors = new int[numDefaultColors]; + for (int i = 0; i < numDefaultColors; i++) { + defaultSwatchColors[i] = source.readInt(); + } + } + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeFloat(iconScale); + dest.writeFloat(iconRotation); + dest.writeFloat(iconRotationVariance); + dest.writeFloat(iconTranslationX); + dest.writeFloat(iconTranslationY); + dest.writeInt(iconDensity); + dest.writeInt(iconSize); + dest.writeInt(iconBacks != null ? iconBacks.length : 0); + if (iconBacks != null) { + for (int resId : iconBacks) { + dest.writeInt(resId); + } + } + dest.writeInt(iconMask); + dest.writeInt(iconUpon); + if (colorFilter != null) { + dest.writeInt(colorFilter.length); + for (float val : colorFilter) { + dest.writeFloat(val); + } + } else { + dest.writeInt(0); + } + dest.writeInt(iconPaletteBack); + dest.writeInt(swatchType.ordinal()); + if (defaultSwatchColors != null) { + dest.writeInt(defaultSwatchColors.length); + for (int color : defaultSwatchColors) { + dest.writeInt(color); + } + } else { + dest.writeInt(0); + } + } + + public static final Creator CREATOR + = new Creator() { + @Override + public ComposedIconInfo createFromParcel(Parcel source) { + return new ComposedIconInfo(source); + } + + @Override + public ComposedIconInfo[] newArray(int size) { + return new ComposedIconInfo[0]; + } + }; + + public enum SwatchType { + None, + Vibrant, + VibrantLight, + VibrantDark, + Muted, + MutedLight, + MutedDark + } +} diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 2ef046d8e1f..645de58ebd0 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -19,6 +19,10 @@ import android.app.usage.IUsageStatsManager; import android.app.usage.UsageStatsManager; import android.appwidget.AppWidgetManager; +import android.content.res.IThemeService; +import android.content.res.ThemeManager; +import android.hardware.ITorchService; +import android.hardware.TorchManager; import android.os.Build; import android.service.persistentdata.IPersistentDataBlockService; import android.service.persistentdata.PersistentDataBlockManager; @@ -431,7 +435,9 @@ public Object createService(ContextImpl ctx) { registerService(BATTERY_SERVICE, new ServiceFetcher() { public Object createService(ContextImpl ctx) { - return new BatteryManager(); + IBinder b = ServiceManager.getService(BATTERY_SERVICE); + IBatteryService service = IBatteryService.Stub.asInterface(b); + return new BatteryManager(service); }}); registerService(NFC_SERVICE, new ServiceFetcher() { @@ -768,6 +774,22 @@ public Object createService(ContextImpl ctx) { IBinder b = ServiceManager.getService(APPWIDGET_SERVICE); return new AppWidgetManager(ctx, IAppWidgetService.Stub.asInterface(b)); }}); + + registerService(THEME_SERVICE, new ServiceFetcher() { + public Object createService(ContextImpl ctx) { + IBinder b = ServiceManager.getService(THEME_SERVICE); + IThemeService service = IThemeService.Stub.asInterface(b); + return new ThemeManager(ctx.getOuterContext(), + service); + }}); + + registerService(TORCH_SERVICE, new ServiceFetcher() { + public Object createService(ContextImpl ctx) { + IBinder b = ServiceManager.getService(TORCH_SERVICE); + ITorchService service = ITorchService.Stub.asInterface(b); + final Context outerContext = ctx.getOuterContext(); + return new TorchManager(outerContext, service); + }}); } static ContextImpl getImpl(Context context) { @@ -846,6 +868,13 @@ public Resources.Theme getTheme() { return mTheme; } + @Override + public void recreateTheme() { + Resources.Theme newTheme = mResources.newTheme(); + newTheme.applyStyle(mThemeResource, true); + mTheme.setTo(newTheme); + } + @Override public ClassLoader getClassLoader() { return mPackageInfo != null ? @@ -2126,13 +2155,19 @@ private void warnIfCallingFromSystemProcess() { @Override public Context createApplicationContext(ApplicationInfo application, int flags) throws NameNotFoundException { + return createApplicationContext(application, null, flags); + } + + @Override + public Context createApplicationContext(ApplicationInfo application, String themePackageName, + int flags) throws NameNotFoundException { LoadedApk pi = mMainThread.getPackageInfo(application, mResources.getCompatibilityInfo(), flags | CONTEXT_REGISTER_PACKAGE); if (pi != null) { final boolean restricted = (flags & CONTEXT_RESTRICTED) == CONTEXT_RESTRICTED; ContextImpl c = new ContextImpl(this, mMainThread, pi, mActivityToken, new UserHandle(UserHandle.getUserId(application.uid)), restricted, - mDisplay, mOverrideConfiguration); + mDisplay, mOverrideConfiguration, themePackageName); if (c.mResources != null) { return c; } @@ -2145,24 +2180,30 @@ public Context createApplicationContext(ApplicationInfo application, int flags) @Override public Context createPackageContext(String packageName, int flags) throws NameNotFoundException { - return createPackageContextAsUser(packageName, flags, + return createPackageContextAsUser(packageName, null, flags, mUser != null ? mUser : Process.myUserHandle()); } @Override public Context createPackageContextAsUser(String packageName, int flags, UserHandle user) throws NameNotFoundException { + return createPackageContextAsUser(packageName, null, flags, user); + } + + @Override + public Context createPackageContextAsUser(String packageName, String themePackageName, + int flags, UserHandle user) throws NameNotFoundException { final boolean restricted = (flags & CONTEXT_RESTRICTED) == CONTEXT_RESTRICTED; if (packageName.equals("system") || packageName.equals("android")) { return new ContextImpl(this, mMainThread, mPackageInfo, mActivityToken, - user, restricted, mDisplay, mOverrideConfiguration); + user, restricted, mDisplay, mOverrideConfiguration, themePackageName); } LoadedApk pi = mMainThread.getPackageInfo(packageName, mResources.getCompatibilityInfo(), flags | CONTEXT_REGISTER_PACKAGE, user.getIdentifier()); if (pi != null) { ContextImpl c = new ContextImpl(this, mMainThread, pi, mActivityToken, - user, restricted, mDisplay, mOverrideConfiguration); + user, restricted, mDisplay, mOverrideConfiguration, themePackageName); if (c.mResources != null) { return c; } @@ -2234,7 +2275,7 @@ public int getUserId() { static ContextImpl createSystemContext(ActivityThread mainThread) { LoadedApk packageInfo = new LoadedApk(mainThread); ContextImpl context = new ContextImpl(null, mainThread, - packageInfo, null, null, false, null, null); + packageInfo, null, null, false, null, null, null); context.mResources.updateConfiguration(context.mResourcesManager.getConfiguration(), context.mResourcesManager.getDisplayMetricsLocked(Display.DEFAULT_DISPLAY)); return context; @@ -2243,7 +2284,7 @@ static ContextImpl createSystemContext(ActivityThread mainThread) { static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) { if (packageInfo == null) throw new IllegalArgumentException("packageInfo"); return new ContextImpl(null, mainThread, - packageInfo, null, null, false, null, null); + packageInfo, null, null, false, null, null, null); } static ContextImpl createActivityContext(ActivityThread mainThread, @@ -2251,12 +2292,19 @@ static ContextImpl createActivityContext(ActivityThread mainThread, if (packageInfo == null) throw new IllegalArgumentException("packageInfo"); if (activityToken == null) throw new IllegalArgumentException("activityInfo"); return new ContextImpl(null, mainThread, - packageInfo, activityToken, null, false, null, null); + packageInfo, activityToken, null, false, null, null, null); } private ContextImpl(ContextImpl container, ActivityThread mainThread, LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted, Display display, Configuration overrideConfiguration) { + this(container, mainThread, packageInfo, activityToken, user, restricted, display, + overrideConfiguration, null); + } + + private ContextImpl(ContextImpl container, ActivityThread mainThread, + LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted, + Display display, Configuration overrideConfiguration, String themePackageName) { mOuterContext = this; mMainThread = mainThread; @@ -2286,15 +2334,20 @@ private ContextImpl(ContextImpl container, ActivityThread mainThread, Resources resources = packageInfo.getResources(mainThread); if (resources != null) { - if (activityToken != null + if (activityToken != null || themePackageName != null || displayId != Display.DEFAULT_DISPLAY || overrideConfiguration != null || (compatInfo != null && compatInfo.applicationScale != resources.getCompatibilityInfo().applicationScale)) { - resources = mResourcesManager.getTopLevelResources(packageInfo.getResDir(), - packageInfo.getSplitResDirs(), packageInfo.getOverlayDirs(), + resources = themePackageName == null ? mResourcesManager.getTopLevelResources( + packageInfo.getResDir(), packageInfo.getSplitResDirs(), + packageInfo.getOverlayDirs(), packageInfo.getApplicationInfo().sharedLibraryFiles, displayId, - overrideConfiguration, compatInfo, activityToken); + packageInfo.getAppDir(), overrideConfiguration, compatInfo, activityToken, + mOuterContext, packageInfo.getApplicationInfo().isThemeable) : + mResourcesManager.getTopLevelThemedResources(packageInfo.getResDir(), displayId, + packageInfo.getPackageName(), themePackageName, compatInfo, activityToken, + packageInfo.getApplicationInfo().isThemeable); } } mResources = resources; @@ -2405,9 +2458,12 @@ private File makeFilename(File base, String name) { * unable to create, they are filtered by replacing with {@code null}. */ private File[] ensureDirsExistOrFilter(File[] dirs) { - File[] result = new File[dirs.length]; + ArrayList result = new ArrayList(dirs.length); for (int i = 0; i < dirs.length; i++) { File dir = dirs[i]; + if (Environment.MEDIA_REMOVED.equals(Environment.getStorageState(dir))) { + continue; + } if (!dir.exists()) { if (!dir.mkdirs()) { // recheck existence in case of cross-process race @@ -2428,9 +2484,9 @@ private File[] ensureDirsExistOrFilter(File[] dirs) { } } } - result[i] = dir; + result.add(dir); } - return result; + return result.toArray(new File[result.size()]); } // ---------------------------------------------------------------------- diff --git a/core/java/android/app/DownloadManager.java b/core/java/android/app/DownloadManager.java old mode 100644 new mode 100755 index 2ed8b0fbe24..4456de83dfd --- a/core/java/android/app/DownloadManager.java +++ b/core/java/android/app/DownloadManager.java @@ -266,6 +266,13 @@ public class DownloadManager { */ public final static int PAUSED_UNKNOWN = 4; + /** + * Value of {@link #COLUMN_REASON} when the download is paused by user. + * + * @hide + */ + public final static int PAUSED_BY_APP = 5; + /** * Broadcast intent action sent by the download manager when a download completes. */ @@ -1120,6 +1127,30 @@ public void restartDownload(long... ids) { mResolver.update(mBaseUri, values, getWhereClauseForIds(ids), getWhereArgsForIds(ids)); } + /** + * Pause the given running download by user. + * + * @param id the ID of the download to be paused + * @hide + */ + public void pauseDownload(long id) { + ContentValues values = new ContentValues(); + values.put(Downloads.Impl.COLUMN_CONTROL, Downloads.Impl.CONTROL_PAUSED); + mResolver.update(ContentUris.withAppendedId(mBaseUri, id), values, null, null); + } + + /** + * Resume the given paused download by user. + * + * @param id the ID of the download to be resumed + * @hide + */ + public void resumeDownload(long id) { + ContentValues values = new ContentValues(); + values.put(Downloads.Impl.COLUMN_CONTROL, Downloads.Impl.CONTROL_RUN); + mResolver.update(ContentUris.withAppendedId(mBaseUri, id), values, null, null); + } + /** * Returns maximum size, in bytes, of downloads that may go over a mobile connection; or null if * there's no limit @@ -1356,6 +1387,9 @@ private long getPausedReason(int status) { case Downloads.Impl.STATUS_QUEUED_FOR_WIFI: return PAUSED_QUEUED_FOR_WIFI; + case Downloads.Impl.STATUS_PAUSED_BY_APP: + return PAUSED_BY_APP; + default: return PAUSED_UNKNOWN; } diff --git a/core/java/android/app/IActivityManager.java b/core/java/android/app/IActivityManager.java index e505d69078e..75943c20db4 100644 --- a/core/java/android/app/IActivityManager.java +++ b/core/java/android/app/IActivityManager.java @@ -106,7 +106,8 @@ public int broadcastIntent(IApplicationThread caller, Intent intent, String resultData, Bundle map, String requiredPermission, int appOp, boolean serialized, boolean sticky, int userId) throws RemoteException; public void unbroadcastIntent(IApplicationThread caller, Intent intent, int userId) throws RemoteException; - public void finishReceiver(IBinder who, int resultCode, String resultData, Bundle map, boolean abortBroadcast) throws RemoteException; + public void finishReceiver(IBinder who, int resultCode, String resultData, Bundle map, + boolean abortBroadcast, int flags) throws RemoteException; public void attachApplication(IApplicationThread app) throws RemoteException; public void activityResumed(IBinder token) throws RemoteException; public void activityIdle(IBinder token, Configuration config, @@ -117,6 +118,7 @@ public void activityStopped(IBinder token, Bundle state, public void activitySlept(IBinder token) throws RemoteException; public void activityDestroyed(IBinder token) throws RemoteException; public String getCallingPackage(IBinder token) throws RemoteException; + public String getCallingPackageForBroadcast(boolean foreground) throws RemoteException; public ComponentName getCallingActivity(IBinder token) throws RemoteException; public List getAppTasks(String callingPackage) throws RemoteException; public int addAppTask(IBinder activityToken, Intent intent, @@ -141,6 +143,7 @@ public List getProcessesInErrorState() public void setFocusedStack(int stackId) throws RemoteException; public void registerTaskStackListener(ITaskStackListener listener) throws RemoteException; public int getTaskForActivity(IBinder token, boolean onlyRoot) throws RemoteException; + public IBinder getActivityForTask(int task, boolean onlyRoot) throws RemoteException; public ContentProviderHolder getContentProvider(IApplicationThread caller, String name, int userId, boolean stable) throws RemoteException; public ContentProviderHolder getContentProviderExternal(String name, int userId, IBinder token) @@ -438,8 +441,7 @@ public IActivityContainer createActivityContainer(IBinder parentActivityToken, public void deleteActivityContainer(IActivityContainer container) throws RemoteException; - public IActivityContainer getEnclosingActivityContainer(IBinder activityToken) - throws RemoteException; + public int getActivityDisplayId(IBinder activityToken) throws RemoteException; public IBinder getHomeActivityToken() throws RemoteException; @@ -754,10 +756,10 @@ private WaitResult(Parcel source) { int GET_PERSISTED_URI_PERMISSIONS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+181; int APP_NOT_RESPONDING_VIA_PROVIDER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+182; int GET_HOME_ACTIVITY_TOKEN_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+183; - int GET_ACTIVITY_CONTAINER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+184; + int GET_ACTIVITY_DISPLAY_ID_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+184; int DELETE_ACTIVITY_CONTAINER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+185; - - + /* FLOAT VIEW */ + int GET_ACTIVITY_FOR_TASK_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+200; // Start of L transactions int GET_TAG_FOR_INTENT_SENDER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+210; int START_USER_IN_BACKGROUND_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+211; @@ -793,4 +795,5 @@ private WaitResult(Parcel source) { int CHECK_PERMISSION_WITH_TOKEN_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+241; int REGISTER_TASK_STACK_LISTENER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+242; int SYSTEM_BACKUP_RESTORED = IBinder.FIRST_CALL_TRANSACTION+243; + int GET_CALLING_PACKAGE_FOR_BROADCAST_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+244; } diff --git a/core/java/android/app/IAlarmManager.aidl b/core/java/android/app/IAlarmManager.aidl index 194082e2d9f..a2bc417e6b7 100644 --- a/core/java/android/app/IAlarmManager.aidl +++ b/core/java/android/app/IAlarmManager.aidl @@ -34,6 +34,8 @@ interface IAlarmManager { void setTimeZone(String zone); void remove(in PendingIntent operation); AlarmManager.AlarmClockInfo getNextAlarmClock(int userId); + // update the uids being synchronized by network socket request manager + void updateBlockedUids(int uid, boolean isBlocked); } diff --git a/core/java/android/app/IBackupAgent.aidl b/core/java/android/app/IBackupAgent.aidl index 451af99010b..398bd13f5b8 100644 --- a/core/java/android/app/IBackupAgent.aidl +++ b/core/java/android/app/IBackupAgent.aidl @@ -147,4 +147,28 @@ oneway interface IBackupAgent { * @param message The message to be passed to the agent's application in an exception. */ void fail(String message); + + /** + * Perform a backup for all the files specified by domainTokens parameter. The output file + * should be a socket or other non-seekable, write-only data sink. When this method is + * called, the app should write all of its files to the output. + * + * @param data Write-only file to receive the backed-up content stream. + * The data must be formatted correctly for the resulting archive to be + * legitimate and restorable. + * + * @param token Opaque token identifying this transaction. This must + * be echoed back to the backup service binder once the agent + * completes restoring the application based on the restore data + * contents. + * + * @param domainTokens File paths of the files (represented as domains) to include. + * + * @param excludeFilesRegex Files to exclude specified as a Java-compatible pattern. + * + * @param callbackBinder Binder on which to indicate operation completion, + * passed here as a convenience to the agent. + */ + void doBackupFiles(in ParcelFileDescriptor data, int token, in String[] domainTokens, + in String excludeFilesRegex, IBackupManager callbackBinder); } diff --git a/core/java/android/app/IBatteryService.aidl b/core/java/android/app/IBatteryService.aidl new file mode 100644 index 00000000000..45270199be9 --- /dev/null +++ b/core/java/android/app/IBatteryService.aidl @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2015, The CyanogenMod 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.app; + +/** + * System private API for talking with the battery service. + * + * {@hide} + */ +interface IBatteryService { + boolean isDockBatterySupported(); +} diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index 88b9080a2dc..1e4bbde7f7f 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -51,6 +51,9 @@ interface INotificationManager void setPackageVisibilityOverride(String pkg, int uid, int visibility); int getPackageVisibilityOverride(String pkg, int uid); + void setShowNotificationForPackageOnKeyguard(String pkg, int uid, int status); + int getShowNotificationForPackageOnKeyguard(String pkg, int uid); + // TODO: Remove this when callers have been migrated to the equivalent // INotificationListener method. StatusBarNotification[] getActiveNotifications(String callingPkg); diff --git a/core/java/android/app/IUiModeManager.aidl b/core/java/android/app/IUiModeManager.aidl index 7e9873e5f2f..817354d0f33 100644 --- a/core/java/android/app/IUiModeManager.aidl +++ b/core/java/android/app/IUiModeManager.aidl @@ -51,4 +51,16 @@ interface IUiModeManager { * 2 for night, and 3 for automatic mode switching. */ int getNightMode(); + + /** + * Sets the system ui theme mode. Only the system can do this. + * + * possible theme modes @link Configuration + * {@link #UI_THEME_MODE_NORMAL}, + * {@link #UI_THEME_MODE_HOLO_DARK}, {@link #UI_THEME_MODE_HOLO_LIGHT} + * + * @hide + */ + void setUiThemeMode(int mode); + } diff --git a/core/java/android/app/IWallpaperManager.aidl b/core/java/android/app/IWallpaperManager.aidl index 3b5900b4a5d..3117c2d4388 100644 --- a/core/java/android/app/IWallpaperManager.aidl +++ b/core/java/android/app/IWallpaperManager.aidl @@ -30,6 +30,12 @@ interface IWallpaperManager { * Set the wallpaper. */ ParcelFileDescriptor setWallpaper(String name); + + /** + * Set the keyguard wallpaper. + * @hide + */ + ParcelFileDescriptor setKeyguardWallpaper(String name); /** * Set the live wallpaper. @@ -41,6 +47,13 @@ interface IWallpaperManager { */ ParcelFileDescriptor getWallpaper(IWallpaperManagerCallback cb, out Bundle outParams); + + /** + * Get the keyguard wallpaper. + * @hide + */ + ParcelFileDescriptor getKeyguardWallpaper(IWallpaperManagerCallback cb, + out Bundle outParams); /** * Get information about a live wallpaper. @@ -52,6 +65,12 @@ interface IWallpaperManager { */ void clearWallpaper(); + /* + * Clear the keyguard wallpaper. + * @hide + */ + void clearKeyguardWallpaper(); + /** * Return whether there is a wallpaper set with the given name. */ diff --git a/core/java/android/app/IWallpaperManagerCallback.aidl b/core/java/android/app/IWallpaperManagerCallback.aidl index 991b2bc924b..b217318291d 100644 --- a/core/java/android/app/IWallpaperManagerCallback.aidl +++ b/core/java/android/app/IWallpaperManagerCallback.aidl @@ -28,4 +28,9 @@ oneway interface IWallpaperManagerCallback { * Called when the wallpaper has changed */ void onWallpaperChanged(); + + /** + * Called when the keygaurd wallpaper has changed + */ + void onKeyguardWallpaperChanged(); } diff --git a/core/java/android/app/IconPackHelper.java b/core/java/android/app/IconPackHelper.java new file mode 100644 index 00000000000..627330fc187 --- /dev/null +++ b/core/java/android/app/IconPackHelper.java @@ -0,0 +1,1057 @@ +/* + * Copyright (C) 2014 The CyanogenMod 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.app; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +import android.content.pm.PackageInfo; +import android.content.res.IThemeService; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; +import android.graphics.Paint; +import android.graphics.PaintFlagsDrawFilter; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.PaintDrawable; +import android.graphics.drawable.VectorDrawable; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.Log; +import android.util.TypedValue; +import com.android.internal.util.cm.palette.Palette; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ThemeUtils; +import android.content.res.AssetManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.XmlResourceParser; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.util.DisplayMetrics; + +/** @hide */ +public class IconPackHelper { + private static final String TAG = IconPackHelper.class.getSimpleName(); + + private static final boolean DEBUG = false; + + private static final String ICON_MASK_TAG = "iconmask"; + private static final String ICON_BACK_TAG = "iconback"; + private static final String ICON_UPON_TAG = "iconupon"; + private static final String ICON_SCALE_TAG = "scale"; + private static final String ICON_ROTATE_TAG = "rotate"; + private static final String ICON_TRANSLATE_TAG = "translate"; + private static final String ICON_BACK_FORMAT = "iconback%d"; + + // Palettized icon background constants + private static final String ICON_PALETTIZED_BACK_TAG = "paletteback"; + private static final String IMG_ATTR = "img"; + private static final String SWATCH_TYPE_ATTR = "swatchType"; + private static final String DEFAULT_SWATCH_COLOR_ATTR = "defaultSwatchColor"; + private static final String VIBRANT_VALUE = "vibrant"; + private static final String VIBRANT_LIGHT_VALUE = "vibrantLight"; + private static final String VIBRANT_DARK_VALUE = "vibrantDark"; + private static final String MUTED_VALUE = "muted"; + private static final String MUTED_LIGHT_VALUE = "mutedLight"; + private static final String MUTED_DARK_VALUE = "mutedDark"; + private static final int NUM_PALETTE_COLORS = 32; + + // Rotation and translation constants + private static final String ANGLE_ATTR = "angle"; + private static final String ANGLE_VARIANCE = "plusMinus"; + private static final String TRANSLATE_X_ATTR = "xOffset"; + private static final String TRANSLATE_Y_ATTR = "yOffset"; + + private static final ComponentName ICON_BACK_COMPONENT; + private static final ComponentName ICON_MASK_COMPONENT; + private static final ComponentName ICON_UPON_COMPONENT; + private static final ComponentName ICON_SCALE_COMPONENT; + + private static final float DEFAULT_SCALE = 1.0f; + private static final int COMPOSED_ICON_COOKIE = 128; + + private final Context mContext; + private Map mIconPackResourceMap; + private String mLoadedIconPackName; + private Resources mLoadedIconPackResource; + private ComposedIconInfo mComposedIconInfo; + private int mIconBackCount = 0; + private ColorFilterUtils.Builder mFilterBuilder; + + static { + ICON_BACK_COMPONENT = new ComponentName(ICON_BACK_TAG, ""); + ICON_MASK_COMPONENT = new ComponentName(ICON_MASK_TAG, ""); + ICON_UPON_COMPONENT = new ComponentName(ICON_UPON_TAG, ""); + ICON_SCALE_COMPONENT = new ComponentName(ICON_SCALE_TAG, ""); + } + + public IconPackHelper(Context context) { + mContext = context; + mIconPackResourceMap = new HashMap(); + ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + mComposedIconInfo = new ComposedIconInfo(); + mComposedIconInfo.iconSize = am.getLauncherLargeIconSize(); + mComposedIconInfo.iconDensity = am.getLauncherLargeIconDensity(); + mFilterBuilder = new ColorFilterUtils.Builder(); + } + + private void loadResourcesFromXmlParser(XmlPullParser parser, + Map iconPackResources) + throws XmlPullParserException, IOException { + mIconBackCount = 0; + int eventType = parser.getEventType(); + do { + + if (eventType != XmlPullParser.START_TAG) { + continue; + } + + if (parseComposedIconComponent(parser, iconPackResources)) { + continue; + } + + if (ColorFilterUtils.parseIconFilter(parser, mFilterBuilder)) { + continue; + } + + if (parser.getName().equalsIgnoreCase(ICON_SCALE_TAG)) { + String factor = parser.getAttributeValue(null, "factor"); + if (factor == null) { + if (parser.getAttributeCount() == 1) { + factor = parser.getAttributeValue(0); + } + } + iconPackResources.put(ICON_SCALE_COMPONENT, factor); + continue; + } + + if (parseRotationComponent(parser, mComposedIconInfo)) { + continue; + } + + if (parseTranslationComponent(parser, mComposedIconInfo)) { + continue; + } + + if (!parser.getName().equalsIgnoreCase("item")) { + continue; + } + + String component = parser.getAttributeValue(null, "component"); + String drawable = parser.getAttributeValue(null, "drawable"); + + // Validate component/drawable exist + if (TextUtils.isEmpty(component) || TextUtils.isEmpty(drawable)) { + continue; + } + + // Validate format/length of component + if (!component.startsWith("ComponentInfo{") || !component.endsWith("}") + || component.length() < 16 || drawable.length() == 0) { + continue; + } + + // Sanitize stored value + component = component.substring(14, component.length() - 1).toLowerCase(); + + ComponentName name = null; + if (!component.contains("/")) { + // Package icon reference + name = new ComponentName(component.toLowerCase(), ""); + } else { + name = ComponentName.unflattenFromString(component); + } + + if (name != null) { + iconPackResources.put(name, drawable); + } + } while ((eventType = parser.next()) != XmlPullParser.END_DOCUMENT); + } + + private boolean isComposedIconComponent(String tag) { + return ICON_MASK_TAG.equalsIgnoreCase(tag) || + ICON_BACK_TAG.equalsIgnoreCase(tag) || + ICON_UPON_TAG.equalsIgnoreCase(tag) || + ICON_PALETTIZED_BACK_TAG.equalsIgnoreCase(tag); + } + + private boolean parseComposedIconComponent(XmlPullParser parser, + Map iconPackResources) { + String icon; + String tag = parser.getName(); + if (!isComposedIconComponent(tag)) { + return false; + } + + if (parser.getAttributeCount() >= 1) { + if (ICON_BACK_TAG.equalsIgnoreCase(tag)) { + mIconBackCount = parser.getAttributeCount(); + for (int i = 0; i < mIconBackCount; i++) { + tag = String.format(ICON_BACK_FORMAT, i); + icon = parser.getAttributeValue(i); + iconPackResources.put(new ComponentName(tag, ""), icon); + } + } else if (ICON_PALETTIZED_BACK_TAG.equalsIgnoreCase(tag)) { + parsePalettizedBackground(parser, mComposedIconInfo); + } else { + icon = parser.getAttributeValue(0); + iconPackResources.put(new ComponentName(tag, ""), + icon); + } + return true; + } + + return false; + } + + private void parsePalettizedBackground(XmlPullParser parser, ComposedIconInfo iconInfo) { + int attrCount = parser.getAttributeCount(); + ArrayList convertedColors = new ArrayList(); + for (int i = 0; i < attrCount; i++) { + String name = parser.getAttributeName(i); + String value = parser.getAttributeValue(i); + if (TextUtils.isEmpty(name)) { + Log.w(TAG, "Attribute name cannot be empty or null"); + continue; + } + if (TextUtils.isEmpty(value)) { + Log.w(TAG, "Attribute value cannot be empty or null"); + continue; + } + if (IMG_ATTR.equalsIgnoreCase(name)) { + iconInfo.iconPaletteBack = getResourceIdForDrawable(value); + if (DEBUG) { + Log.d(TAG, String.format("img=%s, resId=%d", value, + iconInfo.iconPaletteBack)); + } + } else if (SWATCH_TYPE_ATTR.equalsIgnoreCase(name)) { + ComposedIconInfo.SwatchType type = ComposedIconInfo.SwatchType.None; + if (VIBRANT_VALUE.equalsIgnoreCase(value)) { + type = ComposedIconInfo.SwatchType.Vibrant; + } else if (VIBRANT_LIGHT_VALUE.equalsIgnoreCase(value)) { + type = ComposedIconInfo.SwatchType.VibrantLight; + } else if (VIBRANT_DARK_VALUE.equalsIgnoreCase(value)) { + type = ComposedIconInfo.SwatchType.VibrantDark; + } else if (MUTED_VALUE.equalsIgnoreCase(value)) { + type = ComposedIconInfo.SwatchType.Muted; + } else if (MUTED_LIGHT_VALUE.equalsIgnoreCase(value)) { + type = ComposedIconInfo.SwatchType.MutedLight; + } else if (MUTED_DARK_VALUE.equalsIgnoreCase(value)) { + type = ComposedIconInfo.SwatchType.MutedDark; + } + if (type != ComposedIconInfo.SwatchType.None) { + iconInfo.swatchType = type; + if (DEBUG) Log.d(TAG, "PaletteType=" + type); + } + } else if (name.startsWith(DEFAULT_SWATCH_COLOR_ATTR)) { + try { + // ensure alpha is always 0xff + convertedColors.add(Color.parseColor(value) | 0xff000000); + } catch (IllegalArgumentException e) { + Log.w(TAG, "Invalid color format", e); + } + } + if (convertedColors.size() > 0) { + iconInfo.defaultSwatchColors = new int[convertedColors.size()]; + for (int j = 0; j < convertedColors.size(); j++) { + iconInfo.defaultSwatchColors[j] = convertedColors.get(j); + } + } + } + } + + private boolean parseRotationComponent(XmlPullParser parser, ComposedIconInfo iconInfo) { + if (!parser.getName().equalsIgnoreCase(ICON_ROTATE_TAG)) return false; + String angle = parser.getAttributeValue(null, ANGLE_ATTR); + String variance = parser.getAttributeValue(null, ANGLE_VARIANCE); + if (angle != null) { + try { + iconInfo.iconRotation = Float.valueOf(angle); + } catch (NumberFormatException e) { + Log.w(TAG, "Error parsing " + ANGLE_ATTR, e); + } + } + if (variance != null) { + try { + iconInfo.iconRotationVariance = Float.valueOf(variance); + } catch (NumberFormatException e) { + Log.w(TAG, "Error parsing " + ANGLE_VARIANCE, e); + } + } + return true; + } + + private boolean parseTranslationComponent(XmlPullParser parser, ComposedIconInfo iconInfo) { + if (!parser.getName().equalsIgnoreCase(ICON_TRANSLATE_TAG)) return false; + + final float density = mContext.getResources().getDisplayMetrics().density; + String translateX = parser.getAttributeValue(null, TRANSLATE_X_ATTR); + String translateY = parser.getAttributeValue(null, TRANSLATE_Y_ATTR); + if (translateX != null) { + try { + iconInfo.iconTranslationX = Float.valueOf(translateX) * density; + } catch (NumberFormatException e) { + Log.w(TAG, "Error parsing " + TRANSLATE_X_ATTR, e); + } + } + if (translateY != null) { + try { + iconInfo.iconTranslationY = Float.valueOf(translateY) * density; + } catch (NumberFormatException e) { + Log.w(TAG, "Error parsing " + TRANSLATE_Y_ATTR, e); + } + } + return true; + } + + public void loadIconPack(String packageName) throws NameNotFoundException { + if (packageName == null) { + mLoadedIconPackResource = null; + mLoadedIconPackName = null; + mComposedIconInfo.iconBacks = null; + mComposedIconInfo.iconMask = mComposedIconInfo.iconUpon = 0; + mComposedIconInfo.iconScale = 0; + mComposedIconInfo.iconRotation = 0; + mComposedIconInfo.iconTranslationX = 0; + mComposedIconInfo.iconTranslationY = 0; + mComposedIconInfo.colorFilter = null; + mComposedIconInfo.iconPaletteBack = 0; + mComposedIconInfo.swatchType = ComposedIconInfo.SwatchType.None; + } else { + mIconBackCount = 0; + Resources res = createIconResource(mContext, packageName); + mLoadedIconPackResource = res; + mLoadedIconPackName = packageName; + mIconPackResourceMap = getIconResMapFromXml(res, packageName); + loadComposedIconComponents(); + ColorMatrix cm = mFilterBuilder.build(); + if (cm != null) { + mComposedIconInfo.colorFilter = cm.getArray().clone(); + } + } + } + + public ComposedIconInfo getComposedIconInfo() { + return mComposedIconInfo; + } + + private void loadComposedIconComponents() { + mComposedIconInfo.iconMask = getResourceIdForName(ICON_MASK_COMPONENT); + mComposedIconInfo.iconUpon = getResourceIdForName(ICON_UPON_COMPONENT); + + // Take care of loading iconback which can have multiple images + if (mIconBackCount > 0) { + mComposedIconInfo.iconBacks = new int[mIconBackCount]; + for (int i = 0; i < mIconBackCount; i++) { + mComposedIconInfo.iconBacks[i] = + getResourceIdForName( + new ComponentName(String.format(ICON_BACK_FORMAT, i), "")); + } + } + + // Get the icon scale from this pack + String scale = mIconPackResourceMap.get(ICON_SCALE_COMPONENT); + if (scale != null) { + try { + mComposedIconInfo.iconScale = Float.valueOf(scale); + } catch (NumberFormatException e) { + mComposedIconInfo.iconScale = DEFAULT_SCALE; + } + } else { + mComposedIconInfo.iconScale = DEFAULT_SCALE; + } + } + + private int getResourceIdForName(ComponentName component) { + String item = mIconPackResourceMap.get(component); + if (!TextUtils.isEmpty(item)) { + return getResourceIdForDrawable(item); + } + return 0; + } + + public static Resources createIconResource(Context context, String packageName) + throws NameNotFoundException { + PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0); + String themeApk = info.applicationInfo.publicSourceDir; + + String prefixPath; + String iconApkPath; + String iconResPath; + if (info.isLegacyIconPackApk) { + iconResPath = ""; + iconApkPath = ""; + prefixPath = ""; + } else { + prefixPath = ThemeUtils.ICONS_PATH; //path inside APK + iconApkPath = ThemeUtils.getIconPackApkPath(packageName); + iconResPath = ThemeUtils.getIconPackResPath(packageName); + } + + AssetManager assets = new AssetManager(); + assets.addIconPath(themeApk, iconApkPath, + prefixPath, Resources.THEME_ICON_PKG_ID); + + DisplayMetrics dm = context.getResources().getDisplayMetrics(); + Configuration config = context.getResources().getConfiguration(); + Resources res = new Resources(assets, dm, config); + return res; + } + + public Map getIconResMapFromXml(Resources res, String packageName) { + XmlPullParser parser = null; + InputStream inputStream = null; + Map iconPackResources = new HashMap(); + + try { + inputStream = res.getAssets().open("appfilter.xml"); + XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); + parser = factory.newPullParser(); + parser.setInput(inputStream, "UTF-8"); + } catch (Exception e) { + // Catch any exception since we want to fall back to parsing the xml/ + // resource in all cases + int resId = res.getIdentifier("appfilter", "xml", packageName); + if (resId != 0) { + parser = res.getXml(resId); + } + } + + if (parser != null) { + try { + loadResourcesFromXmlParser(parser, iconPackResources); + return iconPackResources; + } catch (XmlPullParserException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + // Cleanup resources + if (parser instanceof XmlResourceParser) { + ((XmlResourceParser) parser).close(); + } else if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + } + } + } + } + + // Application uses a different theme format (most likely launcher pro) + int arrayId = res.getIdentifier("theme_iconpack", "array", packageName); + if (arrayId == 0) { + arrayId = res.getIdentifier("icon_pack", "array", packageName); + } + + if (arrayId != 0) { + String[] iconPack = res.getStringArray(arrayId); + ComponentName compName = null; + for (String entry : iconPack) { + + if (TextUtils.isEmpty(entry)) { + continue; + } + + String icon = entry; + entry = entry.replaceAll("_", "."); + + compName = new ComponentName(entry.toLowerCase(), ""); + iconPackResources.put(compName, icon); + + int activityIndex = entry.lastIndexOf("."); + if (activityIndex <= 0 || activityIndex == entry.length() - 1) { + continue; + } + + String iconPackage = entry.substring(0, activityIndex); + if (TextUtils.isEmpty(iconPackage)) { + continue; + } + + String iconActivity = entry.substring(activityIndex + 1); + if (TextUtils.isEmpty(iconActivity)) { + continue; + } + + // Store entries as lower case to ensure match + iconPackage = iconPackage.toLowerCase(); + iconActivity = iconActivity.toLowerCase(); + + iconActivity = iconPackage + "." + iconActivity; + compName = new ComponentName(iconPackage, iconActivity); + iconPackResources.put(compName, icon); + } + } + return iconPackResources; + } + + boolean isIconPackLoaded() { + return mLoadedIconPackResource != null && + mLoadedIconPackName != null && + mIconPackResourceMap != null; + } + + private int getResourceIdForDrawable(String resource) { + int resId = + mLoadedIconPackResource.getIdentifier(resource, "drawable",mLoadedIconPackName); + return resId; + } + + public int getResourceIdForActivityIcon(ActivityInfo info) { + if (!isIconPackLoaded()) { + return 0; + } + ComponentName compName = new ComponentName(info.packageName.toLowerCase(), + info.name.toLowerCase()); + String drawable = mIconPackResourceMap.get(compName); + if (drawable != null) { + int resId = getResourceIdForDrawable(drawable); + if (resId != 0) return resId; + } + + // Icon pack doesn't have an icon for the activity, fallback to package icon + compName = new ComponentName(info.packageName.toLowerCase(), ""); + drawable = mIconPackResourceMap.get(compName); + if (drawable == null) { + return 0; + } + return getResourceIdForDrawable(drawable); + } + + public int getResourceIdForApp(String pkgName) { + ActivityInfo info = new ActivityInfo(); + info.packageName = pkgName; + info.name = ""; + return getResourceIdForActivityIcon(info); + } + + public Drawable getDrawableForActivity(ActivityInfo info) { + int id = getResourceIdForActivityIcon(info); + if (id == 0) return null; + return mLoadedIconPackResource.getDrawable(id, null, false); + } + + public Drawable getDrawableForActivityWithDensity(ActivityInfo info, int density) { + int id = getResourceIdForActivityIcon(info); + if (id == 0) return null; + return mLoadedIconPackResource.getDrawableForDensity(id, density, null, false); + } + + public static boolean shouldComposeIcon(ComposedIconInfo iconInfo) { + return iconInfo != null && + (iconInfo.iconBacks != null || + iconInfo.iconMask != 0 || + iconInfo.iconUpon != 0 || + iconInfo.colorFilter != null || + iconInfo.iconPaletteBack != 0 || + iconInfo.iconRotation != 0 || + iconInfo.iconRotationVariance != 0 || + iconInfo.iconTranslationX != 0 || + iconInfo.iconTranslationY != 0 || + iconInfo.iconScale != 1f); + } + + public static class IconCustomizer { + private static final Random sRandom = new Random(); + private static final IThemeService sThemeService; + + static { + sThemeService = IThemeService.Stub.asInterface( + ServiceManager.getService(Context.THEME_SERVICE)); + } + + public static Drawable getComposedIconDrawable(Drawable icon, Context context, + ComposedIconInfo iconInfo) { + final Resources res = context.getResources(); + return getComposedIconDrawable(icon, res, iconInfo); + } + + public static Drawable getComposedIconDrawable(Drawable icon, Resources res, + ComposedIconInfo iconInfo) { + if (iconInfo == null) return icon; + int back = 0; + int defaultSwatchColor = 0; + if (iconInfo.swatchType != ComposedIconInfo.SwatchType.None) { + back = iconInfo.iconPaletteBack; + if (iconInfo.defaultSwatchColors.length > 0) { + defaultSwatchColor = iconInfo.defaultSwatchColors[ + sRandom.nextInt(iconInfo.defaultSwatchColors.length)]; + } + } else if (iconInfo.iconBacks != null && iconInfo.iconBacks.length > 0) { + back = iconInfo.iconBacks[sRandom.nextInt(iconInfo.iconBacks.length)]; + } + Bitmap bmp = createIconBitmap(icon, res, back, defaultSwatchColor, iconInfo); + return bmp != null ? new BitmapDrawable(res, bmp): null; + } + + public static void getValue(Resources res, int resId, TypedValue outValue, + Drawable baseIcon) { + final String pkgName = res.getAssets().getAppName(); + final ComposedIconInfo iconInfo = res.getComposedIconInfo(); + if (iconInfo == null) { + // No composed icon info available so return, keeping original value + return; + } + TypedValue tempValue = new TypedValue(); + tempValue.setTo(outValue); + outValue.assetCookie = COMPOSED_ICON_COOKIE; + outValue.data = resId & (COMPOSED_ICON_COOKIE << 24 | 0x00ffffff); + outValue.string = getCachedIconPath(pkgName, resId, outValue.density); + int hashCode = outValue.string.hashCode() & 0x7fffffff; + int defaultSwatchColor = 0; + + if (!(new File(outValue.string.toString()).exists())) { + // compose the icon and cache it + int back = 0; + if (iconInfo.swatchType != ComposedIconInfo.SwatchType.None) { + back = iconInfo.iconPaletteBack; + if (iconInfo.defaultSwatchColors.length > 0) { + defaultSwatchColor =iconInfo.defaultSwatchColors[ + hashCode % iconInfo.defaultSwatchColors.length]; + } + } else if (iconInfo.iconBacks != null && iconInfo.iconBacks.length > 0) { + back = iconInfo.iconBacks[hashCode % iconInfo.iconBacks.length]; + } + if (DEBUG) { + Log.d(TAG, "Composing icon for " + pkgName); + } + Bitmap bmp = createIconBitmap(baseIcon, res, back, defaultSwatchColor, iconInfo); + if (!cacheComposedIcon(bmp, getCachedIconName(pkgName, resId, outValue.density))) { + Log.w(TAG, "Unable to cache icon " + outValue.string); + // restore the original TypedValue + outValue.setTo(tempValue); + } + } + } + + private static Bitmap createIconBitmap(Drawable icon, Resources res, int iconBack, + int defaultSwatchColor, ComposedIconInfo iconInfo) { + if (iconInfo.iconSize <= 0) return null; + + final Canvas canvas = new Canvas(); + canvas.setDrawFilter(new PaintFlagsDrawFilter(Paint.ANTI_ALIAS_FLAG, + Paint.FILTER_BITMAP_FLAG)); + + int width = 0, height = 0; + int backTintColor = 0; + if (icon instanceof PaintDrawable) { + PaintDrawable painter = (PaintDrawable) icon; + painter.setIntrinsicWidth(iconInfo.iconSize); + painter.setIntrinsicHeight(iconInfo.iconSize); + + // A PaintDrawable does not have an exact size + width = iconInfo.iconSize; + height = iconInfo.iconSize; + } else if (icon instanceof BitmapDrawable) { + // Ensure the bitmap has a density. + BitmapDrawable bitmapDrawable = (BitmapDrawable) icon; + Bitmap bitmap = bitmapDrawable.getBitmap(); + if (bitmap.getDensity() == Bitmap.DENSITY_NONE) { + bitmapDrawable.setTargetDensity(res.getDisplayMetrics()); + } + canvas.setDensity(bitmap.getDensity()); + + // If the original size of the icon isn't greater + // than twice the size of recommended large icons + // respect the original size of the icon + // otherwise enormous icons can easily create + // OOM situations. + if ((bitmap.getWidth() < (iconInfo.iconSize * 2)) + && (bitmap.getHeight() < (iconInfo.iconSize * 2))) { + width = bitmap.getWidth(); + height = bitmap.getHeight(); + } else { + width = iconInfo.iconSize; + height = iconInfo.iconSize; + } + if (iconInfo.swatchType != ComposedIconInfo.SwatchType.None) { + Palette palette = Palette.generate(bitmap, NUM_PALETTE_COLORS); + switch (iconInfo.swatchType) { + case Vibrant: + backTintColor = palette.getVibrantColor(defaultSwatchColor); + break; + case VibrantLight: + backTintColor = palette.getLightVibrantColor(defaultSwatchColor); + break; + case VibrantDark: + backTintColor = palette.getDarkVibrantColor(defaultSwatchColor); + break; + case Muted: + backTintColor = palette.getMutedColor(defaultSwatchColor); + break; + case MutedLight: + backTintColor = palette.getLightMutedColor(defaultSwatchColor); + break; + case MutedDark: + backTintColor = palette.getDarkMutedColor(defaultSwatchColor); + break; + } + if (DEBUG) { + Log.d(TAG, String.format("palette tint color=0x%08x", backTintColor)); + } + } + } else if (icon instanceof VectorDrawable) { + width = height = iconInfo.iconSize; + } + + if (width <= 0 || height <= 0) return null; + + Bitmap bitmap = Bitmap.createBitmap(width, height, + Bitmap.Config.ARGB_8888); + canvas.setBitmap(bitmap); + + // Scale the original + Rect oldBounds = new Rect(); + oldBounds.set(icon.getBounds()); + icon.setBounds(0, 0, width, height); + canvas.save(); + final float halfWidth = width / 2f; + final float halfHeight = height / 2f; + float angle = iconInfo.iconRotation; + if (iconInfo.iconRotationVariance != 0) { + angle += (sRandom.nextFloat() * (iconInfo.iconRotationVariance * 2)) + - iconInfo.iconRotationVariance; + } + canvas.scale(iconInfo.iconScale, iconInfo.iconScale, halfWidth, halfHeight); + canvas.translate(iconInfo.iconTranslationX, iconInfo.iconTranslationY); + canvas.rotate(angle, halfWidth, halfHeight); + if (iconInfo.colorFilter != null) { + Paint p = null; + if (icon instanceof BitmapDrawable) { + p = ((BitmapDrawable) icon).getPaint(); + } else if (icon instanceof PaintDrawable) { + p = ((PaintDrawable) icon).getPaint(); + } + if (p != null) p.setColorFilter(new ColorMatrixColorFilter(iconInfo.colorFilter)); + } + icon.draw(canvas); + canvas.restore(); + + // Mask off the original if iconMask is not null + if (iconInfo.iconMask != 0) { + Drawable mask = res.getDrawable(iconInfo.iconMask); + if (mask != null) { + mask.setBounds(icon.getBounds()); + ((BitmapDrawable) mask).getPaint().setXfermode( + new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); + mask.draw(canvas); + } + } + // Draw the iconBacks if not null and then the original (scaled and masked) icon on top + if (iconBack != 0) { + Drawable back = res.getDrawable(iconBack); + if (back != null) { + back.setBounds(icon.getBounds()); + Paint paint = ((BitmapDrawable) back).getPaint(); + paint.setXfermode( + new PorterDuffXfermode(PorterDuff.Mode.DST_OVER)); + if (backTintColor != 0) { + paint.setColorFilter(new PorterDuffColorFilter(backTintColor, + PorterDuff.Mode.MULTIPLY)); + } + back.draw(canvas); + } + } + // Finally draw the foreground if one was supplied + if (iconInfo.iconUpon != 0) { + Drawable upon = res.getDrawable(iconInfo.iconUpon); + if (upon != null) { + upon.setBounds(icon.getBounds()); + upon.draw(canvas); + } + } + icon.setBounds(oldBounds); + bitmap.setDensity(canvas.getDensity()); + + return bitmap; + } + + private static boolean cacheComposedIcon(Bitmap bmp, String path) { + try { + return sThemeService.cacheComposedIcon(bmp, path); + } catch (RemoteException e) { + Log.e(TAG, "Unable to cache icon.", e); + } + + return false; + } + + private static String getCachedIconPath(String pkgName, int resId, int density) { + return String.format("%s/%s", ThemeUtils.SYSTEM_THEME_ICON_CACHE_DIR, + getCachedIconName(pkgName, resId, density)); + } + + private static String getCachedIconName(String pkgName, int resId, int density) { + return String.format("%s_%08x_%d.png", pkgName, resId, density); + } + } + + public static class ColorFilterUtils { + private static final String TAG_FILTER = "filter"; + private static final String FILTER_HUE = "hue"; + private static final String FILTER_SATURATION = "saturation"; + private static final String FILTER_INVERT = "invert"; + private static final String FILTER_BRIGHTNESS = "brightness"; + private static final String FILTER_CONTRAST = "contrast"; + private static final String FILTER_ALPHA = "alpha"; + private static final String FILTER_TINT = "tint"; + + private static final int MIN_HUE = -180; + private static final int MAX_HUE = 180; + private static final int MIN_SATURATION = 0; + private static final int MAX_SATURATION = 200; + private static final int MIN_BRIGHTNESS = 0; + private static final int MAX_BRIGHTNESS = 200; + private static final int MIN_CONTRAST = -100; + private static final int MAX_CONTRAST = 100; + private static final int MIN_ALPHA = 0; + private static final int MAX_ALPHA = 100; + + public static boolean parseIconFilter(XmlPullParser parser, Builder builder) + throws IOException, XmlPullParserException { + String tag = parser.getName(); + if (!TAG_FILTER.equals(tag)) return false; + + int attrCount = parser.getAttributeCount(); + String attrName; + String attr = null; + int intValue; + while (attrCount-- > 0) { + attrName = parser.getAttributeName(attrCount); + if (attrName.equals("name")) { + attr = parser.getAttributeValue(attrCount); + } + } + String content = parser.nextText(); + if (attr != null && content != null && content.length() > 0) { + content = content.trim(); + if (FILTER_HUE.equalsIgnoreCase(attr)) { + intValue = clampValue(getInt(content, 0),MIN_HUE, MAX_HUE); + builder.hue(intValue); + } else if (FILTER_SATURATION.equalsIgnoreCase(attr)) { + intValue = clampValue(getInt(content, 100), + MIN_SATURATION, MAX_SATURATION); + builder.saturate(intValue); + } else if (FILTER_INVERT.equalsIgnoreCase(attr)) { + if ("true".equalsIgnoreCase(content)) { + builder.invertColors(); + } + } else if (FILTER_BRIGHTNESS.equalsIgnoreCase(attr)) { + intValue = clampValue(getInt(content, 100), + MIN_BRIGHTNESS, MAX_BRIGHTNESS); + builder.brightness(intValue); + } else if (FILTER_CONTRAST.equalsIgnoreCase(attr)) { + intValue = clampValue(getInt(content, 0), + MIN_CONTRAST, MAX_CONTRAST); + builder.contrast(intValue); + } else if (FILTER_ALPHA.equalsIgnoreCase(attr)) { + intValue = clampValue(getInt(content, 100), MIN_ALPHA, MAX_ALPHA); + builder.alpha(intValue); + } else if (FILTER_TINT.equalsIgnoreCase(attr)) { + try { + intValue = Color.parseColor(content); + builder.tint(intValue); + } catch (IllegalArgumentException e) { + Log.w(TAG, "Cannot apply tint, invalid argument: " + content); + } + } + } + return true; + } + + private static int getInt(String value, int defaultValue) { + try { + return Integer.valueOf(value); + } catch (NumberFormatException e) { + return defaultValue; + } + } + + private static int clampValue(int value, int min, int max) { + return Math.min(max, Math.max(min, value)); + } + + /** + * See the following links for reference + * http://groups.google.com/group/android-developers/browse_thread/thread/9e215c83c3819953 + * http://gskinner.com/blog/archives/2007/12/colormatrix_cla.html + * @param value + */ + public static ColorMatrix adjustHue(float value) { + ColorMatrix cm = new ColorMatrix(); + value = value / 180 * (float) Math.PI; + if (value != 0) { + float cosVal = (float) Math.cos(value); + float sinVal = (float) Math.sin(value); + float lumR = 0.213f; + float lumG = 0.715f; + float lumB = 0.072f; + float[] mat = new float[]{ + lumR + cosVal * (1 - lumR) + sinVal * (-lumR), + lumG + cosVal * (-lumG) + sinVal * (-lumG), + lumB + cosVal * (-lumB) + sinVal * (1 - lumB), 0, 0, + lumR + cosVal * (-lumR) + sinVal * (0.143f), + lumG + cosVal * (1 - lumG) + sinVal * (0.140f), + lumB + cosVal * (-lumB) + sinVal * (-0.283f), 0, 0, + lumR + cosVal * (-lumR) + sinVal * (-(1 - lumR)), + lumG + cosVal * (-lumG) + sinVal * (lumG), + lumB + cosVal * (1 - lumB) + sinVal * (lumB), 0, 0, + 0, 0, 0, 1, 0, + 0, 0, 0, 0, 1}; + cm.set(mat); + } + return cm; + } + + public static ColorMatrix adjustSaturation(float saturation) { + saturation = saturation / 100; + ColorMatrix cm = new ColorMatrix(); + cm.setSaturation(saturation); + + return cm; + } + + public static ColorMatrix invertColors() { + float[] matrix = { + -1, 0, 0, 0, 255, //red + 0, -1, 0, 0, 255, //green + 0, 0, -1, 0, 255, //blue + 0, 0, 0, 1, 0 //alpha + }; + + return new ColorMatrix(matrix); + } + + public static ColorMatrix adjustBrightness(float brightness) { + brightness = brightness / 100; + ColorMatrix cm = new ColorMatrix(); + cm.setScale(brightness, brightness, brightness, 1); + + return cm; + } + + public static ColorMatrix adjustContrast(float contrast) { + contrast = contrast / 100 + 1; + float o = (-0.5f * contrast + 0.5f) * 255; + float[] matrix = { + contrast, 0, 0, 0, o, //red + 0, contrast, 0, 0, o, //green + 0, 0, contrast, 0, o, //blue + 0, 0, 0, 1, 0 //alpha + }; + + return new ColorMatrix(matrix); + } + + public static ColorMatrix adjustAlpha(float alpha) { + alpha = alpha / 100; + ColorMatrix cm = new ColorMatrix(); + cm.setScale(1, 1, 1, alpha); + + return cm; + } + + public static ColorMatrix applyTint(int color) { + float alpha = Color.alpha(color) / 255f; + float red = Color.red(color) * alpha; + float green = Color.green(color) * alpha; + float blue = Color.blue(color) * alpha; + + float[] matrix = { + 1, 0, 0, 0, red, //red + 0, 1, 0, 0, green, //green + 0, 0, 1, 0, blue, //blue + 0, 0, 0, 1, 0 //alpha + }; + + return new ColorMatrix(matrix); + } + + public static class Builder { + private List mMatrixList; + + public Builder() { + mMatrixList = new ArrayList(); + } + + public Builder hue(float value) { + mMatrixList.add(adjustHue(value)); + return this; + } + + public Builder saturate(float saturation) { + mMatrixList.add(adjustSaturation(saturation)); + return this; + } + + public Builder brightness(float brightness) { + mMatrixList.add(adjustBrightness(brightness)); + return this; + } + + public Builder contrast(float contrast) { + mMatrixList.add(adjustContrast(contrast)); + return this; + } + + public Builder alpha(float alpha) { + mMatrixList.add(adjustAlpha(alpha)); + return this; + } + + public Builder invertColors() { + mMatrixList.add(ColorFilterUtils.invertColors()); + return this; + } + + public Builder tint(int color) { + mMatrixList.add(applyTint(color)); + return this; + } + + public ColorMatrix build() { + if (mMatrixList == null || mMatrixList.size() == 0) return null; + + ColorMatrix colorMatrix = new ColorMatrix(); + for (ColorMatrix cm : mMatrixList) { + colorMatrix.postConcat(cm); + } + return colorMatrix; + } + } + } +} diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index ad2b61f2446..c7966a3c363 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -1546,10 +1546,18 @@ public void execStartActivitiesAsUser(Context who, IBinder contextThread, } } try { + // we must resolve if the last intent in the stack is floating to give the flag to the previous + boolean floating = false; + if (intents.length > 0) { + floating = (intents[intents.length - 1].getFlags()&Intent.FLAG_FLOATING_WINDOW) == Intent.FLAG_FLOATING_WINDOW; + } String[] resolvedTypes = new String[intents.length]; for (int i=0; i Loader restartLoader(int id, Bundle args, LoaderManager.LoaderCall // finishes or is canceled. if (DEBUG) Log.v(TAG, " Current loader is running; attempting to cancel"); info.cancel(); + + // info.cancel() may have changed the currently active + // loader. If it has, we must start over. + if(info != mLoaders.get(id)) { + if (DEBUG) Log.v(TAG, " Active loader change; restartLoader again"); + return restartLoader(id, args, callback); + } + if (info.mPendingLoader != null) { if (DEBUG) Log.v(TAG, " Removing pending loader: " + info.mPendingLoader); info.mPendingLoader.destroy(); diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 80b57b70382..400234382b0 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -510,6 +510,21 @@ public class Notification implements Parcelable @Priority public int priority; + /** + * Default. + * Show all notifications from an app on keyguard. + * + * @hide + */ + public static final int SHOW_ALL_NOTI_ON_KEYGUARD = 0x01; + + /** + * Show only notifications from an app which are not ongoing ones. + * + * @hide + */ + public static final int SHOW_NO_ONGOING_NOTI_ON_KEYGUARD = 0x02; + /** * Accent color (an ARGB integer like the constants in {@link android.graphics.Color}) * to be applied by the standard Style templates when presenting this notification. @@ -875,6 +890,13 @@ public String getSortKey() { */ public static final int HEADS_UP_REQUESTED = 2; + /** + * Used by light picker in Settings to force + * notification lights on when screen is on + * @hide + */ + public static final String EXTRA_FORCE_SHOW_LIGHTS = "android.forceShowLights"; + /** * Structure to encapsulate a named action that can be shown as part of this notification. * It must include an icon, a label, and a {@link PendingIntent} to be fired when the action is diff --git a/core/java/android/app/NotificationGroup.aidl b/core/java/android/app/NotificationGroup.aidl new file mode 100644 index 00000000000..44b62907fb4 --- /dev/null +++ b/core/java/android/app/NotificationGroup.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2012, The CyanogenMod 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.app; + +parcelable NotificationGroup; diff --git a/core/java/android/app/NotificationGroup.java b/core/java/android/app/NotificationGroup.java new file mode 100644 index 00000000000..bcb70d31749 --- /dev/null +++ b/core/java/android/app/NotificationGroup.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2011 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.app; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.content.Context; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.ParcelUuid; +import android.os.RemoteException; +import android.text.TextUtils; +import android.util.Log; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +/** @hide */ +public class NotificationGroup implements Parcelable { + private static final String TAG = "NotificationGroup"; + + private String mName; + private int mNameResId; + + private UUID mUuid; + + private Set mPackages = new HashSet(); + + private boolean mDirty; + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public NotificationGroup createFromParcel(Parcel in) { + return new NotificationGroup(in); + } + + @Override + public NotificationGroup[] newArray(int size) { + return new NotificationGroup[size]; + } + }; + + public NotificationGroup(String name) { + this(name, -1, null); + } + + public NotificationGroup(String name, int nameResId, UUID uuid) { + mName = name; + mNameResId = nameResId; + mUuid = (uuid != null) ? uuid : UUID.randomUUID(); + mDirty = uuid == null; + } + + private NotificationGroup(Parcel in) { + readFromParcel(in); + } + + @Override + public String toString() { + return getName(); + } + + public String getName() { + return mName; + } + + public void setName(String name) { + mName = name; + mNameResId = -1; + mDirty = true; + } + + public UUID getUuid() { + return mUuid; + } + + public void addPackage(String pkg) { + mPackages.add(pkg); + mDirty = true; + } + + public String[] getPackages() { + return mPackages.toArray(new String[mPackages.size()]); + } + + public void removePackage(String pkg) { + mPackages.remove(pkg); + mDirty = true; + } + + public boolean hasPackage(String pkg) { + return mPackages.contains(pkg); + } + + public boolean isDirty() { + return mDirty; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mName); + dest.writeInt(mNameResId); + dest.writeInt(mDirty ? 1 : 0); + new ParcelUuid(mUuid).writeToParcel(dest, 0); + dest.writeStringArray(getPackages()); + } + + public void readFromParcel(Parcel in) { + mName = in.readString(); + mNameResId = in.readInt(); + mDirty = in.readInt() != 0; + mUuid = ParcelUuid.CREATOR.createFromParcel(in).getUuid(); + mPackages.addAll(Arrays.asList(in.readStringArray())); + } + + public void getXmlString(StringBuilder builder, Context context) { + builder.append(" 0) { + builder.append("nameres=\""); + builder.append(context.getResources().getResourceEntryName(mNameResId)); + } else { + builder.append("name=\""); + builder.append(TextUtils.htmlEncode(getName())); + } + builder.append("\" uuid=\""); + builder.append(TextUtils.htmlEncode(getUuid().toString())); + builder.append("\">\n"); + for (String pkg : mPackages) { + builder.append("" + TextUtils.htmlEncode(pkg) + "\n"); + } + builder.append("\n"); + mDirty = false; + } + + public static NotificationGroup fromXml(XmlPullParser xpp, Context context) + throws XmlPullParserException, IOException { + String value = xpp.getAttributeValue(null, "nameres"); + int nameResId = -1; + String name = null; + UUID uuid = null; + + if (value != null) { + nameResId = context.getResources().getIdentifier(value, "string", "android"); + if (nameResId > 0) { + name = context.getResources().getString(nameResId); + } + } + + if (name == null) { + name = xpp.getAttributeValue(null, "name"); + } + + value = xpp.getAttributeValue(null, "uuid"); + if (value != null) { + try { + uuid = UUID.fromString(value); + } catch (IllegalArgumentException e) { + Log.w(TAG, "UUID not recognized for " + name + ", using new one."); + } + } + + NotificationGroup notificationGroup = new NotificationGroup(name, nameResId, uuid); + int event = xpp.next(); + while (event != XmlPullParser.END_TAG || !xpp.getName().equals("notificationGroup")) { + if (event == XmlPullParser.START_TAG) { + if (xpp.getName().equals("package")) { + String pkg = xpp.nextText(); + notificationGroup.addPackage(pkg); + } + } + event = xpp.next(); + } + + /* we just loaded from XML, no need to save */ + notificationGroup.mDirty = false; + + return notificationGroup; + } +} diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index cf54107848f..d4fbc357f74 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -17,6 +17,7 @@ package android.app; import android.annotation.SdkConstant; +import android.app.Notification; import android.app.Notification.Builder; import android.content.ComponentName; import android.content.Context; @@ -276,5 +277,17 @@ public boolean isSystemConditionProviderEnabled(String path) { } } + /** + * @hide + */ + public int getShowNotificationForPackageOnKeyguard(String pkg, int uid) { + INotificationManager service = getService(); + try { + return getService().getShowNotificationForPackageOnKeyguard(pkg, uid); + } catch (RemoteException e) { + return Notification.SHOW_ALL_NOTI_ON_KEYGUARD; + } + } + private Context mContext; } diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java index 1691d8e28f5..adde127d514 100644 --- a/core/java/android/app/ResourcesManager.java +++ b/core/java/android/app/ResourcesManager.java @@ -18,21 +18,37 @@ import static android.app.ActivityThread.DEBUG_CONFIGURATION; +import android.content.Context; import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.IPackageManager; +import android.content.pm.PackageInfo; +import android.content.pm.PackageItemInfo; +import android.content.pm.PackageManager; +import android.content.pm.ThemeUtils; import android.content.res.AssetManager; import android.content.res.CompatibilityInfo; import android.content.res.Configuration; +import android.content.res.ThemeConfig; import android.content.res.Resources; import android.content.res.ResourcesKey; +import android.graphics.Typeface; import android.hardware.display.DisplayManagerGlobal; import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.UserHandle; +import android.text.TextUtils; import android.util.ArrayMap; import android.util.DisplayMetrics; +import android.util.Log; import android.util.Slog; +import android.util.SparseArray; import android.view.Display; import android.view.DisplayAdjustments; import java.lang.ref.WeakReference; +import java.util.List; import java.util.Locale; /** @hide */ @@ -49,10 +65,17 @@ public class ResourcesManager { = new ArrayMap(); CompatibilityInfo mResCompatibilityInfo; + static IPackageManager sPackageManager; Configuration mResConfiguration; final Configuration mTmpConfig = new Configuration(); + /** + * Number of default assets attached to a Resource object's AssetManager + * This currently includes framework and cmsdk resources + */ + private static final int NUM_DEFAULT_ASSETS = 2; + public static ResourcesManager getInstance() { synchronized (ResourcesManager.class) { if (sResourcesManager == null) { @@ -150,10 +173,13 @@ public boolean applyCompatConfiguration(int displayDensity, * @param token the application token for determining stack bounds. */ public Resources getTopLevelResources(String resDir, String[] splitResDirs, - String[] overlayDirs, String[] libDirs, int displayId, - Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token) { + String[] overlayDirs, String[] libDirs, int displayId, String packageName, + Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token, + Context context, boolean isThemeable) { final float scale = compatInfo.applicationScale; - ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfiguration, scale, token); + final ThemeConfig themeConfig = getThemeConfig(); + ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfiguration, scale, + isThemeable, themeConfig, token); Resources r; synchronized (this) { // Resources is app scale dependent. @@ -178,6 +204,8 @@ public Resources getTopLevelResources(String resDir, String[] splitResDirs, //} AssetManager assets = new AssetManager(); + assets.setAppName(packageName); + assets.setThemeSupport(isThemeable); // resDir can be null if the 'android' package is creating a new Resources object. // This is fine, since each AssetManager automatically loads the 'android' package // already. @@ -197,7 +225,7 @@ public Resources getTopLevelResources(String resDir, String[] splitResDirs, if (overlayDirs != null) { for (String idmapPath : overlayDirs) { - assets.addOverlayPath(idmapPath); + assets.addOverlayPath(idmapPath, null, null, null, null); } } @@ -211,7 +239,10 @@ public Resources getTopLevelResources(String resDir, String[] splitResDirs, } //Slog.i(TAG, "Resource: key=" + key + ", display metrics=" + metrics); - DisplayMetrics dm = getDisplayMetricsLocked(displayId); + DisplayMetrics dm = null; + synchronized(this) { + dm = getDisplayMetricsLocked(displayId); + } Configuration config; boolean isDefaultDisplay = (displayId == Display.DEFAULT_DISPLAY); final boolean hasOverrideConfig = key.hasOverrideConfiguration(); @@ -226,7 +257,33 @@ public Resources getTopLevelResources(String resDir, String[] splitResDirs, } else { config = getConfiguration(); } + + boolean iconsAttached = false; + /* Attach theme information to the resulting AssetManager when appropriate. */ + if (isThemeable && config != null && !context.getPackageManager().isSafeMode()) { + if (config.themeConfig == null) { + try { + config.themeConfig = ThemeConfig.getBootTheme(context.getContentResolver()); + } catch (Exception e) { + Slog.d(TAG, "ThemeConfig.getBootTheme failed, falling back to system theme", e); + config.themeConfig = ThemeConfig.getSystemTheme(); + } + } + + if (config.themeConfig != null) { + attachThemeAssets(assets, config.themeConfig); + attachCommonAssets(assets, config.themeConfig); + iconsAttached = attachIconAssets(assets, config.themeConfig); + } + } else if (!isThemeable && config.themeConfig != null && + !ThemeConfig.SYSTEM_DEFAULT.equals(config.themeConfig.getFontPkgName())) { + // use system fonts if not themeable and a theme font is currently in use + Typeface.recreateDefaults(true); + } + r = new Resources(assets, dm, config, compatInfo, token); + if (iconsAttached) setActivityIcons(r); + if (false) { Slog.i(TAG, "Created app resources " + resDir + " " + r + ": " + r.getConfiguration() + " appScale=" @@ -249,6 +306,116 @@ public Resources getTopLevelResources(String resDir, String[] splitResDirs, } } + /** + * Creates the top level Resources for applications with the given compatibility info. + * + * @param resDir the resource directory. + * @param compatInfo the compability info. Must not be null. + * @param token the application token for determining stack bounds. + * + * @hide + */ + public Resources getTopLevelThemedResources(String resDir, int displayId, String packageName, + String themePackageName, CompatibilityInfo compatInfo, IBinder token, + boolean isThemeable) { + Resources r; + + AssetManager assets = new AssetManager(); + assets.setAppName(packageName); + assets.setThemeSupport(isThemeable); + if (assets.addAssetPath(resDir) == 0) { + return null; + } + + //Slog.i(TAG, "Resource: key=" + key + ", display metrics=" + metrics); + DisplayMetrics dm = getDisplayMetricsLocked(displayId); + Configuration config; + boolean isDefaultDisplay = (displayId == Display.DEFAULT_DISPLAY); + if (!isDefaultDisplay) { + config = new Configuration(getConfiguration()); + applyNonDefaultDisplayMetricsToConfigurationLocked(dm, config); + } else { + config = getConfiguration(); + } + + boolean iconsAttached = false; + if (isThemeable) { + /* Attach theme information to the resulting AssetManager when appropriate. */ + ThemeConfig.Builder builder = new ThemeConfig.Builder(); + builder.defaultOverlay(themePackageName); + builder.defaultIcon(themePackageName); + builder.defaultFont(themePackageName); + + ThemeConfig themeConfig = builder.build(); + attachThemeAssets(assets, themeConfig); + attachCommonAssets(assets, themeConfig); + iconsAttached = attachIconAssets(assets, themeConfig); + } + r = new Resources(assets, dm, config, compatInfo, token); + if (iconsAttached) setActivityIcons(r); + + return r; + } + + /** + * Creates a map between an activity & app's icon ids to its component info. This map + * is then stored in the resource object. + * When resource.getDrawable(id) is called it will check this mapping and replace + * the id with the themed resource id if one is available + * @param context + * @param pkgName + * @param r + */ + private void setActivityIcons(Resources r) { + SparseArray iconResources = new SparseArray(); + String pkgName = r.getAssets().getAppName(); + PackageInfo pkgInfo = null; + ApplicationInfo appInfo = null; + + try { + pkgInfo = getPackageManager().getPackageInfo(pkgName, PackageManager.GET_ACTIVITIES, + UserHandle.getCallingUserId()); + } catch (RemoteException e1) { + Log.e(TAG, "Unable to get pkg " + pkgName, e1); + return; + } + + final ThemeConfig themeConfig = r.getConfiguration().themeConfig; + if (pkgName != null && themeConfig != null && + pkgName.equals(themeConfig.getIconPackPkgName())) { + return; + } + + //Map application icon + if (pkgInfo != null && pkgInfo.applicationInfo != null) { + appInfo = pkgInfo.applicationInfo; + if (appInfo.themedIcon != 0 || iconResources.get(appInfo.icon) == null) { + iconResources.put(appInfo.icon, appInfo); + } + } + + //Map activity icons. + if (pkgInfo != null && pkgInfo.activities != null) { + for (ActivityInfo ai : pkgInfo.activities) { + if (ai.icon != 0 && (ai.themedIcon != 0 || iconResources.get(ai.icon) == null)) { + iconResources.put(ai.icon, ai); + } else if (appInfo != null && appInfo.icon != 0 && + (ai.themedIcon != 0 || iconResources.get(appInfo.icon) == null)) { + iconResources.put(appInfo.icon, ai); + } + } + } + + r.setIconResources(iconResources); + final IPackageManager pm = getPackageManager(); + try { + ComposedIconInfo iconInfo = pm.getComposedIconInfo(); + r.setComposedIconInfo(iconInfo); + } catch (Exception e) { + Log.wtf(TAG, "Failed to retrieve ComposedIconInfo", e); + } + } + public final boolean applyConfigurationToResourcesLocked(Configuration config, CompatibilityInfo compat) { if (mResConfiguration == null) { @@ -293,6 +460,23 @@ public final boolean applyConfigurationToResourcesLocked(Configuration config, boolean isDefaultDisplay = (displayId == Display.DEFAULT_DISPLAY); DisplayMetrics dm = defaultDisplayMetrics; final boolean hasOverrideConfiguration = key.hasOverrideConfiguration(); + boolean themeChanged = (changes & ActivityInfo.CONFIG_THEME_RESOURCE) != 0; + final boolean themeChanged2 = (changes & ActivityInfo.CONFIG_UI_THEME_MODE) != 0; + if (themeChanged) { + AssetManager am = r.getAssets(); + if (am.hasThemeSupport()) { + r.setIconResources(null); + r.setComposedIconInfo(null); + detachThemeAssets(am); + if (config.themeConfig != null) { + attachThemeAssets(am, config.themeConfig); + attachCommonAssets(am, config.themeConfig); + if (attachIconAssets(am, config.themeConfig)) { + setActivityIcons(r); + } + } + } + } if (!isDefaultDisplay || hasOverrideConfiguration) { if (tmpConfig == null) { tmpConfig = new Configuration(); @@ -309,6 +493,9 @@ public final boolean applyConfigurationToResourcesLocked(Configuration config, } else { r.updateConfiguration(config, dm, compat); } + if (themeChanged || themeChanged2) { + r.updateStringCache(); + } //Slog.i(TAG, "Updated app resources " + v.getKey() // + " " + r + ": " + r.getConfiguration()); } else { @@ -320,4 +507,243 @@ public final boolean applyConfigurationToResourcesLocked(Configuration config, return changes != 0; } + public static IPackageManager getPackageManager() { + if (sPackageManager != null) { + return sPackageManager; + } + IBinder b = ServiceManager.getService("package"); + sPackageManager = IPackageManager.Stub.asInterface(b); + return sPackageManager; + } + + + /** + * Attach the necessary theme asset paths and meta information to convert an + * AssetManager to being globally "theme-aware". + * + * @param assets + * @param theme + * @return true if the AssetManager is now theme-aware; false otherwise. + * This can fail, for example, if the theme package has been been + * removed and the theme manager has yet to revert formally back to + * the framework default. + */ + private boolean attachThemeAssets(AssetManager assets, ThemeConfig theme) { + PackageInfo piTheme = null; + PackageInfo piTarget = null; + PackageInfo piAndroid = null; + + // Some apps run in process of another app (eg keyguard/systemUI) so we must get the + // package name from the res tables. The 0th base package name will be the android group. + // The 1st base package name will be the app group if one is attached. Check if it is there + // first or else the system will crash! + String basePackageName = null; + String resourcePackageName = null; + int count = assets.getBasePackageCount(); + if (count > NUM_DEFAULT_ASSETS) { + basePackageName = assets.getBasePackageName(NUM_DEFAULT_ASSETS); + resourcePackageName = assets.getBaseResourcePackageName(NUM_DEFAULT_ASSETS); + } else if (count == NUM_DEFAULT_ASSETS) { + basePackageName = assets.getBasePackageName(0); + } else { + return false; + } + + try { + piTheme = getPackageManager().getPackageInfo( + theme.getOverlayPkgNameForApp(basePackageName), 0, + UserHandle.getCallingUserId()); + piTarget = getPackageManager().getPackageInfo( + basePackageName, 0, UserHandle.getCallingUserId()); + + // Handle special case where a system app (ex trebuchet) may have had its pkg name + // renamed during an upgrade. basePackageName would be the manifest value which will + // fail on getPackageInfo(). resource pkg is assumed to have the original name + if (piTarget == null && resourcePackageName != null) { + piTarget = getPackageManager().getPackageInfo(resourcePackageName, + 0, UserHandle.getCallingUserId()); + } + piAndroid = getPackageManager().getPackageInfo("android", 0, + UserHandle.getCallingUserId()); + } catch (RemoteException e) { + } + + if (piTheme == null || piTheme.applicationInfo == null || + piTarget == null || piTarget.applicationInfo == null || + piAndroid == null || piAndroid.applicationInfo == null || + piTheme.mOverlayTargets == null) { + return false; + } + + String themePackageName = piTheme.packageName; + String themePath = piTheme.applicationInfo.publicSourceDir; + if (!piTarget.isThemeApk && piTheme.mOverlayTargets.contains(basePackageName)) { + String targetPackagePath = piTarget.applicationInfo.sourceDir; + String prefixPath = ThemeUtils.getOverlayPathToTarget(basePackageName); + + String resCachePath = ThemeUtils.getTargetCacheDir(piTarget.packageName, piTheme); + String resApkPath = resCachePath + "/resources.apk"; + String idmapPath = ThemeUtils.getIdmapPath(piTarget.packageName, piTheme.packageName); + int cookie = assets.addOverlayPath(idmapPath, themePath, resApkPath, + targetPackagePath, prefixPath); + + if (cookie != 0) { + assets.setThemePackageName(themePackageName); + assets.addThemeCookie(cookie); + } + } + + if (!piTarget.isThemeApk && !"android".equals(basePackageName) && + piTheme.mOverlayTargets.contains("android")) { + String resCachePath= ThemeUtils.getTargetCacheDir(piAndroid.packageName, piTheme); + String prefixPath = ThemeUtils.getOverlayPathToTarget(piAndroid.packageName); + String targetPackagePath = piAndroid.applicationInfo.publicSourceDir; + String resApkPath = resCachePath + "/resources.apk"; + String idmapPath = ThemeUtils.getIdmapPath("android", piTheme.packageName); + int cookie = assets.addOverlayPath(idmapPath, themePath, + resApkPath, targetPackagePath, prefixPath); + if (cookie != 0) { + assets.setThemePackageName(themePackageName); + assets.addThemeCookie(cookie); + } + } + + return true; + } + + /** + * Attach the necessary icon asset paths. Icon assets should be in a different + * namespace than the standard 0x7F. + * + * @param assets + * @param theme + * @return true if succes, false otherwise + */ + private boolean attachIconAssets(AssetManager assets, ThemeConfig theme) { + PackageInfo piIcon = null; + try { + piIcon = getPackageManager().getPackageInfo(theme.getIconPackPkgName(), 0, + UserHandle.getCallingUserId()); + } catch (RemoteException e) { + } + + if (piIcon == null || piIcon.applicationInfo == null) { + return false; + } + + String iconPkg = theme.getIconPackPkgName(); + if (iconPkg != null && !iconPkg.isEmpty()) { + String themeIconPath = piIcon.applicationInfo.publicSourceDir; + String prefixPath = ThemeUtils.ICONS_PATH; + String iconDir = ThemeUtils.getIconPackDir(iconPkg); + String resTablePath = iconDir + "/resources.arsc"; + String resApkPath = iconDir + "/resources.apk"; + + // Legacy Icon packs have everything in their APK + if (piIcon.isLegacyIconPackApk) { + prefixPath = ""; + resApkPath = ""; + resTablePath = ""; + } + + int cookie = assets.addIconPath(themeIconPath, resApkPath, prefixPath, + Resources.THEME_ICON_PKG_ID); + if (cookie != 0) { + assets.setIconPackCookie(cookie); + assets.setIconPackageName(iconPkg); + } + } + + return true; + } + + /** + * Attach the necessary common asset paths. Common assets should be in a different + * namespace than the standard 0x7F. + * + * @param assets + * @param theme + * @return true if succes, false otherwise + */ + private boolean attachCommonAssets(AssetManager assets, ThemeConfig theme) { + // Some apps run in process of another app (eg keyguard/systemUI) so we must get the + // package name from the res tables. The 0th base package name will be the android group. + // The 1st base package name will be the app group if one is attached. Check if it is there + // first or else the system will crash! + String basePackageName; + int count = assets.getBasePackageCount(); + if (count > NUM_DEFAULT_ASSETS) { + basePackageName = assets.getBasePackageName(NUM_DEFAULT_ASSETS); + } else if (count == NUM_DEFAULT_ASSETS) { + basePackageName = assets.getBasePackageName(0); + } else { + return false; + } + + PackageInfo piTheme = null; + try { + piTheme = getPackageManager().getPackageInfo( + theme.getOverlayPkgNameForApp(basePackageName), 0, + UserHandle.getCallingUserId()); + } catch (RemoteException e) { + } + + if (piTheme == null || piTheme.applicationInfo == null) { + return false; + } + + String themePackageName = + ThemeUtils.getCommonPackageName(piTheme.applicationInfo.packageName); + if (themePackageName != null && !themePackageName.isEmpty()) { + String themePath = piTheme.applicationInfo.publicSourceDir; + String prefixPath = ThemeUtils.COMMON_RES_PATH; + String resCachePath = + ThemeUtils.getTargetCacheDir(ThemeUtils.COMMON_RES_TARGET, piTheme); + String resApkPath = resCachePath + "/resources.apk"; + int cookie = assets.addCommonOverlayPath(themePath, resApkPath, + prefixPath); + if (cookie != 0) { + assets.setCommonResCookie(cookie); + assets.setCommonResPackageName(themePackageName); + } + } + + return true; + } + + private void detachThemeAssets(AssetManager assets) { + String themePackageName = assets.getThemePackageName(); + String iconPackageName = assets.getIconPackageName(); + String commonResPackageName = assets.getCommonResPackageName(); + + //Remove Icon pack if it exists + if (!TextUtils.isEmpty(iconPackageName) && assets.getIconPackCookie() > 0) { + assets.removeOverlayPath(iconPackageName, assets.getIconPackCookie()); + assets.setIconPackageName(null); + assets.setIconPackCookie(0); + } + //Remove common resources if it exists + if (!TextUtils.isEmpty(commonResPackageName) && assets.getCommonResCookie() > 0) { + assets.removeOverlayPath(commonResPackageName, assets.getCommonResCookie()); + assets.setCommonResPackageName(null); + assets.setCommonResCookie(0); + } + final List themeCookies = assets.getThemeCookies(); + if (!TextUtils.isEmpty(themePackageName) && !themeCookies.isEmpty()) { + // remove overlays in reverse order + for (int i = themeCookies.size() - 1; i >= 0; i--) { + assets.removeOverlayPath(themePackageName, themeCookies.get(i)); + } + } + assets.getThemeCookies().clear(); + assets.setThemePackageName(null); + } + + private ThemeConfig getThemeConfig() { + Configuration config = getConfiguration(); + if (config != null) { + return config.themeConfig; + } + return null; + } } diff --git a/core/java/android/app/SearchManager.java b/core/java/android/app/SearchManager.java index a40b29a25d7..d7c4467b3af 100644 --- a/core/java/android/app/SearchManager.java +++ b/core/java/android/app/SearchManager.java @@ -540,11 +540,6 @@ public class SearchManager private final Context mContext; - /** - * The package associated with this seach manager. - */ - private String mAssociatedPackage; - // package private since they are used by the inner class SearchManagerCallback /* package */ final Handler mHandler; /* package */ OnDismissListener mDismissListener = null; @@ -742,10 +737,6 @@ public ComponentName getWebSearchActivity() { public void triggerSearch(String query, ComponentName launchActivity, Bundle appSearchData) { - if (!mAssociatedPackage.equals(launchActivity.getPackageName())) { - throw new IllegalArgumentException("invoking app search on a different package " + - "not associated with this search manager"); - } if (query == null || TextUtils.getTrimmedLength(query) == 0) { Log.w(TAG, "triggerSearch called with empty query, ignoring."); return; diff --git a/core/java/android/app/TaskStackBuilder.java b/core/java/android/app/TaskStackBuilder.java index 0077db1eb31..d30872ea889 100644 --- a/core/java/android/app/TaskStackBuilder.java +++ b/core/java/android/app/TaskStackBuilder.java @@ -63,6 +63,7 @@ public class TaskStackBuilder { private final ArrayList mIntents = new ArrayList(); private final Context mSourceContext; + private boolean mFirstTaskOnHome = true; private TaskStackBuilder(Context a) { mSourceContext = a; @@ -79,6 +80,10 @@ public static TaskStackBuilder create(Context context) { return new TaskStackBuilder(context); } + public void setTaskOnHome(boolean firstTaskOnHome) { + mFirstTaskOnHome = firstTaskOnHome; + } + /** * Add a new Intent to the task stack. The most recently added Intent will invoke * the Activity at the top of the final task stack. @@ -301,9 +306,16 @@ public Intent[] getIntents() { Intent[] intents = new Intent[mIntents.size()]; if (intents.length == 0) return intents; - intents[0] = new Intent(mIntents.get(0)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | - Intent.FLAG_ACTIVITY_CLEAR_TASK | - Intent.FLAG_ACTIVITY_TASK_ON_HOME); + Intent newIntent = new Intent(mIntents.get(0)); + newIntent.addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK | + Intent.FLAG_ACTIVITY_CLEAR_TASK); + + if (mFirstTaskOnHome) { + newIntent.addFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME); + } + + intents[0] = newIntent; for (int i = 1; i < intents.length; i++) { intents[i] = new Intent(mIntents.get(i)); } diff --git a/core/java/android/app/UiModeManager.java b/core/java/android/app/UiModeManager.java index 0a255f7f1af..a06ed3a6591 100644 --- a/core/java/android/app/UiModeManager.java +++ b/core/java/android/app/UiModeManager.java @@ -234,4 +234,23 @@ public int getNightMode() { } return -1; } + + /** + * Set system ui theme mode. + * + * possible theme modes @link Configuration + * {@link #UI_THEME_MODE_NORMAL}, + * {@link #UI_THEME_MODE_HOLO_DARK}, {@link #UI_THEME_MODE_HOLO_LIGHT} + * + * @hide + */ + public void setUiThemeMode(int mode) { + if (mService != null) { + try { + mService.setUiThemeMode(mode); + } catch (RemoteException e) { + Log.e(TAG, "setUiThemeMode: RemoteException", e); + } + } + } } diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java index 8bfe6d3859a..00b6361ac6f 100644 --- a/core/java/android/app/WallpaperManager.java +++ b/core/java/android/app/WallpaperManager.java @@ -32,6 +32,7 @@ import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PixelFormat; +import android.graphics.Point; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; @@ -226,9 +227,10 @@ static class Globals extends IWallpaperManagerCallback.Stub { private IWallpaperManager mService; private Bitmap mWallpaper; private Bitmap mDefaultWallpaper; + private Bitmap mKeyguardWallpaper; private static final int MSG_CLEAR_WALLPAPER = 1; - + Globals(Looper looper) { IBinder b = ServiceManager.getService(Context.WALLPAPER_SERVICE); mService = IWallpaperManager.Stub.asInterface(b); @@ -246,6 +248,12 @@ public void onWallpaperChanged() { } } + public void onKeyguardWallpaperChanged() { + synchronized (this) { + mKeyguardWallpaper = null; + } + } + public Bitmap peekWallpaperBitmap(Context context, boolean returnDefault) { synchronized (this) { if (mWallpaper != null) { @@ -272,6 +280,23 @@ public Bitmap peekWallpaperBitmap(Context context, boolean returnDefault) { } } + /** + * @hide + */ + public Bitmap peekKeyguardWallpaperBitmap(Context context) { + synchronized (this) { + if (mKeyguardWallpaper != null) { + return mKeyguardWallpaper; + } + try { + mKeyguardWallpaper = getCurrentKeyguardWallpaperLocked(context); + } catch (OutOfMemoryError e) { + Log.w(TAG, "No memory load current keyguard wallpaper", e); + } + return mKeyguardWallpaper; + } + } + public void forgetLoadedWallpaper() { synchronized (this) { mWallpaper = null; @@ -279,6 +304,12 @@ public void forgetLoadedWallpaper() { } } + public void forgetLoadedKeyguardWallpaper() { + synchronized (this) { + mKeyguardWallpaper = null; + } + } + private Bitmap getCurrentWallpaperLocked(Context context) { if (mService == null) { Log.w(TAG, "WallpaperService not running"); @@ -308,7 +339,37 @@ private Bitmap getCurrentWallpaperLocked(Context context) { } return null; } - + + private Bitmap getCurrentKeyguardWallpaperLocked(Context context) { + if (mService == null) { + Log.w(TAG, "WallpaperService not running"); + return null; + } + try { + Bundle params = new Bundle(); + ParcelFileDescriptor fd = mService.getKeyguardWallpaper(this, params); + if (fd != null) { + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + Bitmap bm = BitmapFactory.decodeFileDescriptor( + fd.getFileDescriptor(), null, options); + return bm; + } catch (OutOfMemoryError e) { + Log.w(TAG, "Can't decode file", e); + } finally { + try { + fd.close(); + } catch (IOException e) { + // Ignore + } + } + } + } catch (RemoteException e) { + // Ignore + } + return null; + } + private Bitmap getDefaultWallpaperLocked(Context context) { InputStream is = openDefaultWallpaper(context); if (is != null) { @@ -327,6 +388,18 @@ private Bitmap getDefaultWallpaperLocked(Context context) { } return null; } + + /** @hide */ + public void clearKeyguardWallpaper() { + synchronized (this) { + try { + mService.clearKeyguardWallpaper(); + } catch (RemoteException e) { + // ignore + } + mKeyguardWallpaper = null; + } + } } private static final Object sSync = new Object[0]; @@ -586,6 +659,15 @@ public Drawable getFastDrawable() { return null; } + /** @hide */ + public Drawable getFastKeyguardDrawable() { + Bitmap bm = sGlobals.peekKeyguardWallpaperBitmap(mContext); + if (bm != null) { + return new FastBitmapDrawable(bm); + } + return null; + } + /** * Like {@link #getFastDrawable()}, but if there is no wallpaper set, * a null pointer is returned. @@ -610,6 +692,13 @@ public Bitmap getBitmap() { return sGlobals.peekWallpaperBitmap(mContext, true); } + /** + * @hide + */ + public Bitmap getKeyguardBitmap() { + return sGlobals.peekKeyguardWallpaperBitmap(mContext); + } + /** * Remove all internal references to the last loaded wallpaper. Useful * for apps that want to reduce memory usage when they only temporarily @@ -620,6 +709,13 @@ public void forgetLoadedWallpaper() { sGlobals.forgetLoadedWallpaper(); } + /** + * @hide + */ + public void forgetLoadedKeyguardWallpaper() { + sGlobals.forgetLoadedKeyguardWallpaper(); + } + /** * If the current wallpaper is a live wallpaper component, return the * information about that wallpaper. Otherwise, if it is a static image, @@ -770,6 +866,35 @@ public void setBitmap(Bitmap bitmap) throws IOException { } } + /** + * @param bitmap + * @throws IOException + * @hide + */ + public void setKeyguardBitmap(Bitmap bitmap) throws IOException { + if (sGlobals.mService == null) { + Log.w(TAG, "WallpaperService not running"); + return; + } + try { + ParcelFileDescriptor fd = sGlobals.mService.setKeyguardWallpaper(null); + if (fd == null) { + return; + } + FileOutputStream fos = null; + try { + fos = new ParcelFileDescriptor.AutoCloseOutputStream(fd); + bitmap.compress(Bitmap.CompressFormat.PNG, 90, fos); + } finally { + if (fos != null) { + fos.close(); + } + } + } catch (RemoteException e) { + // Ignore + } + } + /** * Change the current system wallpaper to a specific byte stream. The * give InputStream is copied into persistent storage and will now be @@ -809,6 +934,33 @@ public void setStream(InputStream data) throws IOException { } } + /** + * @hide + */ + public void setKeyguardStream(InputStream data) throws IOException { + if (sGlobals.mService == null) { + Log.w(TAG, "WallpaperService not running"); + return; + } + try { + ParcelFileDescriptor fd = sGlobals.mService.setKeyguardWallpaper(null); + if (fd == null) { + return; + } + FileOutputStream fos = null; + try { + fos = new ParcelFileDescriptor.AutoCloseOutputStream(fd); + setWallpaper(data, fos); + } finally { + if (fos != null) { + fos.close(); + } + } + } catch (RemoteException e) { + // Ignore + } + } + private void setWallpaper(InputStream data, FileOutputStream fos) throws IOException { byte[] buffer = new byte[32768]; @@ -1029,7 +1181,29 @@ public void setWallpaperOffsetSteps(float xStep, float yStep) { mWallpaperXStep = xStep; mWallpaperYStep = yStep; } - + + /** @hide */ + public int getLastWallpaperX() { + try { + return WindowManagerGlobal.getWindowSession().getLastWallpaperX(); + } catch (RemoteException e) { + // Ignore. + } + + return -1; + } + + /** @hide */ + public int getLastWallpaperY() { + try { + return WindowManagerGlobal.getWindowSession().getLastWallpaperY(); + } catch (RemoteException e) { + // Ignore. + } + + return -1; + } + /** * Send an arbitrary command to the current active wallpaper. * @@ -1086,7 +1260,25 @@ public void clearWallpaperOffsets(IBinder windowToken) { * wallpaper. */ public void clear() throws IOException { - setStream(openDefaultWallpaper(mContext)); + clear(true); + } + + /** @hide */ + public void clear(boolean setToDefault) throws IOException { + if (setToDefault) { + setStream(openDefaultWallpaper(mContext)); + } else { + Bitmap blackBmp = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565); + blackBmp.setPixel(0, 0, mContext.getResources().getColor(android.R.color.black)); + setBitmap(blackBmp); + } + } + + /** + * @hide + */ + public void clearKeyguardWallpaper() { + sGlobals.clearKeyguardWallpaper(); } /** diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index cf06cb75bcb..6d63246a01e 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -662,6 +662,8 @@ public boolean hasGrantedPolicy(ComponentName admin, int usesPolicy) { */ public static final int PASSWORD_QUALITY_COMPLEX = 0x60000; + public static final int PASSWORD_QUALITY_GESTURE_WEAK = 0x80000; + /** * Called by an application that is administering the device to set the * password restrictions it is imposing. After setting this, the user @@ -2698,6 +2700,7 @@ public void setApplicationRestrictions(ComponentName admin, String packageName, *

If {@link #KEYGUARD_DISABLE_TRUST_AGENTS} is set and options is not null for all admins, * then it's up to the TrustAgent itself to aggregate the values from all device admins. *

Consult documentation for the specific TrustAgent to determine legal options parameters. + * @hide */ public void setTrustAgentConfiguration(ComponentName admin, ComponentName target, PersistableBundle configuration) { @@ -2724,6 +2727,7 @@ public void setTrustAgentConfiguration(ComponentName admin, ComponentName target * for this {@param agent} or calls it with a null configuration, null is returned. * @param agent Which component to get enabled features for. * @return configuration for the given trust agent. + * @hide */ public List getTrustAgentConfiguration(ComponentName admin, ComponentName agent) { @@ -3573,4 +3577,24 @@ public List getCrossProfileWidgetProviders(ComponentName admin) { } return Collections.emptyList(); } + + /** + * CM: check if secure keyguard is required + * @hide + */ + public boolean requireSecureKeyguard() { + return requireSecureKeyguard(UserHandle.myUserId()); + } + + /** @hide */ + public boolean requireSecureKeyguard(int userHandle) { + if (mService != null) { + try { + return mService.requireSecureKeyguard(userHandle); + } catch (RemoteException e) { + Log.w(TAG, "Failed to get secure keyguard requirement"); + } + } + return true; + } } diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index 0ca60c00657..7359a369705 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -198,4 +198,6 @@ interface IDevicePolicyManager { boolean getAutoTimeRequired(); boolean isRemovingAdmin(in ComponentName adminReceiver, int userHandle); + + boolean requireSecureKeyguard(int userHandle); } diff --git a/core/java/android/app/backup/BackupAgent.java b/core/java/android/app/backup/BackupAgent.java index 87d785a2223..a210bc09b4e 100644 --- a/core/java/android/app/backup/BackupAgent.java +++ b/core/java/android/app/backup/BackupAgent.java @@ -41,6 +41,7 @@ import java.util.HashSet; import java.util.LinkedList; import java.util.concurrent.CountDownLatch; +import java.util.regex.Pattern; /** * Provides the central interface between an @@ -105,7 +106,14 @@ */ public abstract class BackupAgent extends ContextWrapper { private static final String TAG = "BackupAgent"; - private static final boolean DEBUG = true; + private static final boolean DEBUG = false; + + /** + * To lookup an application's database and shared preferences directories (usually + * /data/data/com.application.name), this generic name is used to invoke the + * getSharedPrefsFile(String) and getDatabasePath(String) APIs. + */ + private static final String GENERIC_FILE_NAME = "foo"; /** @hide */ public static final int TYPE_EOF = 0; @@ -282,8 +290,9 @@ public void onFullBackup(FullBackupDataOutput data) throws IOException { // all of the ones we will be traversing String rootDir = new File(appInfo.dataDir).getCanonicalPath(); String filesDir = getFilesDir().getCanonicalPath(); - String databaseDir = getDatabasePath("foo").getParentFile().getCanonicalPath(); - String sharedPrefsDir = getSharedPrefsFile("foo").getParentFile().getCanonicalPath(); + String databaseDir = getDatabasePath(GENERIC_FILE_NAME).getParentFile().getCanonicalPath(); + String sharedPrefsDir = + getSharedPrefsFile(GENERIC_FILE_NAME).getParentFile().getCanonicalPath(); String cacheDir = getCacheDir().getCanonicalPath(); String libDir = (appInfo.nativeLibraryDir != null) ? new File(appInfo.nativeLibraryDir).getCanonicalPath() @@ -364,8 +373,8 @@ public final void fullBackupFile(File file, FullBackupDataOutput output) { mainDir = new File(appInfo.dataDir).getCanonicalPath(); filesDir = getFilesDir().getCanonicalPath(); nbFilesDir = getNoBackupFilesDir().getCanonicalPath(); - dbDir = getDatabasePath("foo").getParentFile().getCanonicalPath(); - spDir = getSharedPrefsFile("foo").getParentFile().getCanonicalPath(); + dbDir = getDatabasePath(GENERIC_FILE_NAME).getParentFile().getCanonicalPath(); + spDir = getSharedPrefsFile(GENERIC_FILE_NAME).getParentFile().getCanonicalPath(); cacheDir = getCacheDir().getCanonicalPath(); libDir = (appInfo.nativeLibraryDir == null) ? null @@ -432,7 +441,20 @@ public final void fullBackupFile(File file, FullBackupDataOutput output) { * @hide */ protected final void fullBackupFileTree(String packageName, String domain, String rootPath, - HashSet excludes, FullBackupDataOutput output) { + HashSet excludeFullPathDir, FullBackupDataOutput output) { + fullBackupFileTree(packageName, domain, rootPath, excludeFullPathDir, null, output); + } + + /** + * Scan the dir tree (if it actually exists) and process each entry we find. If the + * 'excludes' parameter is non-null, it is consulted each time a new file system entity + * is visited to see whether that entity (and its subtree, if appropriate) should be + * omitted from the backup process. + * + * @hide + */ + protected final void fullBackupFileTree(String packageName, String domain, String rootPath, + HashSet excludeFullPathDir, Pattern excludeFiles, FullBackupDataOutput output) { File rootFile = new File(rootPath); if (rootFile.exists()) { LinkedList scanQueue = new LinkedList(); @@ -445,7 +467,14 @@ protected final void fullBackupFileTree(String packageName, String domain, Strin filePath = file.getCanonicalPath(); // prune this subtree? - if (excludes != null && excludes.contains(filePath)) { + if (excludeFullPathDir != null && excludeFullPathDir.contains(filePath)) { + if (DEBUG) Log.i(TAG, "Excluding directory path: " + filePath); + continue; + } + + // Does this match the regex ? + if (excludeFiles != null && excludeFiles.matcher(filePath).find()) { + if (DEBUG) Log.i(TAG, "Excluding file: " + filePath); continue; } @@ -466,7 +495,9 @@ protected final void fullBackupFileTree(String packageName, String domain, Strin if (DEBUG) Log.w(TAG, "Error canonicalizing path of " + file); continue; } catch (ErrnoException e) { - if (DEBUG) Log.w(TAG, "Error scanning file " + file + " : " + e); + if (DEBUG) { + Log.w(TAG, "Error scanning file " + file + ", skipping this file backup"); + } continue; } @@ -477,6 +508,91 @@ protected final void fullBackupFileTree(String packageName, String domain, Strin } } + /** + * Backup files specified by domainTokens parameter. It should be one (or more) of + * rootDir, filesDir, databaseDir, cacheDir, sharedPrefsDir, externalFilesDir + * excludeFiles is a regex of the files to be excluded when doing the backup. + * + * @param domainTokens + * @param excludeFilesRegex + * @param data + * @hide + */ + private void onBackupFiles(String[] domainTokens, String excludeFilesRegex, + FullBackupDataOutput data) throws IOException { + String rootDir, filesDir, databaseDir, sharedPrefsDir, cacheDir, libDir, efLocationDir; + ApplicationInfo appInfo = getApplicationInfo(); + + rootDir = new File(appInfo.dataDir).getCanonicalPath(); + filesDir = getFilesDir().getCanonicalPath(); + databaseDir = getDatabasePath(GENERIC_FILE_NAME).getParentFile().getCanonicalPath(); + sharedPrefsDir = getSharedPrefsFile(GENERIC_FILE_NAME).getParentFile().getCanonicalPath(); + cacheDir = getCacheDir().getCanonicalPath(); + libDir = (appInfo.nativeLibraryDir != null) + ? new File(appInfo.nativeLibraryDir).getCanonicalPath() + : null; + efLocationDir = getExternalFilesDir(null).getCanonicalPath(); + + // Filters, the scan queue, and the set of resulting entities + HashSet filterSet = new HashSet(); + String packageName = getPackageName(); + // Okay, start with the app's root tree, but exclude all of the canonical subdirs + if (libDir != null) { + filterSet.add(libDir); + } + filterSet.add(cacheDir); + filterSet.add(databaseDir); + filterSet.add(sharedPrefsDir); + filterSet.add(filesDir); + + Pattern excludeFiles = null; + if (excludeFilesRegex != null) { + excludeFiles = Pattern.compile(excludeFilesRegex, Pattern.CASE_INSENSITIVE); + } + + for (String dToken : domainTokens) { + if (FullBackup.ROOT_TREE_TOKEN.equals(dToken)) { + fullBackupFileTree(packageName, FullBackup.ROOT_TREE_TOKEN, rootDir, filterSet, + excludeFiles, data); + } + + if (FullBackup.DATA_TREE_TOKEN.equals(dToken)) { + filterSet.add(rootDir); + filterSet.remove(filesDir); + fullBackupFileTree(packageName, FullBackup.DATA_TREE_TOKEN, filesDir, filterSet, + excludeFiles, data); + } + + if (FullBackup.DATABASE_TREE_TOKEN.equals(dToken)) { + filterSet.add(filesDir); + filterSet.remove(databaseDir); + fullBackupFileTree(packageName, FullBackup.DATABASE_TREE_TOKEN, databaseDir, + filterSet, excludeFiles, data); + } + + if (FullBackup.SHAREDPREFS_TREE_TOKEN.equals(dToken)) { + filterSet.add(databaseDir); + filterSet.remove(sharedPrefsDir); + fullBackupFileTree(packageName, FullBackup.SHAREDPREFS_TREE_TOKEN, sharedPrefsDir, + filterSet, excludeFiles, data); + } + + if (FullBackup.MANAGED_EXTERNAL_TREE_TOKEN.equals(dToken)) { + // getExternalFilesDir() location associated with this app. Technically there should + // not be any files here if the app does not properly have permission to access + // external storage, but edge cases happen. fullBackupFileTree() catches + // IOExceptions and similar, and treats them as non-fatal, so we rely on that; and + // we know a priori that processes running as the system UID are not permitted to + // access external storage, so we check for that as well to avoid nastygrams in + // the log. + if (Process.myUid() != Process.SYSTEM_UID) { + fullBackupFileTree(packageName, FullBackup.MANAGED_EXTERNAL_TREE_TOKEN, + efLocationDir, null, excludeFiles, data); + } + } + } + } + /** * Handle the data delivered via the given file descriptor during a full restore * operation. The agent is given the path to the file's original location as well @@ -527,11 +643,11 @@ protected void onRestoreFile(ParcelFileDescriptor data, long size, if (domain.equals(FullBackup.DATA_TREE_TOKEN)) { basePath = getFilesDir().getCanonicalPath(); } else if (domain.equals(FullBackup.DATABASE_TREE_TOKEN)) { - basePath = getDatabasePath("foo").getParentFile().getCanonicalPath(); + basePath = getDatabasePath(GENERIC_FILE_NAME).getParentFile().getCanonicalPath(); } else if (domain.equals(FullBackup.ROOT_TREE_TOKEN)) { basePath = new File(getApplicationInfo().dataDir).getCanonicalPath(); } else if (domain.equals(FullBackup.SHAREDPREFS_TREE_TOKEN)) { - basePath = getSharedPrefsFile("foo").getParentFile().getCanonicalPath(); + basePath = getSharedPrefsFile(GENERIC_FILE_NAME).getParentFile().getCanonicalPath(); } else if (domain.equals(FullBackup.CACHE_TREE_TOKEN)) { basePath = getCacheDir().getCanonicalPath(); } else if (domain.equals(FullBackup.MANAGED_EXTERNAL_TREE_TOKEN)) { @@ -713,6 +829,46 @@ public void doFullBackup(ParcelFileDescriptor data, } } + @Override + public void doBackupFiles(ParcelFileDescriptor data, int token, String[] domainTokens, + String excludeFilesRegex, IBackupManager callbackBinder) { + // Ensure that we're running with the app's normal permission level + long ident = Binder.clearCallingIdentity(); + + // Ensure that any SharedPreferences writes have landed *before* + // we potentially try to back up the underlying files directly. + waitForSharedPrefs(); + + try { + BackupAgent.this.onBackupFiles(domainTokens, excludeFilesRegex, + new FullBackupDataOutput(data)); + } catch (IOException ex) { + throw new RuntimeException(ex); + } catch (RuntimeException ex) { + throw ex; + } finally { + // ... and then again after, as in the doBackup() case + waitForSharedPrefs(); + + // Send the EOD marker indicating that there is no more data + // forthcoming from this agent. + try { + FileOutputStream out = new FileOutputStream(data.getFileDescriptor()); + byte[] buf = new byte[4]; + out.write(buf); + } catch (IOException e) { + Log.e(TAG, "Unable to finalize backup stream!"); + } + + Binder.restoreCallingIdentity(ident); + try { + callbackBinder.opComplete(token); + } catch (RemoteException e) { + // we'll time out anyway, so we're safe + } + } + } + @Override public void doRestoreFile(ParcelFileDescriptor data, long size, int type, String domain, String path, long mode, long mtime, diff --git a/core/java/android/app/backup/IBackupManager.aidl b/core/java/android/app/backup/IBackupManager.aidl index 41ad9361a38..8af9cc35c2f 100644 --- a/core/java/android/app/backup/IBackupManager.aidl +++ b/core/java/android/app/backup/IBackupManager.aidl @@ -178,6 +178,31 @@ interface IBackupManager { */ void fullTransportBackup(in String[] packageNames); + /** + * Write a backup of the given package to the supplied file descriptor. + * The fd may be a socket or other non-seekable destination. + * + *

Use this method instead of fullBackup() when non-interactive operations are needed. + * + *

This method is synchronous -- it does not return until the backup has + * completed. + * + *

Callers must hold the android.permission.BACKUP permission to use this method. + * + * @param fd The file descriptor to which a 'tar' file stream is to be written. + * @param domainTokens - The specific files paths (expressed as domain tokens) that should + * be backedup. + * @param excludeFilesRegex - The files that should be excluded, specified as a regex. + * @param packageName - The name of the package. + * @param shouldKillAfterBackup - Should the process be killed once the backup is done. + * @param ignoreEncryptionPasswordCheck - Should encryption password check be ignored + * @param observer - The Backup Observer which is notified of the start / finish / error state + * of the backup. + */ + void fullBackupNoninteractive(in ParcelFileDescriptor fd, in String[] domainTokens, + in String excludeFilesRegex, in String packageName, boolean shouldKillAfterBackup, + boolean ignoreEncryptionPasswordCheck, IFullBackupRestoreObserver observer); + /** * Restore device content from the data stream passed through the given socket. The * data stream must be in the format emitted by fullBackup(). @@ -186,6 +211,17 @@ interface IBackupManager { */ void fullRestore(in ParcelFileDescriptor fd); + /** + * Restore device content from the data stream passed through the given socket. The + * data stream must be in the format emitted by fullBackup(). + * + *

Use this method instead of fullRestore() when non-interactive operations are needed. + * + *

Callers must hold the android.permission.BACKUP permission to use this method. + */ + void fullRestoreNoninteractive(in ParcelFileDescriptor fd, + boolean ignoreEncryptionPasswordCheck, IFullBackupRestoreObserver observer); + /** * Confirm that the requested full backup/restore operation can proceed. The system will * not actually perform the operation described to fullBackup() / fullRestore() unless the diff --git a/core/java/android/app/backup/IFullBackupRestoreObserver.aidl b/core/java/android/app/backup/IFullBackupRestoreObserver.aidl index 3e0b73dc948..d11a8fb13f1 100644 --- a/core/java/android/app/backup/IFullBackupRestoreObserver.aidl +++ b/core/java/android/app/backup/IFullBackupRestoreObserver.aidl @@ -43,6 +43,14 @@ oneway interface IFullBackupRestoreObserver { */ void onEndBackup(); + /** + * Notification: the full backup operation has ended with result. + * + *

To ensure backwards-compatibility, onEndBackup() and onEndBackupWithResult() + * are both called for the same backup operation. + */ + void onEndBackupWithResult(int result); + /** * Notification: a restore-from-full-backup operation has begun. */ @@ -62,6 +70,14 @@ oneway interface IFullBackupRestoreObserver { */ void onEndRestore(); + /** + * Notification: the restore-from-full-backup operation has ended with result. + * + *

To ensure backwards-compatibility, onEndBackup() and onEndBackupWithResult() + * are both called for the same backup operation. + */ + void onEndRestoreWithResult(int result); + /** * The user's window of opportunity for confirming the operation has timed out. */ diff --git a/core/java/android/appwidget/AppWidgetHost.java b/core/java/android/appwidget/AppWidgetHost.java index 8e86824761e..7eb4b2f2a36 100644 --- a/core/java/android/appwidget/AppWidgetHost.java +++ b/core/java/android/appwidget/AppWidgetHost.java @@ -221,10 +221,10 @@ public final void startAppWidgetConfigureActivityForResult(@NonNull Activity act int appWidgetId, int intentFlags, int requestCode, @Nullable Bundle options) { try { IntentSender intentSender = sService.createAppWidgetConfigIntentSender( - mContextOpPackageName, appWidgetId, intentFlags); + mContextOpPackageName, appWidgetId); if (intentSender != null) { - activity.startIntentSenderForResult(intentSender, requestCode, null, 0, 0, 0, - options); + activity.startIntentSenderForResult(intentSender, requestCode, null, 0, + intentFlags, intentFlags, options); } else { throw new ActivityNotFoundException(); } diff --git a/core/java/android/bluetooth/BluetoothA2dpSink.java b/core/java/android/bluetooth/BluetoothA2dpSink.java index 2e273459add..74302f27ec1 100644 --- a/core/java/android/bluetooth/BluetoothA2dpSink.java +++ b/core/java/android/bluetooth/BluetoothA2dpSink.java @@ -370,6 +370,89 @@ && isValidDevice(device)) { return null; } + /** + * Set priority of the profile + * + *

The device should already be paired. + * Priority can be one of {@link #PRIORITY_ON} orgetBluetoothManager + * {@link #PRIORITY_OFF}, + * + *

Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} + * permission. + * + * @param device Paired bluetooth device + * @param priority + * @return true if priority is set, false on error + * @hide + */ + public boolean setPriority(BluetoothDevice device, int priority) { + if (DBG) log("setPriority(" + device + ", " + priority + ")"); + if (mService != null && isEnabled() + && isValidDevice(device)) { + if (priority != BluetoothProfile.PRIORITY_OFF && + priority != BluetoothProfile.PRIORITY_ON){ + return false; + } + try { + return mService.setPriority(device, priority); + } catch (RemoteException e) { + Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); + return false; + } + } + if (mService == null) Log.w(TAG, "Proxy not attached to service"); + return false; + } + + /** + * Get the priority of the profile. + * + *

The priority can be any of: + * {@link #PRIORITY_AUTO_CONNECT}, {@link #PRIORITY_OFF}, + * {@link #PRIORITY_ON}, {@link #PRIORITY_UNDEFINED} + * + *

Requires {@link android.Manifest.permission#BLUETOOTH} permission. + * + * @param device Bluetooth device + * @return priority of the device + * @hide + */ + public int getPriority(BluetoothDevice device) { + if (VDBG) log("getPriority(" + device + ")"); + if (mService != null && isEnabled() + && isValidDevice(device)) { + try { + return mService.getPriority(device); + } catch (RemoteException e) { + Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); + return BluetoothProfile.PRIORITY_OFF; + } + } + if (mService == null) Log.w(TAG, "Proxy not attached to service"); + return BluetoothProfile.PRIORITY_OFF; + } + + /** + * Check if A2DP profile is streaming music. + * + *

Requires {@link android.Manifest.permission#BLUETOOTH} permission. + * + * @param device BluetoothDevice device + */ + public boolean isA2dpPlaying(BluetoothDevice device) { + if (mService != null && isEnabled() + && isValidDevice(device)) { + try { + return mService.isA2dpPlaying(device); + } catch (RemoteException e) { + Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); + return false; + } + } + if (mService == null) Log.w(TAG, "Proxy not attached to service"); + return false; + } + /** * Helper for converting a state to a string. * diff --git a/core/java/android/bluetooth/BluetoothAdapter.java b/core/java/android/bluetooth/BluetoothAdapter.java index b8f4bf8f044..984dbfc3951 100644 --- a/core/java/android/bluetooth/BluetoothAdapter.java +++ b/core/java/android/bluetooth/BluetoothAdapter.java @@ -1,4 +1,7 @@ /* + * Copyright (c) 2013-2014, The Linux Foundation. All rights reserved. + * Not a Contribution. + * * Copyright (C) 2009-2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,6 +21,7 @@ import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; +import android.app.ActivityThread; import android.bluetooth.le.BluetoothLeAdvertiser; import android.bluetooth.le.BluetoothLeScanner; import android.bluetooth.le.ScanCallback; @@ -571,7 +575,7 @@ public boolean enable() { return true; } try { - return mManagerService.enable(); + return mManagerService.enable(ActivityThread.currentPackageName()); } catch (RemoteException e) {Log.e(TAG, "", e);} return false; } @@ -1128,6 +1132,34 @@ public BluetoothServerSocket listenUsingRfcommWithServiceRecord(String name, UUI return createNewRfcommSocketAndRecord(name, uuid, true, true); } + /** + * Create a listening, secure L2CAP Bluetooth socket with Service Record. + *

A remote device connecting to this socket will be authenticated and + * communication on this socket will be encrypted. + *

Use {@link BluetoothServerSocket#accept} to retrieve incoming + * connections from a listening {@link BluetoothServerSocket}. + *

The system will assign an unused L2CAP PSM to listen on. + *

The system will also register a Service Discovery + * Protocol (SDP) record with the local SDP server containing the specified + * UUID, service name, and auto-assigned PSM number. Remote Bluetooth devices + * can use the same UUID to query our SDP server and discover which PSM + * to connect to. This SDP record will be removed when this socket is + * closed, or if this application closes unexpectedly. + *

Use {@link BluetoothDevice#createL2capSocketToServiceRecord} to + * connect to this socket from another device using the same {@link UUID}. + *

Requires {@link android.Manifest.permission#BLUETOOTH} + * @param name service name for SDP record + * @param uuid uuid for SDP record + * @return a listening L2CAP BluetoothServerSocket + * @throws IOException on error, for example Bluetooth not available, or + * insufficient permissions, or channel in use. + * @hide + */ + public BluetoothServerSocket listenUsingL2capWithServiceRecord(String name, UUID uuid) + throws IOException { + return createNewL2capSocketAndRecord(name, uuid, true, true); + } + /** * Create a listening, insecure RFCOMM Bluetooth socket with Service Record. *

The link key is not required to be authenticated, i.e the communication may be @@ -1159,6 +1191,38 @@ public BluetoothServerSocket listenUsingInsecureRfcommWithServiceRecord(String n return createNewRfcommSocketAndRecord(name, uuid, false, false); } + /** + * Create a listening, insecure L2CAP Bluetooth socket with Service Record. + *

The link key is not required to be authenticated, i.e the communication may be + * vulnerable to Man In the Middle attacks. For Bluetooth 2.1 devices, + * the link will be encrypted, as encryption is mandartory. + * For legacy devices (pre Bluetooth 2.1 devices) the link will not + * be encrypted. Use {@link #listenUsingL2capWithServiceRecord}, if an + * encrypted and authenticated communication channel is desired. + *

Use {@link BluetoothServerSocket#accept} to retrieve incoming + * connections from a listening {@link BluetoothServerSocket}. + *

The system will assign an unused L2CAP PSM to listen on. + *

The system will also register a Service Discovery + * Protocol (SDP) record with the local SDP server containing the specified + * UUID, service name, and auto-assigned PSM number. Remote Bluetooth devices + * can use the same UUID to query our SDP server and discover which PSM + * to connect to. This SDP record will be removed when this socket is + * closed, or if this application closes unexpectedly. + *

Use {@link BluetoothDevice#createL2capSocketToServiceRecord} to + * connect to this socket from another device using the same {@link UUID}. + *

Requires {@link android.Manifest.permission#BLUETOOTH} + * @param name service name for SDP record + * @param uuid uuid for SDP record + * @return a listening L2CAP BluetoothServerSocket + * @throws IOException on error, for example Bluetooth not available, or + * insufficient permissions, or channel in use. + * @hide + */ + public BluetoothServerSocket listenUsingInsecureL2capWithServiceRecord(String name, UUID uuid) + throws IOException { + return createNewL2capSocketAndRecord(name, uuid, false, false); + } + /** * Create a listening, encrypted, * RFCOMM Bluetooth socket with Service Record. @@ -1214,6 +1278,22 @@ private BluetoothServerSocket createNewRfcommSocketAndRecord(String name, UUID u return socket; } + private BluetoothServerSocket createNewL2capSocketAndRecord(String name, UUID uuid, + boolean auth, boolean encrypt) throws IOException { + BluetoothServerSocket socket; + socket = new BluetoothServerSocket(BluetoothSocket.TYPE_L2CAP, auth, + encrypt, new ParcelUuid(uuid)); + socket.setServiceName(name); + int errno = socket.mSocket.bindListen(); + if (errno != 0) { + //TODO(BT): Throw the same exception error code + // that the previous code was using. + //socket.mSocket.throwErrnoNative(errno); + throw new IOException("Error: " + errno); + } + return socket; + } + /** * Construct an unencrypted, unauthenticated, RFCOMM server socket. * Call #accept to retrieve connections to this socket. @@ -1347,6 +1427,12 @@ public boolean getProfileProxy(Context context, BluetoothProfile.ServiceListener } else if (profile == BluetoothProfile.PAN) { BluetoothPan pan = new BluetoothPan(context, listener); return true; + } else if (profile == BluetoothProfile.DUN) { + BluetoothDun dun = new BluetoothDun(context, listener); + return true; + } else if (profile == BluetoothProfile.SAP) { + BluetoothSap sap = new BluetoothSap(context, listener); + return true; } else if (profile == BluetoothProfile.HEALTH) { BluetoothHealth health = new BluetoothHealth(context, listener); return true; @@ -1356,6 +1442,9 @@ public boolean getProfileProxy(Context context, BluetoothProfile.ServiceListener } else if (profile == BluetoothProfile.HEADSET_CLIENT) { BluetoothHeadsetClient headsetClient = new BluetoothHeadsetClient(context, listener); return true; + } else if (profile == BluetoothProfile.HID_DEVICE) { + BluetoothHidDevice hidd = new BluetoothHidDevice(context, listener); + return true; } else { return false; } @@ -1400,6 +1489,13 @@ public void closeProfileProxy(int profile, BluetoothProfile proxy) { BluetoothPan pan = (BluetoothPan)proxy; pan.close(); break; + case BluetoothProfile.DUN: + BluetoothDun dun = (BluetoothDun)proxy; + dun.close(); + case BluetoothProfile.SAP: + BluetoothSap sap = (BluetoothSap)proxy; + sap.close(); + break; case BluetoothProfile.HEALTH: BluetoothHealth health = (BluetoothHealth)proxy; health.close(); @@ -1420,6 +1516,10 @@ public void closeProfileProxy(int profile, BluetoothProfile proxy) { BluetoothHeadsetClient headsetClient = (BluetoothHeadsetClient)proxy; headsetClient.close(); break; + case BluetoothProfile.HID_DEVICE: + BluetoothHidDevice hidd = (BluetoothHidDevice) proxy; + hidd.close(); + break; } } @@ -1654,6 +1754,7 @@ public interface LeScanCallback { */ @Deprecated public boolean startLeScan(LeScanCallback callback) { + if (getState() != STATE_ON) return false; return startLeScan(null, callback); } @@ -1761,6 +1862,7 @@ public void onScanResult(int callbackType, ScanResult result) { @Deprecated public void stopLeScan(LeScanCallback callback) { if (DBG) Log.d(TAG, "stopLeScan()"); + if (getState() != STATE_ON) return; BluetoothLeScanner scanner = getBluetoothLeScanner(); if (scanner == null) { return; diff --git a/core/java/android/bluetooth/BluetoothClass.java b/core/java/android/bluetooth/BluetoothClass.java index 54bf4afa93f..4a38287e7ca 100644 --- a/core/java/android/bluetooth/BluetoothClass.java +++ b/core/java/android/bluetooth/BluetoothClass.java @@ -283,6 +283,8 @@ public int getDeviceClass() { public static final int PROFILE_PANU = 4; /** @hide */ public static final int PROFILE_NAP = 5; + /** @hide */ + public static final int PROFILE_A2DP_SINK = 6; /** * Check class bits for possible bluetooth profile support. @@ -310,6 +312,21 @@ public boolean doesClassMatch(int profile) { default: return false; } + } else if (profile == PROFILE_A2DP_SINK) { + if (hasService(Service.CAPTURE)) { + return true; + } + // By the A2DP spec, srcs must indicate the CAPTURE service. + // However if some device that do not, we try to + // match on some other class bits. + switch (getDeviceClass()) { + case Device.AUDIO_VIDEO_HIFI_AUDIO: + case Device.AUDIO_VIDEO_SET_TOP_BOX: + case Device.AUDIO_VIDEO_VCR : + return true; + default: + return false; + } } else if (profile == PROFILE_HEADSET) { // The render service class is required by the spec for HFP, so is a // pretty good signal diff --git a/core/java/android/bluetooth/BluetoothDevice.java b/core/java/android/bluetooth/BluetoothDevice.java index bb0d0a39f56..1d713d86eea 100644 --- a/core/java/android/bluetooth/BluetoothDevice.java +++ b/core/java/android/bluetooth/BluetoothDevice.java @@ -276,10 +276,10 @@ public final class BluetoothDevice implements Parcelable { public static final String EXTRA_PAIRING_VARIANT = "android.bluetooth.device.extra.PAIRING_VARIANT"; - /** - * Used as an int extra field in {@link #ACTION_PAIRING_REQUEST} - * intents as the value of passkey. - */ + /** @hide */ + public static final String EXTRA_SECURE_PAIRING = + "codeaurora.bluetooth.device.extra.SECURE"; + public static final String EXTRA_PAIRING_KEY = "android.bluetooth.device.extra.PAIRING_KEY"; /** @@ -1004,6 +1004,46 @@ public BluetoothClass getBluetoothClass() { return null; } + /** + * Get trust state of a remote device. + *

Requires {@link android.Manifest.permission#BLUETOOTH}. + * @return true/false + * @hide + */ + public boolean getTrustState() { + if (sService == null) { + Log.e(TAG, "BT not enabled. Cannot get Remote Device Alias"); + return false; + } + + try { + return sService.getRemoteTrust(this); + } catch (RemoteException e) { + Log.e(TAG, "", e); + } + return false; + } + + /** + * Set trust state for a remote device. + *

Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}. + * @param value the trust state value (true or false) + * @return true/false + * @hide + */ + public boolean setTrust(boolean trustValue) { + if (sService == null) { + Log.e(TAG, "BT not enabled. Cannot set Remote Device name"); + return false; + } + try { + return sService.setRemoteTrust(this, trustValue); + } catch (RemoteException e) { + Log.e(TAG, "", e); + } + return false; + } + /** * Returns the supported features (UUIDs) of the remote device. * @@ -1296,6 +1336,33 @@ public BluetoothSocket createRfcommSocketToServiceRecord(UUID uuid) throws IOExc new ParcelUuid(uuid)); } + /** + * Create an L2CAP {@link BluetoothSocket} ready to start a secure + * outgoing connection to this remote device using SDP lookup of uuid. + *

This is designed to be used with {@link + * BluetoothAdapter#listenUsingL2capWithServiceRecord} for peer-peer + * Bluetooth applications. + *

Use {@link BluetoothSocket#connect} to initiate the outgoing + * connection. This will also perform an SDP lookup of the given uuid to + * determine which channel to connect to. + *

The remote device will be authenticated and communication on this + * socket will be encrypted. + *

Use this socket only if an authenticated socket link is possible. + * Authentication refers to the authentication of the link key to + * prevent man-in-the-middle type of attacks. + *

Requires {@link android.Manifest.permission#BLUETOOTH} + * + * @param uuid service record uuid to lookup L2CAP PSM + * @return a L2CAP BluetoothServerSocket ready for an outgoing connection + * @throws IOException on error, for example Bluetooth not available, or + * insufficient permissions + * @hide + */ + public BluetoothSocket createL2capSocketToServiceRecord(UUID uuid) throws IOException { + return new BluetoothSocket(BluetoothSocket.TYPE_L2CAP, -1, true, true, this, -1, + new ParcelUuid(uuid)); + } + /** * Create an RFCOMM {@link BluetoothSocket} socket ready to start an insecure * outgoing connection to this remote device using SDP lookup of uuid. @@ -1329,6 +1396,37 @@ public BluetoothSocket createInsecureRfcommSocketToServiceRecord(UUID uuid) thro new ParcelUuid(uuid)); } + /** + * Create an L2CAP {@link BluetoothSocket} socket ready to start an insecure + * outgoing connection to this remote device using SDP lookup of uuid. + *

The communication channel will not have an authenticated link key + * i.e it will be subject to man-in-the-middle attacks. For Bluetooth 2.1 + * devices, the link key will be encrypted, as encryption is mandatory. + * For legacy devices (pre Bluetooth 2.1 devices) the link key will + * be not be encrypted. Use {@link #createL2capSocketToServiceRecord} if an + * encrypted and authenticated communication channel is desired. + *

This is designed to be used with {@link + * BluetoothAdapter#listenUsingInsecureL2capWithServiceRecord} for peer-peer + * Bluetooth applications. + *

Use {@link BluetoothSocket#connect} to initiate the outgoing + * connection. This will also perform an SDP lookup of the given uuid to + * determine which l2cap psm to connect to. + *

The remote device will be authenticated and communication on this + * socket will be encrypted. + *

Requires {@link android.Manifest.permission#BLUETOOTH} + * + * @param uuid service record uuid to lookup L2CAP PSM + * @return a L2CAP BluetoothServerSocket ready for an outgoing connection + * @throws IOException on error, for example Bluetooth not available, or + * insufficient permissions + * @hide + */ + public BluetoothSocket createInsecureL2capSocketToServiceRecord(UUID uuid) throws IOException { + return new BluetoothSocket(BluetoothSocket.TYPE_L2CAP, -1, false, false, this, -1, + new ParcelUuid(uuid)); + } + + /** * Construct an insecure RFCOMM socket ready to start an outgoing * connection. @@ -1439,4 +1537,24 @@ public BluetoothGatt connectGatt(Context context, boolean autoConnect, } catch (RemoteException e) {Log.e(TAG, "", e);} return null; } + + /** + * Get Di record of a remote device. + *

Requires {@link android.Manifest.permission#BLUETOOTH}. + * @return DiRecord or null + * @hide + */ + public BluetoothRemoteDiRecord getRemoteDiRecord() { + if (sService == null) { + Log.e(TAG, "BT not enabled. Cannot get Remote Di record"); + return null; + } + + try { + return sService.getRemoteDiRecord(this); + } catch (RemoteException e) { + Log.e(TAG, "", e); + } + return null; + } } diff --git a/core/java/android/bluetooth/BluetoothDevicePicker.java b/core/java/android/bluetooth/BluetoothDevicePicker.java index c794be2e2c9..51d14cc0a11 100644 --- a/core/java/android/bluetooth/BluetoothDevicePicker.java +++ b/core/java/android/bluetooth/BluetoothDevicePicker.java @@ -43,6 +43,14 @@ public interface BluetoothDevicePicker { public static final String ACTION_DEVICE_SELECTED = "android.bluetooth.devicepicker.action.DEVICE_SELECTED"; + /** + * Broadcast when no BT device is selected from BT device picker screen. + * This happens when user presses back button. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_DEVICE_NOT_SELECTED = + "org.codeaurora.bluetooth.devicepicker.action.DEVICE_NOT_SELECTED"; + /** * Broadcast when someone want to select one BT device from devices list. * This intent contains below extra data: diff --git a/core/java/android/bluetooth/BluetoothDun.java b/core/java/android/bluetooth/BluetoothDun.java new file mode 100644 index 00000000000..79444c94d53 --- /dev/null +++ b/core/java/android/bluetooth/BluetoothDun.java @@ -0,0 +1,294 @@ +/* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of The Linux Foundation nor + * the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NON-INFRINGEMENT ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package android.bluetooth; + +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class provides the APIs to control the Bluetooth Dun + * Profile. + * + *

BluetoothDun is a proxy object for controlling the Bluetooth DUN + * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get + * the BluetoothDun proxy object. + * + *

Each method is protected with its appropriate permission. + *@hide + */ +public final class BluetoothDun implements BluetoothProfile { + private static final String TAG = "BluetoothDun"; + private static final boolean DBG = false; + private static final boolean VDBG = false; + + /** + * Intent used to broadcast the change in connection state of the Dun + * profile. + * + *

This intent will have 3 extras: + *

    + *
  • {@link #EXTRA_STATE} - The current state of the profile.
  • + *
  • {@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile.
  • + *
  • {@link BluetoothDevice#EXTRA_DEVICE} - The remote device.
  • + *
+ * + *

{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of + * {@link #STATE_DISCONNECTED}, {@link #STATE_CONNECTED}. + * + *

Requires {@link android.Manifest.permission#BLUETOOTH} permission to + * receive. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_CONNECTION_STATE_CHANGED = + "codeaurora.bluetooth.dun.profile.action.CONNECTION_STATE_CHANGED"; + + private Context mContext; + private ServiceListener mServiceListener; + private BluetoothAdapter mAdapter; + private IBluetoothDun mDunService; + + /** + * Create a BluetoothDun proxy object for interacting with the local + * Bluetooth Service which handles the Dun profile + * + */ + /*package*/ BluetoothDun(Context context, ServiceListener l) { + mContext = context; + mServiceListener = l; + mAdapter = BluetoothAdapter.getDefaultAdapter(); + try { + mAdapter.getBluetoothManager().registerStateChangeCallback(mStateChangeCallback); + } catch (RemoteException re) { + Log.w(TAG,"Unable to register BluetoothStateChangeCallback",re); + } + Log.d(TAG, "BluetoothDun() call bindService"); + doBind(); + } + + boolean doBind() { + Intent intent = new Intent(IBluetoothDun.class.getName()); + ComponentName comp = intent.resolveSystemService(mContext.getPackageManager(), 0); + intent.setComponent(comp); + if (comp == null || !mContext.bindServiceAsUser(intent, mConnection, 0, + android.os.Process.myUserHandle())) { + Log.e(TAG, "Could not bind to Bluetooth Dun Service with " + intent); + return false; + } + return true; + } + + + /*package*/ void close() { + if (VDBG) log("close()"); + mServiceListener = null; + IBluetoothManager mgr = mAdapter.getBluetoothManager(); + if (mgr != null) { + try { + mgr.unregisterStateChangeCallback(mStateChangeCallback); + } catch (RemoteException re) { + Log.w(TAG,"Unable to unregister BluetoothStateChangeCallback",re); + } + } + + synchronized (mConnection) { + if ( mDunService != null) { + try { + mDunService = null; + mContext.unbindService(mConnection); + } catch (Exception re) { + Log.e(TAG,"",re); + } + } + } + } + + protected void finalize() { + close(); + } + + private IBluetoothStateChangeCallback mStateChangeCallback = + new IBluetoothStateChangeCallback.Stub() { + + @Override + public void onBluetoothStateChange(boolean on) { + //Handle enable request to bind again. + Log.d(TAG, "onBluetoothStateChange on: " + on); + if (on) { + try { + if (mDunService == null) { + Log.d(TAG, "onBluetoothStateChange call bindService"); + doBind(); + } + } catch (IllegalStateException e) { + Log.e(TAG,"onBluetoothStateChange: could not bind to DUN service: ", e); + } catch (SecurityException e) { + Log.e(TAG,"onBluetoothStateChange: could not bind to DUN service: ", e); + } + } else { + if (VDBG) Log.d(TAG,"Unbinding service..."); + synchronized (mConnection) { + if ( mDunService != null) { + try { + mDunService = null; + mContext.unbindService(mConnection); + } catch (Exception re) { + Log.e(TAG,"",re); + } + } + } + } + } + }; + + /** + * Initiate disconnection from DUN server. + * + *

Once the disconnection is initiated by any device either local host + * or remote device, the state will transition from {@link #STATE_CONNECTED} + * to {@link #STATE_DISCONNECTED}. + * + *

Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} + * permission. + * + * @param device Remote Bluetooth Device + * @return false on immediate error, + * true otherwise + * @hide + */ + public boolean disconnect(BluetoothDevice device) { + if (DBG) log("disconnect(" + device + ")"); + if (mDunService != null && isEnabled() && + isValidDevice(device)) { + try { + return mDunService.disconnect(device); + } catch (RemoteException e) { + Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); + return false; + } + } + if (mDunService == null) Log.w(TAG, "Proxy not attached to service"); + return false; + } + /** + * {@inheritDoc} + */ + public List getConnectedDevices() { + if (VDBG) log("getConnectedDevices()"); + if (mDunService != null && isEnabled()) { + try { + return mDunService.getConnectedDevices(); + } catch (RemoteException e) { + Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); + return new ArrayList(); + } + } + if (mDunService == null) Log.w(TAG, "Proxy not attached to service"); + return new ArrayList(); + } + + /** + * {@inheritDoc} + */ + public List getDevicesMatchingConnectionStates(int[] states) { + if (VDBG) log("getDevicesMatchingStates()"); + if (mDunService != null && isEnabled()) { + try { + return mDunService.getDevicesMatchingConnectionStates(states); + } catch (RemoteException e) { + Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); + return new ArrayList(); + } + } + if (mDunService == null) Log.w(TAG, "Proxy not attached to service"); + return new ArrayList(); + } + + /** + * {@inheritDoc} + */ + public int getConnectionState(BluetoothDevice device) { + if (VDBG) log("getState(" + device + ")"); + if (mDunService != null && isEnabled() + && isValidDevice(device)) { + try { + return mDunService.getConnectionState(device); + } catch (RemoteException e) { + Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); + return BluetoothProfile.STATE_DISCONNECTED; + } + } + if (mDunService == null) Log.w(TAG, "Proxy not attached to service"); + return BluetoothProfile.STATE_DISCONNECTED; + } + + private ServiceConnection mConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder service) { + if (DBG) Log.d(TAG, "BluetoothDUN Proxy object connected"); + mDunService = IBluetoothDun.Stub.asInterface(service); + + if (mServiceListener != null) { + mServiceListener.onServiceConnected(BluetoothProfile.DUN, + BluetoothDun.this); + } + } + public void onServiceDisconnected(ComponentName className) { + if (DBG) Log.d(TAG, "BluetoothDUN Proxy object disconnected"); + mDunService = null; + if (mServiceListener != null) { + mServiceListener.onServiceDisconnected(BluetoothProfile.DUN); + } + } + }; + + private boolean isEnabled() { + if (mAdapter.getState() == BluetoothAdapter.STATE_ON) return true; + return false; + } + + private boolean isValidDevice(BluetoothDevice device) { + if (device == null) return false; + + if (BluetoothAdapter.checkBluetoothAddress(device.getAddress())) return true; + return false; + } + + private static void log(String msg) { + Log.d(TAG, msg); + } +} diff --git a/core/java/android/bluetooth/BluetoothGatt.java b/core/java/android/bluetooth/BluetoothGatt.java index ea2dca08fd4..63e76ce6b1f 100644 --- a/core/java/android/bluetooth/BluetoothGatt.java +++ b/core/java/android/bluetooth/BluetoothGatt.java @@ -16,6 +16,7 @@ package android.bluetooth; +import android.bluetooth.BluetoothAdapter; import android.content.Context; import android.os.ParcelUuid; import android.os.RemoteException; @@ -628,6 +629,8 @@ public void onConfigureMTU(String address, int mtu, int status) { public void close() { if (DBG) Log.d(TAG, "close()"); + BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); + if (adapter.getState() != adapter.STATE_ON) return ; unregisterApp(); mConnState = CONN_STATE_CLOSED; } @@ -727,6 +730,7 @@ private void unregisterApp() { } mConnState = CONN_STATE_CONNECTING; } + mAutoConnect = autoConnect; if (!registerApp(callback)) { synchronized(mStateLock) { mConnState = CONN_STATE_IDLE; @@ -736,7 +740,6 @@ private void unregisterApp() { } // the connection will continue after successful callback registration - mAutoConnect = autoConnect; return true; } @@ -750,6 +753,8 @@ public void disconnect() { if (DBG) Log.d(TAG, "cancelOpen() - device: " + mDevice.getAddress()); if (mService == null || mClientIf == 0) return; + BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); + if (adapter.getState() != adapter.STATE_ON) return ; try { mService.clientDisconnect(mClientIf, mDevice.getAddress()); } catch (RemoteException e) { diff --git a/core/java/android/bluetooth/BluetoothHeadset.java b/core/java/android/bluetooth/BluetoothHeadset.java index 25d9aa9bae6..bf2dadf9f24 100644 --- a/core/java/android/bluetooth/BluetoothHeadset.java +++ b/core/java/android/bluetooth/BluetoothHeadset.java @@ -47,7 +47,7 @@ public final class BluetoothHeadset implements BluetoothProfile { private static final String TAG = "BluetoothHeadset"; private static final boolean DBG = true; - private static final boolean VDBG = false; + private static final boolean VDBG = Log.isLoggable("Handsfree", Log.VERBOSE); /** * Intent used to broadcast the change in connection state of the Headset diff --git a/core/java/android/bluetooth/BluetoothHidDevice.java b/core/java/android/bluetooth/BluetoothHidDevice.java new file mode 100644 index 00000000000..4995dda2f2a --- /dev/null +++ b/core/java/android/bluetooth/BluetoothHidDevice.java @@ -0,0 +1,501 @@ +/* + * Copyright (C) 2013 The Linux Foundation. All rights reserved + * Not a Contribution. + * Copyright (C) 2011 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.bluetooth; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; + +import java.util.Arrays; +import java.util.List; + +/** + * @hide + */ +public final class BluetoothHidDevice implements BluetoothProfile { + + private static final String TAG = BluetoothHidDevice.class.getSimpleName(); + + public static final String ACTION_CONNECTION_STATE_CHANGED = + "android.bluetooth.hid.profile.action.CONNECTION_STATE_CHANGED"; + + /** + * Constants representing device subclass. + * + * @see #registerApp(String, String, String, byte, byte[], + * BluetoothHidDeviceCallback) + */ + public static final byte SUBCLASS1_NONE = (byte) 0x00; + public static final byte SUBCLASS1_KEYBOARD = (byte) 0x40; + public static final byte SUBCLASS1_MOUSE = (byte) 0x80; + public static final byte SUBCLASS1_COMBO = (byte) 0xC0; + + public static final byte SUBCLASS2_UNCATEGORIZED = (byte) 0x00; + public static final byte SUBCLASS2_JOYSTICK = (byte) 0x01; + public static final byte SUBCLASS2_GAMEPAD = (byte) 0x02; + public static final byte SUBCLASS2_REMOTE_CONTROL = (byte) 0x03; + public static final byte SUBCLASS2_SENSING_DEVICE = (byte) 0x04; + public static final byte SUBCLASS2_DIGITIZER_TABLED = (byte) 0x05; + public static final byte SUBCLASS2_CARD_READER = (byte) 0x06; + + /** + * Constants representing report types. + * + * @see BluetoothHidDeviceCallback#onGetReport(byte, byte, int) + * @see BluetoothHidDeviceCallback#onSetReport(byte, byte, byte[]) + * @see BluetoothHidDeviceCallback#onIntrData(byte, byte[]) + */ + public static final byte REPORT_TYPE_INPUT = (byte) 1; + public static final byte REPORT_TYPE_OUTPUT = (byte) 2; + public static final byte REPORT_TYPE_FEATURE = (byte) 3; + + /** + * Constants representing error response for Set Report. + * + * @see BluetoothHidDeviceCallback#onSetReport(byte, byte, byte[]) + */ + public static final byte ERROR_RSP_SUCCESS = (byte) 0; + public static final byte ERROR_RSP_NOT_READY = (byte) 1; + public static final byte ERROR_RSP_INVALID_RPT_ID = (byte) 2; + public static final byte ERROR_RSP_UNSUPPORTED_REQ = (byte) 3; + public static final byte ERROR_RSP_INVALID_PARAM = (byte) 4; + public static final byte ERROR_RSP_UNKNOWN = (byte) 14; + + /** + * Constants representing protocol mode used set by host. Default is always + * {@link #PROTOCOL_REPORT_MODE} unless notified otherwise. + * + * @see BluetoothHidDeviceCallback#onSetProtocol(byte) + */ + public static final byte PROTOCOL_BOOT_MODE = (byte) 0; + public static final byte PROTOCOL_REPORT_MODE = (byte) 1; + + private Context mContext; + + private ServiceListener mServiceListener; + + private IBluetoothHidDevice mService; + + private BluetoothAdapter mAdapter; + + private static class BluetoothHidDeviceCallbackWrapper extends IBluetoothHidDeviceCallback.Stub { + + private BluetoothHidDeviceCallback mCallback; + + public BluetoothHidDeviceCallbackWrapper(BluetoothHidDeviceCallback callback) { + mCallback = callback; + } + + @Override + public void onAppStatusChanged(BluetoothDevice pluggedDevice, + BluetoothHidDeviceAppConfiguration config, boolean registered) { + mCallback.onAppStatusChanged(pluggedDevice, config, registered); + } + + @Override + public void onConnectionStateChanged(BluetoothDevice device, int state) { + mCallback.onConnectionStateChanged(device, state); + } + + @Override + public void onGetReport(byte type, byte id, int bufferSize) { + mCallback.onGetReport(type, id, bufferSize); + } + + @Override + public void onSetReport(byte type, byte id, byte[] data) { + mCallback.onSetReport(type, id, data); + } + + @Override + public void onSetProtocol(byte protocol) { + mCallback.onSetProtocol(protocol); + } + + @Override + public void onIntrData(byte reportId, byte[] data) { + mCallback.onIntrData(reportId, data); + } + + @Override + public void onVirtualCableUnplug() { + mCallback.onVirtualCableUnplug(); + } + } + + final private IBluetoothStateChangeCallback mBluetoothStateChangeCallback = + new IBluetoothStateChangeCallback.Stub() { + + public void onBluetoothStateChange(boolean up) { + Log.d(TAG, "onBluetoothStateChange: up=" + up); + synchronized (mConnection) { + if (!up) { + Log.d(TAG,"Unbinding service..."); + if (mService != null) { + mService = null; + try { + mContext.unbindService(mConnection); + } catch (IllegalArgumentException e) { + Log.e(TAG,"onBluetoothStateChange: could not unbind service:", e); + } + } + } else { + try { + if (mService == null) { + Log.d(TAG,"Binding HID Device service..."); + doBind(); + } + } catch (IllegalStateException e) { + Log.e(TAG,"onBluetoothStateChange: could not bind to HID Dev service: ", e); + } catch (SecurityException e) { + Log.e(TAG,"onBluetoothStateChange: could not bind to HID Dev service: ", e); + } + } + } + } + }; + + private ServiceConnection mConnection = new ServiceConnection() { + + public void onServiceConnected(ComponentName className, IBinder service) { + Log.d(TAG, "onServiceConnected()"); + + mService = IBluetoothHidDevice.Stub.asInterface(service); + + if (mServiceListener != null) { + mServiceListener.onServiceConnected(BluetoothProfile.HID_DEVICE, + BluetoothHidDevice.this); + } + } + + public void onServiceDisconnected(ComponentName className) { + Log.d(TAG, "onServiceDisconnected()"); + + mService = null; + + if (mServiceListener != null) { + mServiceListener.onServiceDisconnected(BluetoothProfile.HID_DEVICE); + } + } + }; + + BluetoothHidDevice(Context context, ServiceListener listener) { + Log.v(TAG, "BluetoothHidDevice"); + + mContext = context; + mServiceListener = listener; + mAdapter = BluetoothAdapter.getDefaultAdapter(); + + IBluetoothManager mgr = mAdapter.getBluetoothManager(); + if (mgr != null) { + try { + mgr.registerStateChangeCallback(mBluetoothStateChangeCallback); + } catch (RemoteException e) { + e.printStackTrace(); + } + } + + doBind(); + } + + boolean doBind() { + Intent intent = new Intent(IBluetoothHidDevice.class.getName()); + ComponentName comp = intent.resolveSystemService(mContext.getPackageManager(), 0); + intent.setComponent(comp); + if (comp == null || !mContext.bindService(intent, mConnection, 0)) { + Log.e(TAG, "Could not bind to Bluetooth HID Device Service with " + intent); + return false; + } + Log.d(TAG, "Bound to HID Device Service"); + return true; + } + + void close() { + Log.v(TAG, "close()"); + + IBluetoothManager mgr = mAdapter.getBluetoothManager(); + if (mgr != null) { + try { + mgr.unregisterStateChangeCallback(mBluetoothStateChangeCallback); + } catch (RemoteException e) { + e.printStackTrace(); + } + } + + synchronized (mConnection) { + if (mService != null) { + mService = null; + try { + mContext.unbindService(mConnection); + } catch (IllegalArgumentException e) { + Log.e(TAG,"close: could not unbind HID Dev service: ", e); + } + } + } + + mServiceListener = null; + } + + @Override + public List getConnectedDevices() { + Log.v(TAG, "getConnectedDevices()"); + return null; + } + + @Override + public List getDevicesMatchingConnectionStates(int[] states) { + Log.v(TAG, "getDevicesMatchingConnectionStates(): states=" + Arrays.toString(states)); + return null; + } + + @Override + public int getConnectionState(BluetoothDevice device) { + Log.v(TAG, "getConnectionState(): device=" + device.getAddress()); + + return STATE_DISCONNECTED; + } + + /** + * Registers application to be used for HID device. Connections to HID + * Device are only possible when application is registered. Only one + * application can be registered at time. When no longer used, application + * should be unregistered using + * {@link #unregisterApp(BluetoothHidDeviceAppConfiguration)}. + * + * @param sdp {@link BluetoothHidDeviceAppSdpSettings} object of + * HID Device SDP record. + * @param inQos {@link BluetoothHidDeviceAppQosSettings} object of + * Incoming QoS Settings. + * @param outQos {@link BluetoothHidDeviceAppQosSettings} object of + * Outgoing QoS Settings. + * @param callback {@link BluetoothHidDeviceCallback} object to which + * callback messages will be sent. + * @return + */ + public boolean registerApp(BluetoothHidDeviceAppSdpSettings sdp, + BluetoothHidDeviceAppQosSettings inQos, BluetoothHidDeviceAppQosSettings outQos, + BluetoothHidDeviceCallback callback) { + Log.v(TAG, "registerApp(): sdp=" + sdp + " inQos=" + inQos + " outQos=" + outQos + + " callback=" + callback); + + boolean result = false; + + if (sdp == null || callback == null) { + return false; + } + + if (mService != null) { + try { + BluetoothHidDeviceAppConfiguration config = + new BluetoothHidDeviceAppConfiguration(); + BluetoothHidDeviceCallbackWrapper cbw = + new BluetoothHidDeviceCallbackWrapper(callback); + result = mService.registerApp(config, sdp, inQos, outQos, cbw); + } catch (RemoteException e) { + Log.e(TAG, e.toString()); + } + } else { + Log.w(TAG, "Proxy not attached to service"); + } + + return result; + } + + /** + * Unregisters application. Active connection will be disconnected and no + * new connections will be allowed until registered again using + * {@link #registerApp(String, String, String, byte, byte[], BluetoothHidDeviceCallback)} + * + * @param config {@link BluetoothHidDeviceAppConfiguration} object as + * obtained from + * {@link BluetoothHidDeviceCallback#onAppStatusChanged(BluetoothDevice, + * BluetoothHidDeviceAppConfiguration, boolean)} + * + * @return + */ + public boolean unregisterApp(BluetoothHidDeviceAppConfiguration config) { + Log.v(TAG, "unregisterApp()"); + + boolean result = false; + + if (mService != null) { + try { + result = mService.unregisterApp(config); + } catch (RemoteException e) { + Log.e(TAG, e.toString()); + } + } else { + Log.w(TAG, "Proxy not attached to service"); + } + + return result; + } + + /** + * Sends report to remote host using interrupt channel. + * + * @param id Report Id, as defined in descriptor. Can be 0 in case Report Id + * are not defined in descriptor. + * @param data Report data, not including Report Id. + * @return + */ + public boolean sendReport(int id, byte[] data) { + Log.v(TAG, "sendReport(): id=" + id); + + boolean result = false; + + if (mService != null) { + try { + result = mService.sendReport(id, data); + } catch (RemoteException e) { + Log.e(TAG, e.toString()); + } + } else { + Log.w(TAG, "Proxy not attached to service"); + } + + return result; + } + + /** + * Sends report to remote host as reply for GET_REPORT request from + * {@link BluetoothHidDeviceCallback#onGetReport(byte, byte, int)}. + * + * @param type Report Type, as in request. + * @param id Report Id, as in request. + * @param data Report data, not including Report Id. + * @return + */ + public boolean replyReport(byte type, byte id, byte[] data) { + Log.v(TAG, "replyReport(): type=" + type + " id=" + id); + + boolean result = false; + + if (mService != null) { + try { + result = mService.replyReport(type, id, data); + } catch (RemoteException e) { + Log.e(TAG, e.toString()); + } + } else { + Log.w(TAG, "Proxy not attached to service"); + } + + return result; + } + + /** + * Sends error handshake message as reply for invalid SET_REPORT request + * from {@link BluetoothHidDeviceCallback#onSetReport(byte, byte, byte[])}. + * + * @param error Error to be sent for SET_REPORT via HANDSHAKE. + * @return + */ + public boolean reportError(byte error) { + Log.v(TAG, "reportError(): error = " + error); + + boolean result = false; + + if (mService != null) { + try { + result = mService.reportError(error); + } catch (RemoteException e) { + Log.e(TAG, e.toString()); + } + } else { + Log.w(TAG, "Proxy not attached to service"); + } + + return result; + } + + /** + * Sends Virtual Cable Unplug to currently connected host. + * + * @return + */ + public boolean unplug() { + Log.v(TAG, "unplug()"); + + boolean result = false; + + if (mService != null) { + try { + result = mService.unplug(); + } catch (RemoteException e) { + Log.e(TAG, e.toString()); + } + } else { + Log.w(TAG, "Proxy not attached to service"); + } + + return result; + } + + /** + * Initiates connection to host which currently has Virtual Cable + * established with device. + * + * @return + */ + public boolean connect() { + Log.v(TAG, "connect()"); + + boolean result = false; + + if (mService != null) { + try { + result = mService.connect(); + } catch (RemoteException e) { + Log.e(TAG, e.toString()); + } + } else { + Log.w(TAG, "Proxy not attached to service"); + } + + return result; + } + + /** + * Disconnects from currently connected host. + * + * @return + */ + public boolean disconnect() { + Log.v(TAG, "disconnect()"); + + boolean result = false; + + if (mService != null) { + try { + result = mService.disconnect(); + } catch (RemoteException e) { + Log.e(TAG, e.toString()); + } + } else { + Log.w(TAG, "Proxy not attached to service"); + } + + return result; + } +} diff --git a/core/java/android/bluetooth/BluetoothHidDeviceAppConfiguration.aidl b/core/java/android/bluetooth/BluetoothHidDeviceAppConfiguration.aidl new file mode 100644 index 00000000000..1af309c67ca --- /dev/null +++ b/core/java/android/bluetooth/BluetoothHidDeviceAppConfiguration.aidl @@ -0,0 +1,21 @@ +/* +** Copyright (C) 2013 The Linux Foundation. All rights reserved +** Not a Contribution. +** Copyright 2011, 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.bluetooth; + +parcelable BluetoothHidDeviceAppConfiguration; diff --git a/core/java/android/bluetooth/BluetoothHidDeviceAppConfiguration.java b/core/java/android/bluetooth/BluetoothHidDeviceAppConfiguration.java new file mode 100644 index 00000000000..9f3cd3c5371 --- /dev/null +++ b/core/java/android/bluetooth/BluetoothHidDeviceAppConfiguration.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2013 The Linux Foundation. All rights reserved + * Not a Contribution. + * Copyright (C) 2011 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.bluetooth; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Random; + +/** @hide */ +public final class BluetoothHidDeviceAppConfiguration implements Parcelable { + private final long mHash; + + BluetoothHidDeviceAppConfiguration() { + Random rnd = new Random(); + mHash = rnd.nextLong(); + } + + BluetoothHidDeviceAppConfiguration(long hash) { + mHash = hash; + } + + @Override + public boolean equals(Object o) { + if (o instanceof BluetoothHidDeviceAppConfiguration) { + BluetoothHidDeviceAppConfiguration config = (BluetoothHidDeviceAppConfiguration) o; + return mHash == config.mHash; + } + return false; + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public BluetoothHidDeviceAppConfiguration createFromParcel(Parcel in) { + long hash = in.readLong(); + return new BluetoothHidDeviceAppConfiguration(hash); + } + + @Override + public BluetoothHidDeviceAppConfiguration[] newArray(int size) { + return new BluetoothHidDeviceAppConfiguration[size]; + } + }; + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeLong(mHash); + } +} diff --git a/core/java/android/bluetooth/BluetoothHidDeviceAppQosSettings.aidl b/core/java/android/bluetooth/BluetoothHidDeviceAppQosSettings.aidl new file mode 100644 index 00000000000..ae93235cc8b --- /dev/null +++ b/core/java/android/bluetooth/BluetoothHidDeviceAppQosSettings.aidl @@ -0,0 +1,21 @@ +/* +** Copyright (C) 2013 The Linux Foundation. All rights reserved +** Not a Contribution. +** Copyright 2011, 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.bluetooth; + +parcelable BluetoothHidDeviceAppQosSettings; diff --git a/core/java/android/bluetooth/BluetoothHidDeviceAppQosSettings.java b/core/java/android/bluetooth/BluetoothHidDeviceAppQosSettings.java new file mode 100644 index 00000000000..a4044d93533 --- /dev/null +++ b/core/java/android/bluetooth/BluetoothHidDeviceAppQosSettings.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2013 The Linux Foundation. All rights reserved + * Not a Contribution. + * Copyright (C) 2011 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.bluetooth; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Random; + +/** @hide */ +public final class BluetoothHidDeviceAppQosSettings implements Parcelable { + + final public int serviceType; + final public int tokenRate; + final public int tokenBucketSize; + final public int peakBandwidth; + final public int latency; + final public int delayVariation; + + final static public int SERVICE_NO_TRAFFIC = 0x00; + final static public int SERVICE_BEST_EFFORT = 0x01; + final static public int SERVICE_GUARANTEED = 0x02; + + final static public int MAX = (int) 0xffffffff; + + public BluetoothHidDeviceAppQosSettings(int serviceType, int tokenRate, int tokenBucketSize, + int peakBandwidth, + int latency, int delayVariation) { + this.serviceType = serviceType; + this.tokenRate = tokenRate; + this.tokenBucketSize = tokenBucketSize; + this.peakBandwidth = peakBandwidth; + this.latency = latency; + this.delayVariation = delayVariation; + } + + @Override + public boolean equals(Object o) { + if (o instanceof BluetoothHidDeviceAppQosSettings) { + BluetoothHidDeviceAppQosSettings qos = (BluetoothHidDeviceAppQosSettings) o; + return false; + } + return false; + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public BluetoothHidDeviceAppQosSettings createFromParcel(Parcel in) { + + return new BluetoothHidDeviceAppQosSettings(in.readInt(), in.readInt(), in.readInt(), + in.readInt(), + in.readInt(), in.readInt()); + } + + @Override + public BluetoothHidDeviceAppQosSettings[] newArray(int size) { + return new BluetoothHidDeviceAppQosSettings[size]; + } + }; + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(serviceType); + out.writeInt(tokenRate); + out.writeInt(tokenBucketSize); + out.writeInt(peakBandwidth); + out.writeInt(latency); + out.writeInt(delayVariation); + } + + public int[] toArray() { + return new int[] { + serviceType, tokenRate, tokenBucketSize, peakBandwidth, latency, delayVariation + }; + } +} diff --git a/core/java/android/bluetooth/BluetoothHidDeviceAppSdpSettings.aidl b/core/java/android/bluetooth/BluetoothHidDeviceAppSdpSettings.aidl new file mode 100644 index 00000000000..38ac1ec6950 --- /dev/null +++ b/core/java/android/bluetooth/BluetoothHidDeviceAppSdpSettings.aidl @@ -0,0 +1,21 @@ +/* +** Copyright (C) 2013 The Linux Foundation. All rights reserved +** Not a Contribution. +** Copyright 2011, 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.bluetooth; + +parcelable BluetoothHidDeviceAppSdpSettings; diff --git a/core/java/android/bluetooth/BluetoothHidDeviceAppSdpSettings.java b/core/java/android/bluetooth/BluetoothHidDeviceAppSdpSettings.java new file mode 100644 index 00000000000..db88f0d74c0 --- /dev/null +++ b/core/java/android/bluetooth/BluetoothHidDeviceAppSdpSettings.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2013 The Linux Foundation. All rights reserved + * Not a Contribution. + * Copyright (C) 2011 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.bluetooth; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Random; + +/** @hide */ +public final class BluetoothHidDeviceAppSdpSettings implements Parcelable { + + final public String name; + final public String description; + final public String provider; + final public byte subclass; + final public byte[] descriptors; + + public BluetoothHidDeviceAppSdpSettings(String name, String description, String provider, + byte subclass, byte[] descriptors) { + this.name = name; + this.description = description; + this.provider = provider; + this.subclass = subclass; + this.descriptors = descriptors.clone(); + } + + @Override + public boolean equals(Object o) { + if (o instanceof BluetoothHidDeviceAppSdpSettings) { + BluetoothHidDeviceAppSdpSettings sdp = (BluetoothHidDeviceAppSdpSettings) o; + return false; + } + return false; + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public BluetoothHidDeviceAppSdpSettings createFromParcel(Parcel in) { + + return new BluetoothHidDeviceAppSdpSettings(in.readString(), in.readString(), + in.readString(), in.readByte(), in.createByteArray()); + } + + @Override + public BluetoothHidDeviceAppSdpSettings[] newArray(int size) { + return new BluetoothHidDeviceAppSdpSettings[size]; + } + }; + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeString(name); + out.writeString(description); + out.writeString(provider); + out.writeByte(subclass); + out.writeByteArray(descriptors); + } +} diff --git a/core/java/android/bluetooth/BluetoothHidDeviceCallback.java b/core/java/android/bluetooth/BluetoothHidDeviceCallback.java new file mode 100644 index 00000000000..cc608339b4e --- /dev/null +++ b/core/java/android/bluetooth/BluetoothHidDeviceCallback.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2013 The Linux Foundation. All rights reserved + * Not a Contribution. + * Copyright (C) 2011 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.bluetooth; + +import android.util.Log; + +/** @hide */ +public abstract class BluetoothHidDeviceCallback { + + private static final String TAG = BluetoothHidDeviceCallback.class.getSimpleName(); + + /** + * Callback called when application registration state changes. Usually it's + * called due to either + * {@link BluetoothHidDevice#registerApp(String, String, String, byte, byte[], + * BluetoothHidDeviceCallback)} + * or + * {@link BluetoothHidDevice#unregisterApp(BluetoothHidDeviceAppConfiguration)} + * , but can be also unsolicited in case e.g. Bluetooth was turned off in + * which case application is unregistered automatically. + * + * @param pluggedDevice {@link BluetoothDevice} object which represents host + * that currently has Virtual Cable established with device. Only + * valid when application is registered, can be null + * . + * @param config {@link BluetoothHidDeviceAppConfiguration} object which + * represents token required to unregister application using + * {@link BluetoothHidDevice#unregisterApp(BluetoothHidDeviceAppConfiguration)} + * . + * @param registered true if application is registered, + * false otherwise. + */ + public void onAppStatusChanged(BluetoothDevice pluggedDevice, + BluetoothHidDeviceAppConfiguration config, boolean registered) { + Log.d(TAG, "onAppStatusChanged: pluggedDevice=" + (pluggedDevice == null ? + null : pluggedDevice.toString()) + " registered=" + registered); + } + + /** + * Callback called when connection state with remote host was changed. + * Application can assume than Virtual Cable is established when called with + * {@link BluetoothProfile#STATE_CONNECTED} state. + * + * @param device {@link BluetoothDevice} object representing host device + * which connection state was changed. + * @param state Connection state as defined in {@link BluetoothProfile}. + */ + public void onConnectionStateChanged(BluetoothDevice device, int state) { + Log.d(TAG, "onConnectionStateChanged: device=" + device.toString() + " state=" + state); + } + + /** + * Callback called when GET_REPORT is received from remote host. Should be + * replied by application using + * {@link BluetoothHidDevice#replyReport(byte, byte, byte[])}. + * + * @param type Requested Report Type. + * @param id Requested Report Id, can be 0 if no Report Id are defined in + * descriptor. + * @param bufferSize Requested buffer size, application shall respond with + * at least given number of bytes. + */ + public void onGetReport(byte type, byte id, int bufferSize) { + Log.d(TAG, "onGetReport: type=" + type + " id=" + id + " bufferSize=" + bufferSize); + } + + /** + * Callback called when SET_REPORT is received from remote host. In case + * received data are invalid, application shall respond with + * {@link BluetoothHidDevice#reportError()}. + * + * @param type Report Type. + * @param id Report Id. + * @param data Report data. + */ + public void onSetReport(byte type, byte id, byte[] data) { + Log.d(TAG, "onSetReport: type=" + type + " id=" + id); + } + + /** + * Callback called when SET_PROTOCOL is received from remote host. + * Application shall use this information to send only reports valid for + * given protocol mode. By default, + * {@link BluetoothHidDevice#PROTOCOL_REPORT_MODE} shall be assumed. + * + * @param protocol Protocol Mode. + */ + public void onSetProtocol(byte protocol) { + Log.d(TAG, "onSetProtocol: protocol=" + protocol); + } + + /** + * Callback called when report data is received over interrupt channel. + * Report Type is assumed to be + * {@link BluetoothHidDevice#REPORT_TYPE_OUTPUT}. + * + * @param reportId Report Id. + * @param data Report data. + */ + public void onIntrData(byte reportId, byte[] data) { + Log.d(TAG, "onIntrData: reportId=" + reportId); + } + + /** + * Callback called when Virtual Cable is removed. This can be either due to + * {@link BluetoothHidDevice#unplug()} or request from remote side. After + * this callback is received connection will be disconnected automatically. + */ + public void onVirtualCableUnplug() { + Log.d(TAG, "onVirtualCableUnplug"); + } +} diff --git a/core/java/android/bluetooth/BluetoothInputDevice.java b/core/java/android/bluetooth/BluetoothInputDevice.java index 252e3d28a25..db23ef54899 100644 --- a/core/java/android/bluetooth/BluetoothInputDevice.java +++ b/core/java/android/bluetooth/BluetoothInputDevice.java @@ -96,6 +96,12 @@ public final class BluetoothInputDevice implements BluetoothProfile { public static final String ACTION_VIRTUAL_UNPLUG_STATUS = "android.bluetooth.input.profile.action.VIRTUAL_UNPLUG_STATUS"; + /** + * @hide + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_IDLE_TIME_CHANGED = + "codeaurora.bluetooth.input.profile.action.IDLE_TIME_CHANGED"; /** * Return codes for the connect and disconnect Bluez / Dbus calls. @@ -199,6 +205,11 @@ public final class BluetoothInputDevice implements BluetoothProfile { */ public static final String EXTRA_VIRTUAL_UNPLUG_STATUS = "android.bluetooth.BluetoothInputDevice.extra.VIRTUAL_UNPLUG_STATUS"; + /** + * @hide + */ + public static final String EXTRA_IDLE_TIME = "codeaurora.bluetooth.BluetoothInputDevice.extra.IDLE_TIME"; + private Context mContext; private ServiceListener mServiceListener; private BluetoothAdapter mAdapter; @@ -658,6 +669,56 @@ public boolean sendData(BluetoothDevice device, String report) { if (mService == null) Log.w(TAG, "Proxy not attached to service"); return false; } + + /** + * Send Get_Idle_Time command to the connected HID input device. + * + *

Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission. + * + * @param device Remote Bluetooth Device + * @return false on immediate error, + * true otherwise + * @hide + */ + public boolean getIdleTime(BluetoothDevice device) { + if (DBG) log("getIdletime(" + device + ")"); + if (mService != null && isEnabled() && isValidDevice(device)) { + try { + return mService.getIdleTime(device); + } catch (RemoteException e) { + Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); + return false; + } + } + if (mService == null) Log.w(TAG, "Proxy not attached to service"); + return false; + } + + /** + * Send Set_Idle_Time command to the connected HID input device. + * + *

Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission. + * + * @param device Remote Bluetooth Device + * @param idleTime Idle time to be set on HID Device + * @return false on immediate error, + * true otherwise + * @hide + */ + public boolean setIdleTime(BluetoothDevice device, byte idleTime) { + if (DBG) log("setIdletime(" + device + "), idleTime=" + idleTime); + if (mService != null && isEnabled() && isValidDevice(device)) { + try { + return mService.setIdleTime(device, idleTime); + } catch (RemoteException e) { + Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); + return false; + } + } + if (mService == null) Log.w(TAG, "Proxy not attached to service"); + return false; + } + private static void log(String msg) { Log.d(TAG, msg); } diff --git a/core/java/android/bluetooth/BluetoothPan.java b/core/java/android/bluetooth/BluetoothPan.java index 4f81f9863c5..1c93ca4db0e 100644 --- a/core/java/android/bluetooth/BluetoothPan.java +++ b/core/java/android/bluetooth/BluetoothPan.java @@ -138,7 +138,6 @@ public final class BluetoothPan implements BluetoothProfile { } if (VDBG) Log.d(TAG, "BluetoothPan() call bindService"); doBind(); - if (VDBG) Log.d(TAG, "BluetoothPan(), bindService called"); } boolean doBind() { @@ -185,12 +184,20 @@ protected void finalize() { final private IBluetoothStateChangeCallback mStateChangeCallback = new IBluetoothStateChangeCallback.Stub() { @Override - public void onBluetoothStateChange(boolean on) throws RemoteException { + public void onBluetoothStateChange(boolean on) { //Handle enable request to bind again. + Log.d(TAG, "onBluetoothStateChange on: " + on); if (on) { - Log.d(TAG, "onBluetoothStateChange(on) call bindService"); - doBind(); - if (VDBG) Log.d(TAG, "BluetoothPan(), bindService called"); + try { + if (mPanService == null) { + Log.d(TAG, "onBluetoothStateChange call bindService"); + doBind(); + } + } catch (IllegalStateException e) { + Log.e(TAG,"onBluetoothStateChange: could not bind to PAN service: ", e); + } catch (SecurityException e) { + Log.e(TAG,"onBluetoothStateChange: could not bind to PAN service: ", e); + } } else { if (VDBG) Log.d(TAG,"Unbinding service..."); synchronized (mConnection) { @@ -333,19 +340,25 @@ && isValidDevice(device)) { public void setBluetoothTethering(boolean value) { if (DBG) log("setBluetoothTethering(" + value + ")"); - try { - mPanService.setBluetoothTethering(value); - } catch (RemoteException e) { - Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); + + if (mPanService != null && isEnabled()) { + try { + mPanService.setBluetoothTethering(value); + } catch (RemoteException e) { + Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); + } } } public boolean isTetheringOn() { if (VDBG) log("isTetheringOn()"); - try { - return mPanService.isTetheringOn(); - } catch (RemoteException e) { - Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); + + if (mPanService != null && isEnabled()) { + try { + return mPanService.isTetheringOn(); + } catch (RemoteException e) { + Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); + } } return false; } diff --git a/core/java/android/bluetooth/BluetoothProfile.java b/core/java/android/bluetooth/BluetoothProfile.java index 136740505a4..80c5aa455fe 100644 --- a/core/java/android/bluetooth/BluetoothProfile.java +++ b/core/java/android/bluetooth/BluetoothProfile.java @@ -121,6 +121,25 @@ public interface BluetoothProfile { */ public static final int HEADSET_CLIENT = 16; + /** + * HID device + * @hide + */ + static public final int HID_DEVICE = 17; + + /** + * SAP + * @hide + */ + public static final int SAP = 20; + + /** + * DUN + * @hide + */ + + public static final int DUN = 21; + /** * Default priority for devices that we try to auto-connect to and * and allow incoming connections for the profile diff --git a/core/java/android/bluetooth/BluetoothRemoteDiRecord.aidl b/core/java/android/bluetooth/BluetoothRemoteDiRecord.aidl new file mode 100644 index 00000000000..793c1a9430a --- /dev/null +++ b/core/java/android/bluetooth/BluetoothRemoteDiRecord.aidl @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2014, The Linux Foundation. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * * Neither the name of The Linux Foundation nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN + * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package android.bluetooth; + +parcelable BluetoothRemoteDiRecord; diff --git a/core/java/android/bluetooth/BluetoothRemoteDiRecord.java b/core/java/android/bluetooth/BluetoothRemoteDiRecord.java new file mode 100644 index 00000000000..0d8be1287e3 --- /dev/null +++ b/core/java/android/bluetooth/BluetoothRemoteDiRecord.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2014, The Linux Foundation. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * * Neither the name of The Linux Foundation nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN + * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package android.bluetooth; + +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Represents a remote Bluetooth device identification record + *{@link BluetoothRemoteDiRecord} If remote device supports DI profile, + * then you can query for the bluetooth remote device information with the respective + * the particular remote device, Such as the vendorId, vendorIdSource, productId, + * productVersion & specificationId, which are important in order to make best use of + * the features on the device identified. Eg. A cellular phone may use this information + * to identify associated accessories or download Java apps from another device + * that advertises its availability. + * + *

To get a {@link BluetoothRemoteDiRecord}, use + * {@link BluetoothDevice#getRemoteDiRecord() + * BluetoothDevice.getRemoteDiRecord()} + * + * Requires the {@link android.Manifest.permission#BLUETOOTH} permission. + */ + +/** @hide */ +public final class BluetoothRemoteDiRecord implements Parcelable { + private int mVendorId; + private int mVendorIdSource; + private int mProductId; + private int mProductVersion; + private int mSpecificationId; + private Object mObject = new Object(); + + public BluetoothRemoteDiRecord(int vendorId,int vendorIdSource, int productId, + int productVersion, int specificationId) { + mVendorId= vendorId; + mVendorIdSource= vendorIdSource; + mProductId= productId; + mProductVersion= productVersion; + mSpecificationId= specificationId; + } + + /** + * @return the vendorId + */ + public int getVendorId() { + synchronized (mObject) { + return mVendorId; + } + } + + /** + * @return the vendorIdSource + */ + public int getVendorIdSource() { + synchronized (mObject) { + return mVendorIdSource; + } + } + + /** + * @return the productId + */ + public int getProductId() { + synchronized (mObject) { + return mProductId; + } + } + + /** + * @return the productVersion + */ + public int getProductVersion() { + synchronized (mObject) { + return mProductVersion; + } + } + + /** + * @return the specificationId + */ + public int getSpecificationId() { + synchronized (mObject) { + return mSpecificationId; + } + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(mVendorId); + out.writeInt(mVendorIdSource); + out.writeInt(mProductId); + out.writeInt(mProductVersion); + out.writeInt(mSpecificationId); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public BluetoothRemoteDiRecord createFromParcel(Parcel in) { + + return new BluetoothRemoteDiRecord(in.readInt(), in.readInt(), in.readInt(), + in.readInt(), in.readInt()); + } + + @Override + public BluetoothRemoteDiRecord[] newArray(int size) { + return new BluetoothRemoteDiRecord[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + +} + diff --git a/core/java/android/bluetooth/BluetoothSap.java b/core/java/android/bluetooth/BluetoothSap.java new file mode 100755 index 00000000000..007cb10faaf --- /dev/null +++ b/core/java/android/bluetooth/BluetoothSap.java @@ -0,0 +1,294 @@ +/* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of The Linux Foundation nor + * the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NON-INFRINGEMENT ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package android.bluetooth; + +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class provides the APIs to control the Bluetooth Sap + * Profile. + * + *

BluetoothSap is a proxy object for controlling the Bluetooth SAP + * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get + * the BluetoothSap proxy object. + * + *

Each method is protected with its appropriate permission. + *@hide + */ +public final class BluetoothSap implements BluetoothProfile { + private static final String TAG = "BluetoothSap"; + private static final boolean DBG = false; + private static final boolean VDBG = false; + + /** + * Intent used to broadcast the change in connection state of the Sap + * profile. + * + *

This intent will have 3 extras: + *

    + *
  • {@link #EXTRA_STATE} - The current state of the profile.
  • + *
  • {@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile.
  • + *
  • {@link BluetoothDevice#EXTRA_DEVICE} - The remote device.
  • + *
+ * + *

{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of + * {@link #STATE_DISCONNECTED}, {@link #STATE_CONNECTED}. + * + *

Requires {@link android.Manifest.permission#BLUETOOTH} permission to + * receive. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_CONNECTION_STATE_CHANGED = + "codeaurora.bluetooth.sap.profile.action.CONNECTION_STATE_CHANGED"; + + private Context mContext; + private ServiceListener mServiceListener; + private BluetoothAdapter mAdapter; + private IBluetoothSap mSapService; + + /** + * Create a BluetoothSap proxy object for interacting with the local + * Bluetooth Service which handles the Sap profile + * + */ + /*package*/ BluetoothSap(Context context, ServiceListener l) { + mContext = context; + mServiceListener = l; + mAdapter = BluetoothAdapter.getDefaultAdapter(); + try { + mAdapter.getBluetoothManager().registerStateChangeCallback(mStateChangeCallback); + } catch (RemoteException re) { + Log.w(TAG,"Unable to register BluetoothStateChangeCallback",re); + } + Log.d(TAG, "BluetoothSap() call bindService"); + doBind(); + } + + boolean doBind() { + Intent intent = new Intent(IBluetoothSap.class.getName()); + ComponentName comp = intent.resolveSystemService(mContext.getPackageManager(), 0); + intent.setComponent(comp); + + if (comp == null || !mContext.bindServiceAsUser(intent, mConnection, 0, + android.os.Process.myUserHandle())) { + Log.e(TAG, "Could not bind to Bluetooth SAP Service with " + intent); + return false; + } + return true; + } + + /*package*/ void close() { + if (VDBG) log("close()"); + mServiceListener = null; + IBluetoothManager mgr = mAdapter.getBluetoothManager(); + if (mgr != null) { + try { + mgr.unregisterStateChangeCallback(mStateChangeCallback); + } catch (RemoteException re) { + Log.w(TAG,"Unable to unregister BluetoothStateChangeCallback",re); + } + } + + synchronized (mConnection) { + if ( mSapService != null) { + try { + mSapService = null; + mContext.unbindService(mConnection); + } catch (Exception re) { + Log.e(TAG,"",re); + } + } + } + } + + protected void finalize() { + close(); + } + + private IBluetoothStateChangeCallback mStateChangeCallback = new IBluetoothStateChangeCallback.Stub() { + + @Override + public void onBluetoothStateChange(boolean on) { + //Handle enable request to bind again. + Log.d(TAG, "onBluetoothStateChange on: " + on); + if (on) { + try { + if (mSapService == null) { + Log.d(TAG, "onBluetoothStateChange call bindService"); + doBind(); + } + } catch (IllegalStateException e) { + Log.e(TAG,"onBluetoothStateChange: could not bind to SAP service: ", e); + } catch (SecurityException e) { + Log.e(TAG,"onBluetoothStateChange: could not bind to SAP service: ", e); + } + Log.d(TAG, "BluetoothSap(), bindService called"); + } else { + if (VDBG) Log.d(TAG,"Unbinding service..."); + synchronized (mConnection) { + if ( mSapService != null) { + try { + mSapService = null; + mContext.unbindService(mConnection); + } catch (Exception re) { + Log.e(TAG,"",re); + } + } + } + } + } + }; + + /** + * Initiate disconnection from SAP server. + * + *

Once the disconnection is initiated by any device either local host + * or remote device, the state will transition from {@link #STATE_CONNECTED} + * to {@link #STATE_DISCONNECTED}. + * + *

Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} + * permission. + * + * @param device Remote Bluetooth Device + * @return false on immediate error, + * true otherwise + * @hide + */ + public boolean disconnect(BluetoothDevice device) { + if (DBG) log("disconnect(" + device + ")"); + if (mSapService != null && isEnabled() && + isValidDevice(device)) { + try { + return mSapService.disconnect(device); + } catch (RemoteException e) { + Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); + return false; + } + } + if (mSapService == null) Log.w(TAG, "Proxy not attached to service"); + return false; + } + /** + * {@inheritDoc} + */ + public List getConnectedDevices() { + if (VDBG) log("getConnectedDevices()"); + if (mSapService != null && isEnabled()) { + try { + return mSapService.getConnectedDevices(); + } catch (RemoteException e) { + Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); + return new ArrayList(); + } + } + if (mSapService == null) Log.w(TAG, "Proxy not attached to service"); + return new ArrayList(); + } + + /** + * {@inheritDoc} + */ + public List getDevicesMatchingConnectionStates(int[] states) { + if (VDBG) log("getDevicesMatchingStates()"); + if (mSapService != null && isEnabled()) { + try { + return mSapService.getDevicesMatchingConnectionStates(states); + } catch (RemoteException e) { + Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); + return new ArrayList(); + } + } + if (mSapService == null) Log.w(TAG, "Proxy not attached to service"); + return new ArrayList(); + } + + /** + * {@inheritDoc} + */ + public int getConnectionState(BluetoothDevice device) { + if (VDBG) log("getState(" + device + ")"); + if (mSapService != null && isEnabled() + && isValidDevice(device)) { + try { + return mSapService.getConnectionState(device); + } catch (RemoteException e) { + Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); + return BluetoothProfile.STATE_DISCONNECTED; + } + } + if (mSapService == null) Log.w(TAG, "Proxy not attached to service"); + return BluetoothProfile.STATE_DISCONNECTED; + } + + private ServiceConnection mConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder service) { + if (DBG) Log.d(TAG, "BluetoothSAP Proxy object connected"); + mSapService = IBluetoothSap.Stub.asInterface(service); + + if (mServiceListener != null) { + mServiceListener.onServiceConnected(BluetoothProfile.SAP, + BluetoothSap.this); + } + } + public void onServiceDisconnected(ComponentName className) { + if (DBG) Log.d(TAG, "BluetoothSAP Proxy object disconnected"); + mSapService = null; + if (mServiceListener != null) { + mServiceListener.onServiceDisconnected(BluetoothProfile.SAP); + } + } + }; + + private boolean isEnabled() { + if (mAdapter.getState() == BluetoothAdapter.STATE_ON) return true; + return false; + } + + private boolean isValidDevice(BluetoothDevice device) { + if (device == null) return false; + + if (BluetoothAdapter.checkBluetoothAddress(device.getAddress())) return true; + return false; + } + + private static void log(String msg) { + Log.d(TAG, msg); + } +} diff --git a/core/java/android/bluetooth/BluetoothSocket.java b/core/java/android/bluetooth/BluetoothSocket.java index 36997e54484..9cefaee2385 100644 --- a/core/java/android/bluetooth/BluetoothSocket.java +++ b/core/java/android/bluetooth/BluetoothSocket.java @@ -192,10 +192,16 @@ private BluetoothSocket acceptSocket(String RemoteAddr) throws IOException { throw new IOException("bt socket acept failed"); } as.mSocket = new LocalSocket(fds[0]); + try { + as.mSocket.closeExternalFd(); + } catch (IOException e) { + Log.e(TAG, "closeExternalFd failed"); + } as.mSocketIS = as.mSocket.getInputStream(); as.mSocketOS = as.mSocket.getOutputStream(); as.mAddress = RemoteAddr; as.mDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(RemoteAddr); + as.mPort = mPort; return as; } /** @@ -409,6 +415,61 @@ public void connect() throws IOException { return acceptedSocket; } + /** + * setSocketOpt for the Buetooth Socket. + * + * @param optionName socket option name + * @param optionVal socket option value + * @param optionLen socket option length + * @return -1 on immediate error, + * 0 otherwise + * @hide + */ + public int setSocketOpt(int optionName, byte [] optionVal, int optionLen) throws IOException { + int ret = 0; + if (mSocketState == SocketState.CLOSED) throw new IOException("socket closed"); + IBluetooth bluetoothProxy = BluetoothAdapter.getDefaultAdapter().getBluetoothService(null); + if (bluetoothProxy == null) { + Log.e(TAG, "setSocketOpt fail, reason: bluetooth is off"); + return -1; + } + try { + if(VDBG) Log.d(TAG, "setSocketOpt(), mType: " + mType + " mPort: " + mPort); + ret = bluetoothProxy.setSocketOpt(mType, mPort, optionName, optionVal, optionLen); + } catch (RemoteException e) { + Log.e(TAG, Log.getStackTraceString(new Throwable())); + return -1; + } + return ret; + } + + /** + * getSocketOpt for the Buetooth Socket. + * + * @param optionName socket option name + * @param optionVal socket option value + * @return -1 on immediate error, + * length of returned socket option otherwise + * @hide + */ + public int getSocketOpt(int optionName, byte [] optionVal) throws IOException { + int ret = 0; + if (mSocketState == SocketState.CLOSED) throw new IOException("socket closed"); + IBluetooth bluetoothProxy = BluetoothAdapter.getDefaultAdapter().getBluetoothService(null); + if (bluetoothProxy == null) { + Log.e(TAG, "getSocketOpt fail, reason: bluetooth is off"); + return -1; + } + try { + if(VDBG) Log.d(TAG, "getSocketOpt(), mType: " + mType + " mPort: " + mPort); + ret = bluetoothProxy.getSocketOpt(mType, mPort, optionName, optionVal); + } catch (RemoteException e) { + Log.e(TAG, Log.getStackTraceString(new Throwable())); + return -1; + } + return ret; + } + /*package*/ int available() throws IOException { if (VDBG) Log.d(TAG, "available: " + mSocketIS); return mSocketIS.available(); diff --git a/core/java/android/bluetooth/IBluetooth.aidl b/core/java/android/bluetooth/IBluetooth.aidl old mode 100644 new mode 100755 index dabb1ceaea8..5e475c8369b --- a/core/java/android/bluetooth/IBluetooth.aidl +++ b/core/java/android/bluetooth/IBluetooth.aidl @@ -20,6 +20,7 @@ import android.bluetooth.IBluetoothCallback; import android.bluetooth.IBluetoothStateChangeCallback; import android.bluetooth.BluetoothActivityEnergyInfo; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothRemoteDiRecord; import android.os.ParcelUuid; import android.os.ParcelFileDescriptor; @@ -65,6 +66,8 @@ interface IBluetooth int getRemoteType(in BluetoothDevice device); String getRemoteAlias(in BluetoothDevice device); boolean setRemoteAlias(in BluetoothDevice device, in String name); + boolean setRemoteTrust(in BluetoothDevice device, in boolean value); + boolean getRemoteTrust(in BluetoothDevice device); int getRemoteClass(in BluetoothDevice device); ParcelUuid[] getRemoteUuids(in BluetoothDevice device); boolean fetchRemoteUuids(in BluetoothDevice device); @@ -99,6 +102,9 @@ interface IBluetooth void getActivityEnergyInfoFromController(); BluetoothActivityEnergyInfo reportActivityInfo(); + int setSocketOpt(int type, int port, int optionName, in byte [] optionVal, int optionLen); + int getSocketOpt(int type, int port, int optionName, out byte [] optionVal); + BluetoothRemoteDiRecord getRemoteDiRecord(in BluetoothDevice device); // for dumpsys support String dump(); } diff --git a/core/java/android/bluetooth/IBluetoothA2dpSink.aidl b/core/java/android/bluetooth/IBluetoothA2dpSink.aidl index b7c64767700..774a1ecd541 100644 --- a/core/java/android/bluetooth/IBluetoothA2dpSink.aidl +++ b/core/java/android/bluetooth/IBluetoothA2dpSink.aidl @@ -30,5 +30,8 @@ interface IBluetoothA2dpSink { List getConnectedDevices(); List getDevicesMatchingConnectionStates(in int[] states); int getConnectionState(in BluetoothDevice device); + boolean setPriority(in BluetoothDevice device, int priority); + int getPriority(in BluetoothDevice device); + boolean isA2dpPlaying(in BluetoothDevice device); BluetoothAudioConfig getAudioConfig(in BluetoothDevice device); } diff --git a/core/java/android/bluetooth/IBluetoothDun.aidl b/core/java/android/bluetooth/IBluetoothDun.aidl new file mode 100755 index 00000000000..23fc90d026d --- /dev/null +++ b/core/java/android/bluetooth/IBluetoothDun.aidl @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of The Linux Foundation nor + * the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NON-INFRINGEMENT ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package android.bluetooth; + +import android.bluetooth.BluetoothDevice; + +/** + * API for Bluetooth Dun service + * + * {@hide} + */ +interface IBluetoothDun { + // Public API + boolean disconnect(in BluetoothDevice device); + int getConnectionState(in BluetoothDevice device); + List getConnectedDevices(); + List getDevicesMatchingConnectionStates(in int[] states); +} diff --git a/core/java/android/bluetooth/IBluetoothHidDevice.aidl b/core/java/android/bluetooth/IBluetoothHidDevice.aidl new file mode 100644 index 00000000000..60358c55031 --- /dev/null +++ b/core/java/android/bluetooth/IBluetoothHidDevice.aidl @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2013 The Linux Foundation. All rights reserved + * Not a Contribution. + * Copyright (C) 2012 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.bluetooth; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHidDeviceAppConfiguration; +import android.bluetooth.IBluetoothHidDeviceCallback; +import android.bluetooth.BluetoothHidDeviceAppSdpSettings; +import android.bluetooth.BluetoothHidDeviceAppQosSettings; + +/** @hide */ +interface IBluetoothHidDevice { + boolean registerApp(in BluetoothHidDeviceAppConfiguration config, + in BluetoothHidDeviceAppSdpSettings sdp, in BluetoothHidDeviceAppQosSettings inQos, + in BluetoothHidDeviceAppQosSettings outQos, in IBluetoothHidDeviceCallback callback); + boolean unregisterApp(in BluetoothHidDeviceAppConfiguration config); + boolean sendReport(in int id, in byte[] data); + boolean replyReport(in byte type, in byte id, in byte[] data); + boolean reportError(byte error); + boolean unplug(); + boolean connect(); + boolean disconnect(); +} diff --git a/core/java/android/bluetooth/IBluetoothHidDeviceCallback.aidl b/core/java/android/bluetooth/IBluetoothHidDeviceCallback.aidl new file mode 100644 index 00000000000..7c71a1799c2 --- /dev/null +++ b/core/java/android/bluetooth/IBluetoothHidDeviceCallback.aidl @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2013 The Linux Foundation. All rights reserved + * Not a Contribution. + * Copyright (C) 2011, 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.bluetooth; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHidDeviceAppConfiguration; + +/** @hide */ +interface IBluetoothHidDeviceCallback { + void onAppStatusChanged(in BluetoothDevice device, in BluetoothHidDeviceAppConfiguration config, boolean registered); + void onConnectionStateChanged(in BluetoothDevice device, in int state); + void onGetReport(in byte type, in byte id, in int bufferSize); + void onSetReport(in byte type, in byte id, in byte[] data); + void onSetProtocol(in byte protocol); + void onIntrData(in byte reportId, in byte[] data); + void onVirtualCableUnplug(); +} diff --git a/core/java/android/bluetooth/IBluetoothInputDevice.aidl b/core/java/android/bluetooth/IBluetoothInputDevice.aidl index 1ebb9ca6eb1..5bd3f781932 100644 --- a/core/java/android/bluetooth/IBluetoothInputDevice.aidl +++ b/core/java/android/bluetooth/IBluetoothInputDevice.aidl @@ -56,4 +56,12 @@ interface IBluetoothInputDevice { * @hide */ boolean sendData(in BluetoothDevice device, String report); + /** + * @hide + */ + boolean getIdleTime(in BluetoothDevice device); + /** + * @hide + */ + boolean setIdleTime(in BluetoothDevice device, byte idleTime); } diff --git a/core/java/android/bluetooth/IBluetoothManager.aidl b/core/java/android/bluetooth/IBluetoothManager.aidl index 7411d3f27b5..3e895579fa8 100644 --- a/core/java/android/bluetooth/IBluetoothManager.aidl +++ b/core/java/android/bluetooth/IBluetoothManager.aidl @@ -18,7 +18,9 @@ package android.bluetooth; import android.bluetooth.IBluetooth; import android.bluetooth.IBluetoothGatt; +import android.bluetooth.IQBluetooth; import android.bluetooth.IBluetoothManagerCallback; +import android.bluetooth.IQBluetoothManagerCallback; import android.bluetooth.IBluetoothProfileServiceConnection; import android.bluetooth.IBluetoothStateChangeCallback; @@ -30,14 +32,17 @@ import android.bluetooth.IBluetoothStateChangeCallback; interface IBluetoothManager { IBluetooth registerAdapter(in IBluetoothManagerCallback callback); + IQBluetooth registerQAdapter(in IQBluetoothManagerCallback callback); void unregisterAdapter(in IBluetoothManagerCallback callback); + void unregisterQAdapter(in IQBluetoothManagerCallback callback); void registerStateChangeCallback(in IBluetoothStateChangeCallback callback); void unregisterStateChangeCallback(in IBluetoothStateChangeCallback callback); boolean isEnabled(); - boolean enable(); + boolean enable(String callingPackage); boolean enableNoAutoConnect(); boolean disable(boolean persist); IBluetoothGatt getBluetoothGatt(); + IQBluetooth getQBluetooth(); boolean bindBluetoothProfileService(int profile, IBluetoothProfileServiceConnection proxy); void unbindBluetoothProfileService(int profile, IBluetoothProfileServiceConnection proxy); diff --git a/core/java/android/bluetooth/IBluetoothSap.aidl b/core/java/android/bluetooth/IBluetoothSap.aidl new file mode 100644 index 00000000000..9299a27ab31 --- /dev/null +++ b/core/java/android/bluetooth/IBluetoothSap.aidl @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of The Linux Foundation nor + * the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NON-INFRINGEMENT ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package android.bluetooth; + +import android.bluetooth.BluetoothDevice; + +/** + * API for Bluetooth Sap service + * + * {@hide} + */ +interface IBluetoothSap { + // Public API + boolean disconnect(in BluetoothDevice device); + int getConnectionState(in BluetoothDevice device); + List getConnectedDevices(); + List getDevicesMatchingConnectionStates(in int[] states); +} diff --git a/core/java/android/bluetooth/IQBluetooth.aidl b/core/java/android/bluetooth/IQBluetooth.aidl new file mode 100644 index 00000000000..f6db26334c3 --- /dev/null +++ b/core/java/android/bluetooth/IQBluetooth.aidl @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * * Neither the name of The Linux Foundation nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN + * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package android.bluetooth; + +import android.bluetooth.IBluetoothCallback; +import android.bluetooth.IQBluetoothAdapterCallback; +import android.bluetooth.IBluetooth; +import android.bluetooth.IBluetoothStateChangeCallback; +import android.bluetooth.BluetoothDevice; +import android.os.ParcelUuid; +import android.os.ParcelFileDescriptor; + +/** + * System private API for talking with the Bluetooth service. + * + * {@hide} + */ +interface IQBluetooth +{ + boolean registerLeLppRssiMonitorClient(in String address, in IQBluetoothAdapterCallback client, in boolean add); + void writeLeLppRssiThreshold(in String address, in byte min, in byte max); + void readLeLppRssiThreshold(in String address); + void enableLeLppRssiMonitor(in String address, in boolean enable); +} diff --git a/core/java/android/bluetooth/IQBluetoothAdapterCallback.aidl b/core/java/android/bluetooth/IQBluetoothAdapterCallback.aidl new file mode 100644 index 00000000000..b254d151c74 --- /dev/null +++ b/core/java/android/bluetooth/IQBluetoothAdapterCallback.aidl @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * * Neither the name of The Linux Foundation nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN + * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package android.bluetooth; + + + +/** + * Callback interface definition for interact with BLE/QAdapterService + * @hide + */ + +interface IQBluetoothAdapterCallback { + void onWriteRssiThreshold(in String address, in int status); + void onReadRssiThreshold(in String address, in int low, in int upper, + in int alert, in int status); + void onEnableRssiMonitor(in String address, in int enable, in int status); + void onRssiThresholdEvent(in String address, in int evtType, in int rssi); + boolean onUpdateLease(); +} diff --git a/core/java/android/bluetooth/IQBluetoothManagerCallback.aidl b/core/java/android/bluetooth/IQBluetoothManagerCallback.aidl new file mode 100644 index 00000000000..f9983c31097 --- /dev/null +++ b/core/java/android/bluetooth/IQBluetoothManagerCallback.aidl @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * * Neither the name of The Linux Foundation nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN + * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package android.bluetooth; + +import android.bluetooth.IQBluetooth; + +/** + * API for Communication between BluetoothAdapter and BluetoothManager + * + * {@hide} + */ +interface IQBluetoothManagerCallback { + void onQBluetoothServiceUp(in IQBluetooth QbluetoothService); + void onQBluetoothServiceDown(); +} diff --git a/core/java/android/bluetooth/QBluetoothAdapter.java b/core/java/android/bluetooth/QBluetoothAdapter.java new file mode 100644 index 00000000000..401d0fcb4c8 --- /dev/null +++ b/core/java/android/bluetooth/QBluetoothAdapter.java @@ -0,0 +1,399 @@ +/* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * * Neither the name of The Linux Foundation nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN + * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package android.bluetooth; + +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.content.Context; +import android.os.Binder; +import android.os.IBinder; +import android.os.Message; +import android.os.ParcelUuid; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.bluetooth.IQBluetoothAdapterCallback; +import android.util.Log; +import android.util.Pair; + +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.UUID; + +/** + * Represents the local device BLE adapter. The {@link QBluetoothAdapter} + * lets you perform fundamental Bluetooth tasks, + * such as register and remove LPP clients. + * + *

To get a {@link QBluetoothAdapter} representing this specific Bluetooth + * adapter, call the + * static {@link #getDefaultAdapter} method; when running on JELLY_BEAN_MR2 and + * higher. Once you have the local adapter, you can register for LPP clients + * and receive alerts based on proximity of the remote device + */ + /** @hide */ +public final class QBluetoothAdapter { + private static final String TAG = "QBluetoothAdapter"; + private static final boolean DBG = false; + private static final boolean VDBG = false; + + private static QBluetoothAdapter sAdapter; + private static BluetoothAdapter mAdapter; + + private final IBluetoothManager mManagerService; + private IBluetooth mService; + private IQBluetooth mQService; + + private final Map mLppClients = new HashMap(); + /** + * Get a handle to the default local QBluetooth adapter. + *

Currently Android only supports one QBluetooth adapter, but the API + * could be extended to support more. This will always return the default + * adapter. + * @return the default local adapter, or null if Bluetooth is not supported + * on this hardware platform + */ + public static synchronized QBluetoothAdapter getDefaultAdapter() { + mAdapter=BluetoothAdapter.getDefaultAdapter(); + IBluetoothManager managerService=mAdapter.getBluetoothManager(); + sAdapter = new QBluetoothAdapter(managerService); + return sAdapter; + } + + /** + * Use {@link #getDefaultAdapter} to get the BluetoothAdapter instance. + */ + QBluetoothAdapter(IBluetoothManager managerService){ + if (managerService == null) { + throw new IllegalArgumentException("bluetooth manager service is null"); + } + try { + //mService = managerService.registerAdapter(mManagerCallback); + mService=mAdapter.getBluetoothService(mAdapterServiceCallback); + //mQService=managerService.getQBluetooth(); + mQService=managerService.registerQAdapter(mManagerCallback); + Log.i(TAG,"mQService= :" + mQService); + } catch (RemoteException e) {Log.e(TAG, "", e);} + mManagerService = managerService; + /* read property value to check if the bluetooth controller support le extended scan */ + } + + public interface LeLppCallback { + public void onWriteRssiThreshold(int status); + + public void onReadRssiThreshold(int low,int upper, int alert, int status); + + public void onEnableRssiMonitor(int enable, int status); + + public void onRssiThresholdEvent(int evtType, int rssi); + + public boolean onUpdateLease(); + }; + + /** @hide */ + public boolean registerLppClient(LeLppCallback client, String address, boolean add) { + synchronized(mLppClients) { + if (add) { + if(mLppClients.containsKey(client)) { + Log.e(TAG, "Lpp Client has been already registered"); + return false; + } + + LeLppClientWrapper wrapper = new LeLppClientWrapper(this, mQService, address, client); + if(wrapper != null && wrapper.register2(true)) { + mLppClients.put(client, wrapper); + return true; + } + return false; + } else { + LeLppClientWrapper wrapper = mLppClients.remove(client); + if (wrapper != null) { + wrapper.register2(false); + return true; + } + return false; + } + } + } + + /** + * Write the rssi threshold for a connected remote device. + * + *

The {@link BluetoothGattCallback#onWriteRssiThreshold} callback will be + * invoked when the write request has been sent. + * + *

Requires {@link android.Manifest.permission#BLUETOOTH} permission. + * + * @param min The lower limit of rssi threshold + * @param max The upper limit of rssi threshold + * @return true, if the rssi threshold writing request has been sent tsuccessfully + */ + /** @hide */ + public boolean writeRssiThreshold(LeLppCallback client, int min, int max) { + LeLppClientWrapper wrapper = null; + synchronized(mLppClients) { + wrapper = mLppClients.get(client); + } + if (wrapper == null) return false; + + wrapper.writeRssiThreshold((byte)min, (byte)max); + return true; + } + + /** + * Enable rssi monitoring for a connected remote device. + * + *

The {@link BluetoothGattCallback#onEnableRssiMonitor} callback will be + * invoked when the rssi monitor enable/disable request has been sent. + * + *

Requires {@link android.Manifest.permission#BLUETOOTH} permission. + * + * @param enable disable/enable rssi monitor + * @return true, if the rssi monitoring request has been sent tsuccessfully + */ + /** @hide */ + public boolean enableRssiMonitor(LeLppCallback client, boolean enable){ + LeLppClientWrapper wrapper = null; + synchronized(mLppClients) { + wrapper = mLppClients.get(client); + } + if (wrapper == null) return false; + + wrapper.enableMonitor(enable); + return true; + } + + /** + * Read the rssi threshold for a connected remote device. + * + *

The {@link BluetoothGattCallback#onReadRssiThreshold} callback will be + * invoked when the rssi threshold has been read. + * + *

Requires {@link android.Manifest.permission#BLUETOOTH} permission. + * + * @return true, if the rssi threshold has been requested successfully + */ + /** @hide */ + public boolean readRssiThreshold(LeLppCallback client) { + LeLppClientWrapper wrapper = null; + synchronized(mLppClients) { + wrapper = mLppClients.get(client); + } + if (wrapper == null) return false; + + wrapper.readRssiThreshold(); + return true; + } + + protected void finalize() throws Throwable { + try { + mManagerService.unregisterQAdapter(mManagerCallback); + } catch (RemoteException e) { + Log.e(TAG, "", e); + } finally { + super.finalize(); + } + } + + private static class LeLppClientWrapper extends IQBluetoothAdapterCallback.Stub { + private final WeakReference mAdapter; + private final IQBluetooth mQBluetoothAdapterService; + private final String mDevice; + private final LeLppCallback client; + + public LeLppClientWrapper (QBluetoothAdapter adapter, IQBluetooth adapterService, + String address, LeLppCallback callback) { + this.mAdapter = new WeakReference (adapter); + this.mQBluetoothAdapterService = adapterService; + this.mDevice = address; + this.client = callback; + } + + public boolean register2(boolean add) { + if(mQBluetoothAdapterService != null) { + try { + return mQBluetoothAdapterService.registerLeLppRssiMonitorClient(mDevice, this, add); + } catch (RemoteException e) { + Log.w(TAG, "", e); + } + } + return false; + } + + public void writeRssiThreshold(byte min, byte max) { + if(mQBluetoothAdapterService != null) { + try { + mQBluetoothAdapterService.writeLeLppRssiThreshold(mDevice, min, max); + } catch (RemoteException e) { + Log.w(TAG, "", e); + } + } + } + + public void enableMonitor(boolean enable) { + if(mQBluetoothAdapterService != null) { + try { + mQBluetoothAdapterService.enableLeLppRssiMonitor(mDevice, enable); + } catch (RemoteException e) { + Log.w(TAG, "", e); + } + } + } + + public void readRssiThreshold() { + if(mQBluetoothAdapterService != null) { + try { + mQBluetoothAdapterService.readLeLppRssiThreshold(mDevice); + } catch (RemoteException e) { + Log.w(TAG, "", e); + } + } + } + + /** + * Rssi threshold has been written + * @hide + */ + public void onWriteRssiThreshold(String address, int status){ + if (client == null) { + return; + } + try { + client.onWriteRssiThreshold(status); + } catch (Exception ex) { + Log.w(TAG, "Unhandled exception: " + ex); + } + } + + /** + * RSSI threshold has been read + * @hide + */ + public void onReadRssiThreshold(String address, int low, int upper, + int alert, int status){ + if (client == null) { + return; + } + try { + client.onReadRssiThreshold(low, upper, alert, status); + } catch (Exception ex) { + Log.w(TAG, "Unhandled exception: " + ex); + } + } + + /** + * Remote Device RSSI monitoring has been enabled/disabled + * @hide + */ + public void onEnableRssiMonitor(String address, int enable, int status){ + if (client == null) { + return; + } + try { + client.onEnableRssiMonitor(enable, status); + } catch (Exception ex) { + Log.w(TAG, "Unhandled exception: " + ex); + } + } + + /** + * RSSI threshold event reported + * @hide + */ + public void onRssiThresholdEvent(String address, int evtType, int rssi){ + if (client == null) { + return; + } + try { + client.onRssiThresholdEvent(evtType, rssi); + } catch (Exception ex) { + Log.w(TAG, "Unhandled exception: " + ex); + } + } + + public boolean onUpdateLease() { + if (client == null) return false; + try { + return client.onUpdateLease(); + } catch (Exception ex) { + Log.w(TAG, "Unhandled exception: " + ex); + return false; + } + } + } + + final private IBluetoothManagerCallback mAdapterServiceCallback = + new IBluetoothManagerCallback.Stub() { + public void onBluetoothServiceUp(IBluetooth bluetoothService) { + synchronized (mAdapterServiceCallback) { + //initialize the global params again + mService = bluetoothService; + Log.i(TAG,"onBluetoothServiceUp Adapter ON: mService: "+ mService + " mQService: " + mQService+ " ManagerService:" + mManagerService); + } + } + + public void onBluetoothServiceDown() { + synchronized (mAdapterServiceCallback) { + mService = null; + Log.i(TAG,"onBluetoothServiceDown Adapter OFF: mService: "+ mService + " mQService: " + mQService); + } + } + }; + + + final private IQBluetoothManagerCallback mManagerCallback = + new IQBluetoothManagerCallback.Stub() { + public void onQBluetoothServiceUp(IQBluetooth qcbluetoothService) { + if (VDBG) Log.i(TAG, "on QBluetoothServiceUp: " + qcbluetoothService); + synchronized (mManagerCallback) { + //initialize the global params again + mQService = qcbluetoothService; + Log.i(TAG,"onQBluetoothServiceUp: Adapter ON: mService: "+ mService + " mQService: " + mQService+ " ManagerService:" + mManagerService); + } + } + + public void onQBluetoothServiceDown() { + if (VDBG) Log.i(TAG, "onQBluetoothServiceDown: " + mService); + synchronized (mManagerCallback) { + mQService=null; + Log.i(TAG,"onQBluetoothServiceDown: Adapter OFF: mService: "+ mService + " mQService: " + mQService); + } + } + }; + +} diff --git a/core/java/android/bluetooth/le/BluetoothLeAdvertiser.java b/core/java/android/bluetooth/le/BluetoothLeAdvertiser.java index 67d9de59409..2efe4b4e6e6 100644 --- a/core/java/android/bluetooth/le/BluetoothLeAdvertiser.java +++ b/core/java/android/bluetooth/le/BluetoothLeAdvertiser.java @@ -267,7 +267,9 @@ public void startRegisteration() { Log.e(TAG, "Failed to start registeration", e); } if (mClientIf > 0 && mIsAdvertising) { - mLeAdvertisers.put(mAdvertiseCallback, this); + if (!mLeAdvertisers.containsKey(mAdvertiseCallback)) { + mLeAdvertisers.put(mAdvertiseCallback, this); + } } else if (mClientIf <= 0) { // Post internal error if registration failed. postStartFailure(mAdvertiseCallback, @@ -308,7 +310,7 @@ public void stopAdvertising() { public void onClientRegistered(int status, int clientIf) { Log.d(TAG, "onClientRegistered() - status=" + status + " clientIf=" + clientIf); synchronized (this) { - if (status == BluetoothGatt.GATT_SUCCESS) { + if (status == BluetoothGatt.GATT_SUCCESS && mBluetoothAdapter.isEnabled()) { mClientIf = clientIf; try { mBluetoothGatt.startMultiAdvertising(mClientIf, mAdvertisement, @@ -333,6 +335,9 @@ public void onMultiAdvertiseCallback(int status, boolean isStart, // Start success mIsAdvertising = true; postStartSuccess(mAdvertiseCallback, settings); + if (!mLeAdvertisers.containsKey(mAdvertiseCallback)) { + mLeAdvertisers.put(mAdvertiseCallback, this); + } } else { // Start failure. postStartFailure(mAdvertiseCallback, status); diff --git a/core/java/android/bluetooth/le/BluetoothLeScanner.java b/core/java/android/bluetooth/le/BluetoothLeScanner.java index 93ea299bc8d..6fca7822e63 100644 --- a/core/java/android/bluetooth/le/BluetoothLeScanner.java +++ b/core/java/android/bluetooth/le/BluetoothLeScanner.java @@ -260,7 +260,6 @@ public void stopLeScan() { } try { mBluetoothGatt.stopScan(mClientIf, false); - mBluetoothGatt.unregisterClient(mClientIf); } catch (RemoteException e) { Log.e(TAG, "Failed to stop scan and unregister", e); } diff --git a/core/java/android/content/BroadcastReceiver.java b/core/java/android/content/BroadcastReceiver.java index 9a32fdffbb9..a2212e3f9cd 100644 --- a/core/java/android/content/BroadcastReceiver.java +++ b/core/java/android/content/BroadcastReceiver.java @@ -16,6 +16,7 @@ package android.content; +import android.app.ActivityManager; import android.app.ActivityManagerNative; import android.app.ActivityThread; import android.app.IActivityManager; @@ -238,6 +239,7 @@ public static class PendingResult { final boolean mInitialStickyHint; final IBinder mToken; final int mSendingUser; + final int mFlags; int mResultCode; String mResultData; @@ -246,8 +248,8 @@ public static class PendingResult { boolean mFinished; /** @hide */ - public PendingResult(int resultCode, String resultData, Bundle resultExtras, - int type, boolean ordered, boolean sticky, IBinder token, int userId) { + public PendingResult(int resultCode, String resultData, Bundle resultExtras, int type, + boolean ordered, boolean sticky, IBinder token, int userId, int flags) { mResultCode = resultCode; mResultData = resultData; mResultExtras = resultExtras; @@ -256,6 +258,7 @@ public PendingResult(int resultCode, String resultData, Bundle resultExtras, mInitialStickyHint = sticky; mToken = token; mSendingUser = userId; + mFlags = flags; } /** @@ -417,11 +420,11 @@ public void sendFinished(IActivityManager am) { } if (mOrderedHint) { am.finishReceiver(mToken, mResultCode, mResultData, mResultExtras, - mAbortBroadcast); + mAbortBroadcast, mFlags); } else { // This broadcast was sent to a component; it is not ordered, // but we still need to tell the activity manager we are done. - am.finishReceiver(mToken, 0, null, null, false); + am.finishReceiver(mToken, 0, null, null, false, mFlags); } } catch (RemoteException ex) { } @@ -745,6 +748,17 @@ public int getSendingUserId() { return mPendingResult.mSendingUser; } + /** @hide */ + public String getSendingPackage(Intent intent) { + final IActivityManager mgr = ActivityManagerNative.getDefault(); + try { + boolean fg = (intent.getFlags() & Intent.FLAG_RECEIVER_FOREGROUND) != 0; + return mgr.getCallingPackageForBroadcast(fg); + } catch (RemoteException ex) { + return null; + } + } + /** * Control inclusion of debugging help for mismatched * calls to {@link Context#registerReceiver(BroadcastReceiver, IntentFilter) diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java index 0cff4c0e9a9..bf587ca44d6 100644 --- a/core/java/android/content/ContentProvider.java +++ b/core/java/android/content/ContentProvider.java @@ -1,4 +1,7 @@ /* + * Copyright (c) 2013-2014, The Linux Foundation. All rights reserved. + * Not a Contribution. + * * Copyright (C) 2006 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -276,7 +279,13 @@ public ContentProviderResult[] applyBatch(String callingPkg, throw new OperationApplicationException("App op not allowed", 0); } } - if (operation.isWriteOperation()) { + + if (operation.isDeleteOperation()) { + if (enforceDeletePermission(callingPkg, uri) + != AppOpsManager.MODE_ALLOWED) { + throw new OperationApplicationException("App op not allowed", 0); + } + } else if (operation.isWriteOperation()) { if (enforceWritePermission(callingPkg, uri, null) != AppOpsManager.MODE_ALLOWED) { throw new OperationApplicationException("App op not allowed", 0); @@ -304,7 +313,7 @@ public ContentProviderResult[] applyBatch(String callingPkg, public int delete(String callingPkg, Uri uri, String selection, String[] selectionArgs) { validateIncomingUri(uri); uri = getUriWithoutUserId(uri); - if (enforceWritePermission(callingPkg, uri, null) != AppOpsManager.MODE_ALLOWED) { + if (enforceDeletePermission(callingPkg, uri) != AppOpsManager.MODE_ALLOWED) { return 0; } final String original = setCallingPackage(callingPkg); @@ -464,6 +473,31 @@ private int enforceWritePermission(String callingPkg, Uri uri, IBinder callerTok } return AppOpsManager.MODE_ALLOWED; } + + private int enforceDeletePermission(String callingPkg, Uri uri) throws SecurityException { + enforceWritePermissionInner(uri, null); + if (mWriteOp != AppOpsManager.OP_NONE) { + int op = mWriteOp; + switch (mWriteOp) { + case AppOpsManager.OP_WRITE_SMS: + op = AppOpsManager.OP_DELETE_SMS; + break; + case AppOpsManager.OP_WRITE_MMS: + op = AppOpsManager.OP_DELETE_MMS; + break; + case AppOpsManager.OP_WRITE_CONTACTS: + op = AppOpsManager.OP_DELETE_CONTACTS; + break; + case AppOpsManager.OP_WRITE_CALL_LOG: + op = AppOpsManager.OP_DELETE_CALL_LOG; + break; + default: + break; + } + return mAppOpsManager.noteOp(op, Binder.getCallingUid(), callingPkg); + } + return AppOpsManager.MODE_ALLOWED; + } } boolean checkUser(int pid, int uid, Context context) { diff --git a/core/java/android/content/ContentProviderOperation.java b/core/java/android/content/ContentProviderOperation.java index 136e54da5cb..058f5974304 100644 --- a/core/java/android/content/ContentProviderOperation.java +++ b/core/java/android/content/ContentProviderOperation.java @@ -208,6 +208,11 @@ public int getType() { return mType; } + /** @hide */ + public boolean isDeleteOperation() { + return mType == TYPE_DELETE; + } + public boolean isWriteOperation() { return mType == TYPE_DELETE || mType == TYPE_INSERT || mType == TYPE_UPDATE; } diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 26735a6c6a6..5fdd9ee0237 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -425,6 +425,9 @@ public int getThemeResId() { @ViewDebug.ExportedProperty(deepExport = true) public abstract Resources.Theme getTheme(); + /** @hide */ + public abstract void recreateTheme(); + /** * Retrieve styled attribute information in this Context's theme. See * {@link Resources.Theme#obtainStyledAttributes(int[])} @@ -2706,6 +2709,16 @@ public abstract boolean startInstrumentation(@NonNull ComponentName className, */ public static final String BATTERY_SERVICE = "batterymanager"; + /** + * Use with {@link #getSystemService} to retrieve a + * {@link android.content.res.ThemeManager} for accessing theme service. + * + * @see #getSystemService + * @see android.content.res.ThemeManager + * @hide + */ + public static final String THEME_SERVICE = "themes"; + /** * Use with {@link #getSystemService} to retrieve a * {@link android.nfc.NfcManager} for using NFC. @@ -2915,6 +2928,25 @@ public abstract boolean startInstrumentation(@NonNull ComponentName className, */ public static final String MEDIA_PROJECTION_SERVICE = "media_projection"; + /** + * Use with {@link #getSystemService} to retrieve a + * {@link com.android.server.TorchService} for accessing torch service. + * + * @see #getSystemService + * @see com.android.server.TorchService + * @hide + */ + public static final String TORCH_SERVICE = "torch"; + + /** + * {@link com.android.server.KillSwitchService}for accessing the kill switch service. + * + * @see #getSystemService + * @see com.android.server.KillSwitchService + * @hide + */ + public static final String KILLSWITCH_SERVICE = "killswitch"; + /** * Determine whether the given permission is allowed for a particular * process and user ID running in the system. @@ -3377,6 +3409,26 @@ public abstract Context createPackageContextAsUser( public abstract Context createApplicationContext(ApplicationInfo application, int flags) throws PackageManager.NameNotFoundException; + /** + * Similar to {@link #createPackageContext(String, int)}, but with a + * different {@link UserHandle}. For example, {@link #getContentResolver()} + * will open any {@link Uri} as the given user. A theme package can be + * specified which will be used when adding resources to this context + * + * @hide + */ + public abstract Context createPackageContextAsUser( + String packageName, String themePackageName, int flags, UserHandle user) + throws PackageManager.NameNotFoundException; + + /** + * Creates a context given an {@link android.content.pm.ApplicationInfo}. + * + * @hide + */ + public abstract Context createApplicationContext(ApplicationInfo application, + String themePackageName, int flags) throws PackageManager.NameNotFoundException; + /** * Get the userId associated with this context * @return user id diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java index cfae1cfeb0d..c9e93c278db 100644 --- a/core/java/android/content/ContextWrapper.java +++ b/core/java/android/content/ContextWrapper.java @@ -122,6 +122,12 @@ public Resources.Theme getTheme() { return mBase.getTheme(); } + /** @hide */ + @Override + public void recreateTheme() { + mBase.recreateTheme(); + } + @Override public ClassLoader getClassLoader() { return mBase.getClassLoader(); @@ -681,7 +687,20 @@ public Context createPackageContextAsUser(String packageName, int flags, UserHan /** @hide */ public Context createApplicationContext(ApplicationInfo application, int flags) throws PackageManager.NameNotFoundException { - return mBase.createApplicationContext(application, flags); + return createApplicationContext(application, null, flags); + } + + /** @hide */ + public Context createApplicationContext(ApplicationInfo application, + String themePackageName, int flags) throws PackageManager.NameNotFoundException { + return mBase.createApplicationContext(application, themePackageName, flags); + } + + /** @hide */ + @Override + public Context createPackageContextAsUser(String packageName, String themePackageName, + int flags, UserHandle user) throws PackageManager.NameNotFoundException { + return mBase.createPackageContextAsUser(packageName, themePackageName, flags, user); } /** @hide */ diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 2fe727cfaf7..a228da41140 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2006 The Android Open Source Project + * This code has been modified. Portions copyright (C) 2010, T-Mobile USA, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -694,6 +695,28 @@ public class Intent implements Parcelable, Cloneable { @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_PICK = "android.intent.action.PICK"; + /** + * Global Action: Shows power menu reboot dialog + *

Input: nothing + *

Output: nothing + * @hide + */ + public static final String ACTION_POWERMENU_REBOOT = "android.intent.action.POWERMENU_REBOOT"; + + /** + * Registered and foreground services only + * @hide + */ + public static final String ACTION_ACTIVITY_LAUNCH_DETECTOR = + "android.intent.action.ACTIVITY_LAUNCH_DETECTOR"; + + /** + * Registered and foreground services only + * @hide + */ + public static final String ACTION_ACTIVITY_END_DETECTOR = + "android.intent.action.ACTIVITY_END_DETECTOR"; + /** * Activity Action: Creates a shortcut. *

Input: Nothing.

@@ -1310,6 +1333,15 @@ public static Intent createChooser(Intent target, CharSequence title, IntentSend @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_SEARCH_LONG_PRESS = "android.intent.action.SEARCH_LONG_PRESS"; + /** + * Activity Action: Start action associated with long press on the recents key. + *

Input: {@link #EXTRA_LONG_PRESS_RELEASE} is set to true if the long press + * is released + *

Output: Nothing + * @hide + */ + public static final String ACTION_RECENTS_LONG_PRESS = "android.intent.action.RECENTS_LONG_PRESS"; + /** * Activity Action: The user pressed the "Report" button in the crash/ANR dialog. * This intent is delivered to the package which installed the application, usually @@ -1640,6 +1672,15 @@ public static Intent createChooser(Intent target, CharSequence title, IntentSend * dialogs are the notification window-shade and the recent tasks dialog. */ public static final String ACTION_CLOSE_SYSTEM_DIALOGS = "android.intent.action.CLOSE_SYSTEM_DIALOGS"; + /** + * Broadcast Action: Update preferences for the power menu dialog. This is to provide a + * way for the preferences that need to be enabled/disabled to update because they were + * toggled elsewhere in the settings (ie profiles, immersive desktop, etc) so we don't have + * to do constant lookups while we wait for the menu to be created. Getting the values once + * when necessary is enough. + *@hide + */ + public static final String UPDATE_POWER_MENU = "android.intent.action.UPDATE_POWER_MENU"; /** * Broadcast Action: Trigger the download and eventual installation * of a package. @@ -1902,6 +1943,15 @@ public static Intent createChooser(Intent target, CharSequence title, IntentSend */ @Deprecated @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_WALLPAPER_CHANGED = "android.intent.action.WALLPAPER_CHANGED"; + + /** + * Broadcast Action: The current keyguard wallpaper configuration + * has changed and should be re-read. + * {@hide} + */ + public static final String ACTION_KEYGUARD_WALLPAPER_CHANGED = + "android.intent.action.KEYGUARD_WALLPAPER_CHANGED"; + /** * Broadcast Action: The current device {@link android.content.res.Configuration} * (orientation, locale, etc) has changed. When such a change happens, the @@ -2245,6 +2295,21 @@ public static Intent createChooser(Intent target, CharSequence title, IntentSend @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_AIRPLANE_MODE_CHANGED = "android.intent.action.AIRPLANE_MODE"; + /** + *

Broadcast Action: The user has changed carrier label:

+ *
    + *
  • state - String value.
  • + *
+ * + *

This is a protected intent that can only be sent + * by the system. + * + * @hide + */ + //@SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_CUSTOM_CARRIER_LABEL_CHANGED + = "android.intent.action.CUSTOM_CARRIER_LABEL"; + /** * Broadcast Action: Some content providers have parts of their namespace * where they publish new events or items that the user may be especially @@ -2287,6 +2352,7 @@ public static Intent createChooser(Intent target, CharSequence title, IntentSend @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_HEADSET_PLUG = android.media.AudioManager.ACTION_HEADSET_PLUG; + /** *

Broadcast Action: The user has switched on advanced settings in the settings app:

*
    @@ -2354,6 +2420,26 @@ public static Intent createChooser(Intent target, CharSequence title, IntentSend public static final String ACTION_NEW_OUTGOING_CALL = "android.intent.action.NEW_OUTGOING_CALL"; + + /** + * Broadcast Action: An outgoing sms is about to be sent. + * + * The Intent will have the following extras: + * destAddr - the phone number originally intended to be dialled + * scAddr - the service center address + * multipart - indicate whether this is a multipart or single message + * parts - ArrayList of text parts (one item if multipart=false) + * sentIntents - ArrayList to send on send + * deliveryIntents - ArrayList to send on delivery + * + * Once the broadcast is finished, resultData is used as the actual + * number to text. + * + * @hide + */ + public static final String ACTION_NEW_OUTGOING_SMS = + "android.intent.action.NEW_OUTGOING_SMS"; + /** * Broadcast Action: Have the device reboot. This is only for use by * system code. @@ -2485,6 +2571,30 @@ public static Intent createChooser(Intent target, CharSequence title, IntentSend public static final String ACTION_GET_RESTRICTION_ENTRIES = "android.intent.action.GET_RESTRICTION_ENTRIES"; + /** + *

    Broadcast Action: The state of the HOTWORD audio input has changed.:

    + *
      + *
    • state - A String value indicating the state of the input. + * {@link #EXTRA_HOTWORD_INPUT_STATE}. The value will be one of: + * {@link android.media.AudioRecord#RECORDSTATE_RECORDING} or + * {@link android.media.AudioRecord#RECORDSTATE_STOPPED}. + *
    • + *
    • package - A String value indicating the package name of the application + * that currently holds the HOTWORD input. + * {@link #EXTRA_CURRENT_PACKAGE_NAME} + *
    • + *
    + * + *

    This is a protected intent that can only be sent + * by the system. It can only be received by packages that hold + * {@link android.Manifest.permission#CAPTURE_AUDIO_HOTWORD}. + * + * @hide + */ + //@SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_HOTWORD_INPUT_CHANGED + = "com.cyanogenmod.intent.action.HOTWORD_INPUT_CHANGED"; + /** * @hide * Activity to challenge the user for a PIN that was configured when setting up @@ -2664,6 +2774,21 @@ public static Intent createChooser(Intent target, CharSequence title, IntentSend public static final String ACTION_QUICK_CLOCK = "android.intent.action.QUICK_CLOCK"; + /** + * Broadcast Action: Indicate that unrecoverable error happened during app launch. + * Could indicate that curently applied theme is malicious. + * @hide + */ + public static final String ACTION_APP_FAILURE = + "com.tmobile.intent.action.APP_FAILURE"; + + /** + * Broadcast Action: Request to reset the unrecoverable errors count to 0. + * @hide + */ + public static final String ACTION_APP_FAILURE_RESET = + "com.tmobile.intent.action.APP_FAILURE_RESET"; + /** * Activity Action: Shows the brightness setting dialog. * @hide @@ -2671,6 +2796,13 @@ public static Intent createChooser(Intent target, CharSequence title, IntentSend public static final String ACTION_SHOW_BRIGHTNESS_DIALOG = "android.intent.action.SHOW_BRIGHTNESS_DIALOG"; + /** + * Activity Action: Shows the notification brightness setting dialog. + * @hide + */ + public static final String ACTION_SHOW_NOTIFICATION_BRIGHTNESS_DIALOG = + "android.intent.action.SHOW_NOTIFICATION_BRIGHTNESS_DIALOG"; + /** * Broadcast Action: A global button was pressed. Includes a single * extra field, {@link #EXTRA_KEY_EVENT}, containing the key event that @@ -2759,6 +2891,19 @@ public static Intent createChooser(Intent target, CharSequence title, IntentSend @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_CREATE_DOCUMENT = "android.intent.action.CREATE_DOCUMENT"; + /** + * Broadcast Action: A theme's resources were cached. Includes two extra fields, + * {@link #EXTRA_THEME_PACKAGE_NAME}, containing the package name of the theme that was + * processed, and {@link #EXTRA_THEME_RESULT}, containing the result code. + * + *

    This is a protected intent that can only be sent + * by the system.

    + * + * @hide + */ + public static final String ACTION_THEME_RESOURCES_CACHED = + "android.intent.action.THEME_RESOURCES_CACHED"; + /** * Activity Action: Allow the user to pick a directory subtree. When * invoked, the system will display the various {@link DocumentsProvider} @@ -2781,6 +2926,10 @@ public static Intent createChooser(Intent target, CharSequence title, IntentSend /** {@hide} */ public static final String ACTION_MASTER_CLEAR = "android.intent.action.MASTER_CLEAR"; + /** {@hide} */ + public static final String ACTION_DOZE_PULSE_STARTING = + "android.intent.action.DOZE_PULSE_STARTING"; + // --------------------------------------------------------------------- // --------------------------------------------------------------------- // Standard intent categories (see addCategory()). @@ -2977,6 +3126,14 @@ public static Intent createChooser(Intent target, CharSequence title, IntentSend @SdkConstant(SdkConstantType.INTENT_CATEGORY) public static final String CATEGORY_CAR_MODE = "android.intent.category.CAR_MODE"; + /** + * Used to indicate that a theme package has been installed or un-installed. + * + * @hide + */ + public static final String CATEGORY_THEME_PACKAGE_INSTALLED_STATE_CHANGE = + "com.tmobile.intent.category.THEME_PACKAGE_INSTALL_STATE_CHANGE"; + // --------------------------------------------------------------------- // --------------------------------------------------------------------- // Application launch intent categories (see addCategory()). @@ -3494,6 +3651,15 @@ public static Intent createChooser(Intent target, CharSequence title, IntentSend public static final String EXTRA_SHUTDOWN_USERSPACE_ONLY = "android.intent.extra.SHUTDOWN_USERSPACE_ONLY"; + /** + * This field is part of the intent {@link #ACTION_RECENTS_LONG_PRESS}. + * The type of the extra is a boolean that indicates if the long press + * is released. + * @hide + */ + public static final String EXTRA_RECENTS_LONG_PRESS_RELEASE = + "android.intent.extra.RECENTS_LONG_PRESS_RELEASE"; + /** * Optional boolean extra for {@link #ACTION_TIME_CHANGED} that indicates the * user has set their time format preferences to the 24 hour format. @@ -3506,6 +3672,43 @@ public static Intent createChooser(Intent target, CharSequence title, IntentSend /** {@hide} */ public static final String EXTRA_REASON = "android.intent.extra.REASON"; + /** + * Extra for {@link #ACTION_THEME_RESOURCES_CACHED} that provides the return value + * from processThemeResources. A value of 0 indicates a successful caching of resources. + * Error results are: + * {@link android.content.pm.PackageManager#INSTALL_FAILED_THEME_AAPT_ERROR} + * {@link android.content.pm.PackageManager#INSTALL_FAILED_THEME_IDMAP_ERROR} + * {@link android.content.pm.PackageManager#INSTALL_FAILED_THEME_UNKNOWN_ERROR} + * + * @hide + */ + public static final String EXTRA_THEME_RESULT = "android.intent.extra.RESULT"; + + /** + * Extra for {@link #ACTION_THEME_RESOURCES_CACHED} that provides the package name of the + * theme that was processed. + * + * @hide + */ + public static final String EXTRA_THEME_PACKAGE_NAME = "android.intent.extra.PACKAGE_NAME"; + + /** + * Extra for {@link #ACTION_RECENTS_LONG_PRESS} that provides the package name of the + * app in foreground when recents was long pressed. Can be reused for other purposes. + * @hide + */ + public static final String EXTRA_CURRENT_PACKAGE_NAME = + "com.cyanogenmod.intent.extra.CURRENT_PACKAGE_NAME"; + + /** + * Extra for {@link #ACTION_HOTWORD_INPUT_CHANGED} that provides the state of + * the input when the broadcast action was sent. + * @hide + */ + public static final String EXTRA_HOTWORD_INPUT_STATE = + "com.cyanogenmod.intent.extra.HOTWORD_INPUT_STATE"; + + // --------------------------------------------------------------------- // --------------------------------------------------------------------- // Intent flags (see mFlags variable). @@ -3869,7 +4072,19 @@ public static boolean isAccessUriMode(int modeFlags) { * Activity.finishAndRemoveTask()}. */ public static final int FLAG_ACTIVITY_RETAIN_IN_RECENTS = 0x00002000; - + /** + * If set, this intent will always match start up as a floating window + * in multi window scenarios. + * @hide + */ + public static final int FLAG_FLOATING_WINDOW = 0x00003000; + /** + * If set in an Intent passed to {@link Context#startActivity Context.startActivity()}, + * this flag will cause a newly launching task to be resized according to the split + * view metrics, making it running alongside another app. + * @hide + */ + public static final int FLAG_ACTIVITY_SPLIT_VIEW = 0x00001000; /** * If set, when sending a broadcast only registered receivers will be * called -- no BroadcastReceiver components will be launched. @@ -6585,6 +6800,9 @@ public Intent putExtras(Intent src) { * @see #removeExtra */ public Intent putExtras(Bundle extras) { + if (extras == null) { + return this; + } if (mExtras == null) { mExtras = new Bundle(); } diff --git a/core/java/android/content/IntentFilter.java b/core/java/android/content/IntentFilter.java index 1240a23d08e..5c54038c69d 100644 --- a/core/java/android/content/IntentFilter.java +++ b/core/java/android/content/IntentFilter.java @@ -1616,6 +1616,15 @@ public final void writeToParcel(Parcel dest, int flags) { dest.writeInt(mHasPartialTypes ? 1 : 0); } + /** + * {@hide} + * @param other + * @return + */ + public int onCompareTie(IntentFilter other) { + return 0; + } + /** * For debugging -- perform a check on the filter, return true if it passed * or false if it failed. diff --git a/core/java/android/content/ThemeVersion.java b/core/java/android/content/ThemeVersion.java new file mode 100644 index 00000000000..05fbc418b01 --- /dev/null +++ b/core/java/android/content/ThemeVersion.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2015 The CyanogenMod 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.content; + +/** + * Warning: Careful moving/refactoring this class as our SDK references it. + * ThemeVersion 1 = CM11 + * ThemeVersion 2 = CM12/CM12.1 First Release + * ThemeVersion 3 = CM12.1 W/ Wallpaper Packs + * @hide + */ +public class ThemeVersion { + /** + * Increment this anytime changes are made to: + * 1) Changes to ThemesContract + * 2) Changes to ThemeService API + * 3) Changes to ThemeManager API + */ + public static int THEME_VERSION = 3; + + /** + * Change this if a change to the contract or service would break compatibility. + * Example: A client app like chooser might be outdated from the framework. + * It could then query the FW for this value and determine whether its safe to proceed. + */ + public static int MIN_SUPPORTED_THEME_VERSION = 2; + + /** + * Do not change the order of this. See SDK. + * Increment the minSupportedVersion when the fw can no longer support a theme's apk structure + * Increment currentVersion when a change to the theme's apk structure is changed + * For example, CM11 to CM12 introduces new resources to overlay, so the overlays + * version should change. Because the changes are not compatible with CM11, the minVersion + * must change as well. + * + * If a new feature is added to a component (ex rotations in icon packs), the current version + * for the ICON component would be incremented. If a new component is created, then add it + * to the enum list. + * + * Wallpaper Version 2: Multi wallpaper ability + * + */ + public static enum ComponentVersion { + OVERLAY(0, 2, 2), + BOOT_ANIM(1, 1, 1), + WALLPAPER(2, 1, 2), + LOCKSCREEN(3, 1, 1), + FONT(4, 1, 2), + ICON(5, 1, 1), + SOUNDS(6, 1, 1); + + public int id; + public int minSupportedVersion; + public int currentVersion; + + private ComponentVersion(int id, int minSupportedVersion, int currentVersion) { + this.id = id; + this.minSupportedVersion = minSupportedVersion; + this.currentVersion = currentVersion; + } + } +} diff --git a/core/java/android/content/UriMatcher.java b/core/java/android/content/UriMatcher.java index 8487dae4f7b..71a035e47f1 100644 --- a/core/java/android/content/UriMatcher.java +++ b/core/java/android/content/UriMatcher.java @@ -20,7 +20,6 @@ import java.util.ArrayList; import java.util.List; -import java.util.regex.Pattern; /** Utility class to aid in matching URIs in content providers. @@ -171,7 +170,7 @@ public void addURI(String authority, String path, int code) if (path.length() > 0 && path.charAt(0) == '/') { newPath = path.substring(1); } - tokens = PATH_SPLIT_PATTERN.split(newPath); + tokens = newPath.split("/"); } int numTokens = tokens != null ? tokens.length : 0; @@ -207,8 +206,6 @@ public void addURI(String authority, String path, int code) node.mCode = code; } - static final Pattern PATH_SPLIT_PATTERN = Pattern.compile("/"); - /** * Try to match against the path in a url. * diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index 641f843ea2d..a0bd11a56a9 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -1,6 +1,7 @@ /* * Copyright (C) 2007 The Android Open Source Project - * + * This code has been modified. Portions copyright (C) 2010, T-Mobile USA, Inc. + * 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 @@ -503,6 +504,13 @@ public class ActivityInfo extends ComponentInfo * {@link android.R.attr#configChanges} attribute. */ public static final int CONFIG_UI_MODE = 0x0200; + /** + * Bit in {@link #configChanges} that indicates that the activity + * can itself handle the inverted ui mode. Set from the + * {@link android.R.attr#configChanges} attribute. + * @hide + */ + public static final int CONFIG_UI_THEME_MODE = 0x0300; /** * Bit in {@link #configChanges} that indicates that the activity * can itself handle the screen size. Set from the @@ -539,6 +547,16 @@ public class ActivityInfo extends ComponentInfo * {@link android.R.attr#configChanges} attribute. */ public static final int CONFIG_LAYOUT_DIRECTION = 0x2000; + /** + * Bit in {@link #configChanges} that indicates a theme change occurred + * @hide + */ + public static final int CONFIG_THEME_RESOURCE = 0x100000; + /** + * Bit in {@link #configChanges} that indicates a font change occurred + * @hide + */ + public static final int CONFIG_THEME_FONT = 0x200000; /** * Bit in {@link #configChanges} that indicates that the activity * can itself handle changes to the font scaling factor. Set from the @@ -563,6 +581,7 @@ public class ActivityInfo extends ComponentInfo Configuration.NATIVE_CONFIG_NAVIGATION, // NAVIGATION Configuration.NATIVE_CONFIG_ORIENTATION, // ORIENTATION Configuration.NATIVE_CONFIG_SCREEN_LAYOUT, // SCREEN LAYOUT + Configuration.NATIVE_CONFIG_UI_THEME_MODE, // UI THEME MODE Configuration.NATIVE_CONFIG_UI_MODE, // UI MODE Configuration.NATIVE_CONFIG_SCREEN_SIZE, // SCREEN SIZE Configuration.NATIVE_CONFIG_SMALLEST_SCREEN_SIZE, // SMALLEST SCREEN SIZE diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index e07edba8892..5e342c9c5f0 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2007 The Android Open Source Project + * This code has been modified. Portions copyright (C) 2010, T-Mobile USA, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -587,6 +588,19 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { */ public int installLocation = PackageInfo.INSTALL_LOCATION_UNSPECIFIED; + /** + * Is given application theme agnostic, i.e. behaves properly when default theme is changed. + * {@hide} + */ + public boolean isThemeable = false; + + /** + * When true, indicates that any one component within this application is + * protected. + * @hide + */ + public boolean protect = false; + public void dump(Printer pw, String prefix) { super.dumpFront(pw, prefix); if (className != null) { @@ -709,6 +723,8 @@ public ApplicationInfo(ApplicationInfo orig) { descriptionRes = orig.descriptionRes; uiOptions = orig.uiOptions; backupAgentName = orig.backupAgentName; + protect = orig.protect; + isThemeable = orig.isThemeable; } @@ -759,6 +775,8 @@ public void writeToParcel(Parcel dest, int parcelableFlags) { dest.writeString(backupAgentName); dest.writeInt(descriptionRes); dest.writeInt(uiOptions); + dest.writeInt(protect ? 1 : 0); + dest.writeInt(isThemeable ? 1 : 0); } public static final Parcelable.Creator CREATOR @@ -808,6 +826,8 @@ private ApplicationInfo(Parcel source) { backupAgentName = source.readString(); descriptionRes = source.readInt(); uiOptions = source.readInt(); + protect = source.readInt() != 0; + isThemeable = source.readInt() != 0; } /** diff --git a/core/java/android/content/pm/BaseThemeInfo.java b/core/java/android/content/pm/BaseThemeInfo.java new file mode 100644 index 00000000000..8ece42d1933 --- /dev/null +++ b/core/java/android/content/pm/BaseThemeInfo.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2010, T-Mobile USA, Inc. + * + * 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.content.pm; + +import android.os.Parcelable; +import android.os.Parcel; +import android.util.Log; +import android.util.AttributeSet; +import android.content.res.Resources; + +/** + * @hide + */ +public class BaseThemeInfo implements Parcelable { + /** + * The theme id, which does not change when the theme is modified. + * Specifies an Android UI Style using style name. + * + * @see themeId attribute + * + */ + public String themeId; + + /** + * The name of the theme (as displayed by UI). + * + * @see name attribute + * + */ + public String name; + + /** + * The author name of the theme package. + * + * @see author attribute + * + */ + public String author; + + /* + * Describe the kinds of special objects contained in this Parcelable's + * marshalled representation. + * + * @return a bitmask indicating the set of special object types marshalled + * by the Parcelable. + * + * @see android.os.Parcelable#describeContents() + */ + public int describeContents() { + return 0; + } + + /* + * Flatten this object in to a Parcel. + * + * @param dest The Parcel in which the object should be written. + * @param flags Additional flags about how the object should be written. + * May be 0 or {@link #PARCELABLE_WRITE_RETURN_VALUE}. + * + * @see android.os.Parcelable#writeToParcel(android.os.Parcel, int) + */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(themeId); + dest.writeString(name); + dest.writeString(author); + } + + /** @hide */ + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public BaseThemeInfo createFromParcel(Parcel source) { + return new BaseThemeInfo(source); + } + + public BaseThemeInfo[] newArray(int size) { + return new BaseThemeInfo[size]; + } + }; + + /** @hide */ + public final String getResolvedString(Resources res, AttributeSet attrs, int index) { + int resId = attrs.getAttributeResourceValue(index, 0); + if (resId !=0 ) { + return res.getString(resId); + } + return attrs.getAttributeValue(index); + } + + protected BaseThemeInfo() { + } + + protected BaseThemeInfo(Parcel source) { + themeId = source.readString(); + name = source.readString(); + author = source.readString(); + } +} diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index 0dc86ad0cab..ab3c84fd80f 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -17,6 +17,7 @@ package android.content.pm; +import android.app.ComposedIconInfo; import android.content.ComponentName; import android.content.Intent; import android.content.IntentFilter; @@ -456,4 +457,19 @@ interface IPackageManager { KeySet getSigningKeySet(String packageName); boolean isPackageSignedByKeySet(String packageName, in KeySet ks); boolean isPackageSignedByKeySetExactly(String packageName, in KeySet ks); + + /** Protected Apps */ + void setComponentProtectedSetting(in ComponentName componentName, + in boolean newState, int userId); + + /** Themes */ + void updateIconMapping(String pkgName); + ComposedIconInfo getComposedIconInfo(); + int processThemeResources(String themePkgName); + + /** Package interception */ + void setPreLaunchCheckActivity(in ComponentName componentName); + void addPreLaunchCheckPackage(String packageName); + void removePreLaunchCheckPackage(String packageName); + void clearPreLaunchCheckPackages(); } diff --git a/core/java/android/content/pm/PackageInfo.java b/core/java/android/content/pm/PackageInfo.java index 92232693e7d..9787e6d5c4b 100644 --- a/core/java/android/content/pm/PackageInfo.java +++ b/core/java/android/content/pm/PackageInfo.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2007 The Android Open Source Project + * This code has been modified. Portions copyright (C) 2010, T-Mobile USA, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +17,11 @@ package android.content.pm; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + import android.os.Parcel; import android.os.Parcelable; @@ -253,6 +259,34 @@ public class PackageInfo implements Parcelable { /** @hide */ public boolean coreApp; + // Is Theme Apk + /** + * {@hide} + */ + public boolean isThemeApk = false; + + /** + * {@hide} + */ + public boolean hasIconPack = false; + + /** + * {@hide} + */ + public ArrayList mOverlayTargets; + + // Is Legacy Icon Apk + /** + * {@hide} + */ + public boolean isLegacyIconPackApk = false; + + // ThemeInfo + /** + * {@hide} + */ + public ThemeInfo themeInfo; + /** @hide */ public boolean requiredForAllUsers; @@ -322,6 +356,13 @@ public void writeToParcel(Parcel dest, int parcelableFlags) { dest.writeString(restrictedAccountType); dest.writeString(requiredAccountType); dest.writeString(overlayTarget); + + /* Theme-specific. */ + dest.writeInt((isThemeApk) ? 1 : 0); + dest.writeStringList(mOverlayTargets); + dest.writeParcelable(themeInfo, parcelableFlags); + dest.writeInt(hasIconPack ? 1 : 0); + dest.writeInt((isLegacyIconPackApk) ? 1 : 0); } public static final Parcelable.Creator CREATOR @@ -371,5 +412,12 @@ private PackageInfo(Parcel source) { restrictedAccountType = source.readString(); requiredAccountType = source.readString(); overlayTarget = source.readString(); + + /* Theme-specific. */ + isThemeApk = (source.readInt() != 0); + mOverlayTargets = source.createStringArrayList(); + themeInfo = source.readParcelable(null); + hasIconPack = source.readInt() == 1; + isLegacyIconPackApk = source.readInt() == 1; } } diff --git a/core/java/android/content/pm/PackageInfoLite.java b/core/java/android/content/pm/PackageInfoLite.java index 1efe082b7fd..d4f33fbed6d 100644 --- a/core/java/android/content/pm/PackageInfoLite.java +++ b/core/java/android/content/pm/PackageInfoLite.java @@ -62,6 +62,7 @@ public class PackageInfoLite implements Parcelable { */ public int recommendedInstallLocation; public int installLocation; + public boolean isTheme; public VerifierInfo[] verifiers; @@ -87,6 +88,7 @@ public void writeToParcel(Parcel dest, int parcelableFlags) { dest.writeInt(recommendedInstallLocation); dest.writeInt(installLocation); dest.writeInt(multiArch ? 1 : 0); + dest.writeInt(isTheme ? 1 : 0); if (verifiers == null || verifiers.length == 0) { dest.writeInt(0); @@ -116,6 +118,7 @@ private PackageInfoLite(Parcel source) { recommendedInstallLocation = source.readInt(); installLocation = source.readInt(); multiArch = (source.readInt() != 0); + isTheme = source.readInt() == 1 ? true : false; final int verifiersLength = source.readInt(); if (verifiersLength == 0) { diff --git a/core/java/android/content/pm/PackageItemInfo.java b/core/java/android/content/pm/PackageItemInfo.java index 22a899cda05..366deb4f9ae 100644 --- a/core/java/android/content/pm/PackageItemInfo.java +++ b/core/java/android/content/pm/PackageItemInfo.java @@ -66,7 +66,14 @@ public class PackageItemInfo { * component's icon. From the "icon" attribute or, if not set, 0. */ public int icon; - + + /** + * A drawable resource identifier in the icon pack's resources + * If there isn't an icon pack or not set, then 0. + * @hide + */ + public int themedIcon; + /** * A drawable resource identifier (in the package's resources) of this * component's banner. From the "banner" attribute or, if not set, 0. @@ -110,6 +117,7 @@ public PackageItemInfo(PackageItemInfo orig) { logo = orig.logo; metaData = orig.metaData; showUserIcon = orig.showUserIcon; + themedIcon = orig.themedIcon; } /** @@ -309,8 +317,9 @@ public void writeToParcel(Parcel dest, int parcelableFlags) { dest.writeBundle(metaData); dest.writeInt(banner); dest.writeInt(showUserIcon); + dest.writeInt(themedIcon); } - + protected PackageItemInfo(Parcel source) { name = source.readString(); packageName = source.readString(); @@ -322,6 +331,7 @@ protected PackageItemInfo(Parcel source) { metaData = source.readBundle(); banner = source.readInt(); showUserIcon = source.readInt(); + themedIcon = source.readInt(); } /** diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index e9f7c50431b..459cf046149 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -1,4 +1,7 @@ /* + * Copyright (c) 2013-2014, The Linux Foundation. All rights reserved. + * Not a Contribution. + * * Copyright (C) 2006 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -201,6 +204,13 @@ public NameNotFoundException(String name) { */ public static final int MATCH_DEFAULT_ONLY = 0x00010000; + /** + * Resolution flag: If there is only 1 activity that the intent resolves too and if this + * flag is set perform pre launch check for the resolved activity. + * @hide + */ + public static final int PERFORM_PRE_LAUNCH_CHECK = 0x00100000; + /** * Flag for {@link addCrossProfileIntentFilter}: if this flag is set: * when resolving an intent that matches the {@link CrossProfileIntentFilter}, the current @@ -623,6 +633,14 @@ public NameNotFoundException(String name) { */ public static final int INSTALL_FAILED_VERSION_DOWNGRADE = -25; + /** + * Installation return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the package is from unknown sources but not trusted sources. + * @hide + */ + public static final int INSTALL_FAILED_UNKNOWN_SOURCES = -26; + /** * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} @@ -770,6 +788,43 @@ public NameNotFoundException(String name) { /** {@hide} */ public static final int INSTALL_FAILED_ABORTED = -115; + /** + * Used by themes + * Installation failed return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} + * if the system failed to install the theme because aapt could not compile the app + * @hide + */ + public static final int INSTALL_FAILED_THEME_AAPT_ERROR = -400; + + /** + * Used by themes + * Installation failed return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} + * if the system failed to install the theme because idmap failed + * apps. + * @hide + */ + public static final int INSTALL_FAILED_THEME_IDMAP_ERROR = -401; + + /** + * Used by themes + * Installation failed return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} + * if the system failed to install the theme for an unknown reason + * apps. + * @hide + */ + public static final int INSTALL_FAILED_THEME_UNKNOWN_ERROR = -402; + + /** + * Used for prebundles + * Installation failed for a prebundled app because the user previously uninstalled it + * and we don't want to bring it back + * @hide + */ + public static final int INSTALL_FAILED_UNINSTALLED_PREBUNDLE = -403; + /** * Flag parameter for {@link #deletePackage} to indicate that you don't want to delete the * package's data directory. @@ -1585,6 +1640,12 @@ public NameNotFoundException(String name) { @SdkConstant(SdkConstantType.FEATURE) public static final String FEATURE_GAMEPAD = "android.hardware.gamepad"; + /** + * Feature for {@link #getSystemAvailableFeatures} and + * {@link #hasSystemFeature}: The device is capable of reading a user's fingerprint + * @hide + */ + public static final String FEATURE_FINGERPRINT = "cyanogenmod.hardware.fingerprint"; /** * Action to external storage service to clean out removed apps. @@ -1675,6 +1736,20 @@ public NameNotFoundException(String name) { public static final String EXTRA_REQUEST_PERMISSION_PERMISSION_LIST = "android.content.pm.extra.PERMISSION_LIST"; + /** + * Flag for {@link #setComponentProtectedSetting(android.content.ComponentName, boolean)}: + * This component or application has set to protected status + * @hide + */ + public static final boolean COMPONENT_PROTECTED_STATUS = false; + + /** + * Flag for {@link #setComponentProtectedSetting(android.content.ComponentName, boolean)}: + * This component or application has been explicitly set to visible status + * @hide + */ + public static final boolean COMPONENT_VISIBLE_STATUS = true; + /** * String extra for {@link PackageInstallObserver} in the 'extras' Bundle in case of * {@link #INSTALL_FAILED_DUPLICATE_PERMISSION}. This extra names the package which provides @@ -3096,6 +3171,18 @@ public abstract Resources getResourcesForApplication(String appPackageName) public abstract Resources getResourcesForApplicationAsUser(String appPackageName, int userId) throws NameNotFoundException; + /** @hide */ + public abstract Resources getThemedResourcesForApplication(ApplicationInfo app, + String themePkgName) throws NameNotFoundException; + + /** @hide */ + public abstract Resources getThemedResourcesForApplication(String appPackageName, + String themePkgName) throws NameNotFoundException; + + /** @hide */ + public abstract Resources getThemedResourcesForApplicationAsUser(String appPackageName, + String themePkgName, int userId) throws NameNotFoundException; + /** * Retrieve overall information about an application package defined * in a package archive file @@ -3891,6 +3978,12 @@ public static String getDataDirForUser(int userId, String packageName) { return Environment.getDataDirectory().toString() + "/user/" + userId + "/" + packageName; } + + /** + * Update Component protection state + * @hide + */ + public abstract void setComponentProtectedSetting(ComponentName componentName, boolean newState); /** * Adds a {@link CrossProfileIntentFilter}. After calling this method all intents sent from the @@ -4104,4 +4197,22 @@ public void onPackageDeleted(String basePackageName, int returnCode, String msg) } } } + + /** + * Updates the theme icon res id for the new theme + * @hide + */ + public abstract void updateIconMaps(String pkgName); + + /** + * Used to compile theme resources for a given theme + * @param themePkgName + * @return A value of 0 indicates success. Possible errors returned are: + * {@link android.content.pm.PackageManager#INSTALL_FAILED_THEME_AAPT_ERROR}, + * {@link android.content.pm.PackageManager#INSTALL_FAILED_THEME_IDMAP_ERROR}, or + * {@link android.content.pm.PackageManager#INSTALL_FAILED_THEME_UNKNOWN_ERROR} + * + * @hide + */ + public abstract int processThemeResources(String themePkgName); } diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index d7d9e8bf6a4..11c5d9184d2 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2007 The Android Open Source Project + * This code has been modified. Portions copyright (C) 2010, T-Mobile USA, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -58,6 +59,7 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; @@ -74,12 +76,17 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.jar.StrictJarFile; import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; /** * Parser for package files (APKs) on disk. This supports apps packaged either @@ -109,6 +116,17 @@ public class PackageParser { /** File name in an APK for the Android manifest. */ private static final String ANDROID_MANIFEST_FILENAME = "AndroidManifest.xml"; + /** Path to overlay directory in a theme APK */ + private static final String OVERLAY_PATH = "assets/overlays/"; + /** Path to icon directory in a theme APK */ + private static final String ICON_PATH = "assets/icons/"; + + private static final String PACKAGE_REDIRECTIONS_XML = "res/xml/redirections.xml"; + + private static final String TAG_PACKAGE_REDIRECTIONS = "package-redirections"; + private static final String TAG_RESOURCE_REDIRECTIONS = "resource-redirections"; + private static final String TAG_ITEM = "item"; + private static final String ATTRIBUTE_ITEM_NAME = "name"; /** @hide */ public static class NewPermissionInfo { @@ -245,6 +263,7 @@ public static class PackageLite { public final int versionCode; public final int installLocation; public final VerifierInfo[] verifiers; + public boolean isTheme; /** Names of any split APKs, ordered by parsed splitName */ public final String[] splitNames; @@ -269,6 +288,7 @@ public static class PackageLite { public final boolean coreApp; public final boolean multiArch; + public PackageLite(String codePath, ApkLite baseApk, String[] splitNames, String[] splitCodePaths, int[] splitRevisionCodes) { this.packageName = baseApk.packageName; @@ -283,6 +303,7 @@ public PackageLite(String codePath, ApkLite baseApk, String[] splitNames, this.splitRevisionCodes = splitRevisionCodes; this.coreApp = baseApk.coreApp; this.multiArch = baseApk.multiArch; + this.isTheme = baseApk.isTheme; } public List getAllCodePaths() { @@ -309,10 +330,11 @@ public static class ApkLite { public final Signature[] signatures; public final boolean coreApp; public final boolean multiArch; + public final boolean isTheme; public ApkLite(String codePath, String packageName, String splitName, int versionCode, int revisionCode, int installLocation, List verifiers, - Signature[] signatures, boolean coreApp, boolean multiArch) { + Signature[] signatures, boolean coreApp, boolean multiArch, boolean isTheme) { this.codePath = codePath; this.packageName = packageName; this.splitName = splitName; @@ -323,6 +345,7 @@ public ApkLite(String codePath, String packageName, String splitName, int versio this.signatures = signatures; this.coreApp = coreApp; this.multiArch = multiArch; + this.isTheme = isTheme; } } @@ -423,6 +446,14 @@ public static PackageInfo generatePackageInfo(PackageParser.Package p, pi.versionName = p.mVersionName; pi.sharedUserId = p.mSharedUserId; pi.sharedUserLabel = p.mSharedUserLabel; + pi.isThemeApk = p.mIsThemeApk; + pi.hasIconPack = p.hasIconPack; + pi.isLegacyIconPackApk = p.mIsLegacyIconPackApk; + + if (pi.isThemeApk) { + pi.mOverlayTargets = p.mOverlayTargets; + pi.themeInfo = p.mThemeInfo; + } pi.applicationInfo = generateApplicationInfo(p, flags, state, userId); pi.installLocation = p.installLocation; pi.coreApp = p.coreApp; @@ -614,6 +645,7 @@ private static Certificate[][] loadCertificates(StrictJarFile jarFile, ZipEntry public final static int PARSE_IS_PRIVILEGED = 1<<7; public final static int PARSE_COLLECT_CERTIFICATES = 1<<8; public final static int PARSE_TRUSTED_OVERLAY = 1<<9; + public final static int PARSE_IS_PREBUNDLED_DIR = 1<<10; private static final Comparator sSplitNameComparator = new SplitNameComparator(); @@ -875,7 +907,7 @@ private Package parseBaseApk(File apkFile, AssetManager assets, int flags) XmlResourceParser parser = null; try { res = new Resources(assets, mMetrics, null); - assets.setConfiguration(0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + assets.setConfiguration(0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, Build.VERSION.RESOURCES_SDK_INT); parser = assets.openXmlResourceParser(cookie, ANDROID_MANIFEST_FILENAME); @@ -889,6 +921,18 @@ private Package parseBaseApk(File apkFile, AssetManager assets, int flags) pkg.baseCodePath = apkPath; pkg.mSignatures = null; + // If the pkg is a theme, we need to know what themes it overlays + // and determine if it has an icon pack + if (pkg.mIsThemeApk) { + //Determine existance of Overlays + ArrayList overlayTargets = scanPackageOverlays(apkFile); + for(String overlay : overlayTargets) { + pkg.mOverlayTargets.add(overlay); + } + + pkg.hasIconPack = packageHasIconPack(apkFile); + } + return pkg; } catch (PackageParserException e) { @@ -917,7 +961,7 @@ private void parseSplitApk(Package pkg, int splitIndex, AssetManager assets, int XmlResourceParser parser = null; try { res = new Resources(assets, mMetrics, null); - assets.setConfiguration(0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + assets.setConfiguration(0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, Build.VERSION.RESOURCES_SDK_INT); parser = assets.openXmlResourceParser(cookie, ANDROID_MANIFEST_FILENAME); @@ -1011,6 +1055,68 @@ private Package parseSplitApk(Package pkg, Resources res, XmlResourceParser pars return pkg; } + + private ArrayList scanPackageOverlays(File originalFile) { + Set overlayTargets = new HashSet(); + ZipFile privateZip = null; + try { + privateZip = new ZipFile(originalFile.getPath()); + final Enumeration privateZipEntries = privateZip.entries(); + while (privateZipEntries.hasMoreElements()) { + final ZipEntry zipEntry = privateZipEntries.nextElement(); + final String zipEntryName = zipEntry.getName(); + + if (zipEntryName.startsWith(OVERLAY_PATH) && zipEntryName.length() > 16) { + String[] subdirs = zipEntryName.split("/"); + overlayTargets.add(subdirs[2]); + } + } + } catch(Exception e) { + e.printStackTrace(); + overlayTargets.clear(); + } finally { + if (privateZip != null) { + try { + privateZip.close(); + } catch (Exception e) { + //Ignore + } + } + } + + ArrayList overlays = new ArrayList(); + overlays.addAll(overlayTargets); + return overlays; + } + + private boolean packageHasIconPack(File originalFile) { + ZipFile privateZip = null; + try { + privateZip = new ZipFile(originalFile.getPath()); + final Enumeration privateZipEntries = privateZip.entries(); + while (privateZipEntries.hasMoreElements()) { + final ZipEntry zipEntry = privateZipEntries.nextElement(); + final String zipEntryName = zipEntry.getName(); + + if (zipEntryName.startsWith(ICON_PATH) && + zipEntryName.length() > ICON_PATH.length()) { + return true; + } + } + } catch(Exception e) { + Log.e(TAG, "Could not read zip entries while checking if apk has icon pack", e); + } finally { + if (privateZip != null) { + try { + privateZip.close(); + } catch (Exception e) { + //Ignore + } + } + } + return false; + } + /** * Gathers the {@link ManifestDigest} for {@code pkg} if it exists in the * APK. If it successfully scanned the package and found the @@ -1026,6 +1132,7 @@ public void collectManifestDigest(Package pkg) throws PackageParserException { final ZipEntry je = jarFile.findEntry(ANDROID_MANIFEST_FILENAME); if (je != null) { pkg.manifestDigest = ManifestDigest.fromInputStream(jarFile.getInputStream(je)); + pkg.manifestHashCode = ThemeUtils.getPackageHashCode(pkg); } } finally { jarFile.close(); @@ -1151,7 +1258,7 @@ public static ApkLite parseApkLite(File apkFile, int flags) XmlResourceParser parser = null; try { assets = new AssetManager(); - assets.setConfiguration(0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + assets.setConfiguration(0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, Build.VERSION.RESOURCES_SDK_INT); int cookie = assets.addAssetPath(apkPath); @@ -1286,6 +1393,9 @@ private static ApkLite parseApkLite(String codePath, Resources res, XmlPullParse // Only search the tree when the tag is directly below int type; final int searchDepth = parser.getDepth() + 1; + // Search for category and actions inside + final int iconPackSearchDepth = parser.getDepth() + 4; + boolean isTheme = false; final List verifiers = new ArrayList(); while ((type = parser.next()) != XmlPullParser.END_DOCUMENT @@ -1310,10 +1420,53 @@ private static ApkLite parseApkLite(String codePath, Resources res, XmlPullParse } } } + + if (parser.getDepth() == searchDepth && "meta-data".equals(parser.getName())) { + for (int i=0; i < parser.getAttributeCount(); i++) { + if ("name".equals(parser.getAttributeName(i)) && + ThemeInfo.META_TAG_NAME.equals(parser.getAttributeValue(i))) { + isTheme = true; + installLocation = PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY; + break; + } + } + } + + if (parser.getDepth() == searchDepth && "theme".equals(parser.getName())) { + isTheme = true; + installLocation = PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY; + } + + if (parser.getDepth() == iconPackSearchDepth && isLegacyIconPack(parser)) { + isTheme = true; + installLocation = PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY; + } } return new ApkLite(codePath, packageSplit.first, packageSplit.second, versionCode, - revisionCode, installLocation, verifiers, signatures, coreApp, multiArch); + revisionCode, installLocation, verifiers, signatures, coreApp, multiArch, + isTheme); + } + + private static boolean isLegacyIconPack(XmlPullParser parser) { + boolean isAction = "action".equals(parser.getName()); + boolean isCategory = "category".equals(parser.getName()); + String[] items = isAction ? ThemeUtils.sSupportedActions + : (isCategory ? ThemeUtils.sSupportedCategories : null); + + if (items != null) { + for (int i = 0; i < parser.getAttributeCount(); i++) { + if ("name".equals(parser.getAttributeName(i))) { + final String value = parser.getAttributeValue(i); + for (String item : items) { + if (item.equals(value)) { + return true; + } + } + } + } + } + return false; } /** @@ -1365,6 +1518,8 @@ private Package parseBaseApk(Resources res, XmlResourceParser parser, int flags, } final Package pkg = new Package(pkgName); + Bundle metaDataBundle = new Bundle(); + boolean foundApp = false; TypedArray sa = res.obtainAttributes(attrs, @@ -1772,6 +1927,11 @@ private Package parseBaseApk(Resources res, XmlResourceParser parser, int flags, XmlUtils.skipCurrentTag(parser); continue; + } else if (parser.getName().equals("meta-data")) { + if ((metaDataBundle=parseMetaData(res, parser, attrs, metaDataBundle, + outError)) == null) { + return null; + } } else if (RIGID_PARSER) { outError[0] = "Bad element under : " + parser.getName(); @@ -1862,6 +2022,9 @@ private Package parseBaseApk(Resources res, XmlResourceParser parser, int flags, >= android.os.Build.VERSION_CODES.DONUT)) { pkg.applicationInfo.flags |= ApplicationInfo.FLAG_SUPPORTS_SCREEN_DENSITIES; } + if (pkg.mIsThemeApk || pkg.mIsLegacyIconPackApk) { + pkg.applicationInfo.isThemeable = false; + } /* * b/8528162: Ignore the attribute if @@ -1874,6 +2037,14 @@ private Package parseBaseApk(Resources res, XmlResourceParser parser, int flags, } } + //Is this pkg a theme? + if (metaDataBundle.containsKey(ThemeInfo.META_TAG_NAME)) { + pkg.mIsThemeApk = true; + pkg.mTrustedOverlay = true; + pkg.mOverlayPriority = 1; + pkg.mThemeInfo = new ThemeInfo(metaDataBundle); + } + return pkg; } @@ -2250,6 +2421,10 @@ private Permission parsePermission(Package owner, Resources res, perm.info.flags = sa.getInt( com.android.internal.R.styleable.AndroidManifestPermission_permissionFlags, 0); + perm.info.allowViaWhitelist = sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestPermission_allowViaWhitelist, + false); + sa.recycle(); if (perm.info.protectionLevel == -1) { @@ -2402,6 +2577,10 @@ private boolean parseBaseApplication(Package owner, Resources res, final ApplicationInfo ai = owner.applicationInfo; final String pkgName = owner.applicationInfo.packageName; + String[] nonThemeablePackages = + res.getStringArray(com.android.internal.R.array.non_themeable_packages); + ai.isThemeable = isPackageThemeable(pkgName, nonThemeablePackages); + TypedArray sa = res.obtainAttributes(attrs, com.android.internal.R.styleable.AndroidManifestApplication); @@ -3188,6 +3367,26 @@ private Activity parseActivity(Package owner, Resources res, if (!parseIntent(res, parser, attrs, true, intent, outError)) { return null; } + + // Check if package is a legacy icon pack + if (!owner.mIsLegacyIconPackApk) { + for(String action : ThemeUtils.sSupportedActions) { + if (intent.hasAction(action)) { + owner.mIsLegacyIconPackApk = true; + break; + } + + } + } + if (!owner.mIsLegacyIconPackApk) { + for(String category : ThemeUtils.sSupportedCategories) { + if (intent.hasCategory(category)) { + owner.mIsLegacyIconPackApk = true; + break; + } + } + } + if (intent.countActions() == 0) { Slog.w(TAG, "No actions in intent filter at " + mArchiveSourcePath + " " @@ -4179,6 +4378,22 @@ private boolean parseIntent(Resources res, XmlPullParser parser, AttributeSet at return true; } + /**1 + * Returns whether the specified package is themeable + * @param packageName Name of package to check + * @param nonThemeablePackages Array of packages that are declared as non-themeable + * @return True if the package is themeable, false otherwise + */ + private static boolean isPackageThemeable(String packageName, String[] nonThemeablePackages) { + for (String pkg : nonThemeablePackages) { + if (packageName.startsWith(pkg)) { + return false; + } + } + + return true; + } + /** * Representation of a full package parsed from APK files on disk. A package * consists of a single base APK, and zero or more split APKs. @@ -4269,6 +4484,17 @@ public final static class Package { // For use by package manager to keep track of when a package was last used. public long mLastPackageUsageTimeInMills; + // Is Theme Apk + public boolean mIsThemeApk = false; + public final ArrayList mOverlayTargets = new ArrayList(0); + public Map> mPackageRedirections + = new HashMap>(); + + // Theme info + public ThemeInfo mThemeInfo = null; + + // Legacy icon pack + public boolean mIsLegacyIconPackApk = false; // // User set enabled state. // public int mSetEnabled = PackageManager.COMPONENT_ENABLED_STATE_DEFAULT; @@ -4314,6 +4540,9 @@ public final static class Package { public int mOverlayPriority; public boolean mTrustedOverlay; + public boolean hasIconPack; + public int manifestHashCode; + /** * Data used to feed the KeySetManagerService */ @@ -4627,6 +4856,12 @@ private static boolean copyNeeded(int flags, Package p, && p.usesLibraryFiles != null) { return true; } + if (state.protectedComponents != null) { + boolean protect = state.protectedComponents.size() > 0; + if (p.applicationInfo.protect != protect) { + return true; + } + } return false; } @@ -4660,6 +4895,9 @@ private static void updateApplicationInfo(ApplicationInfo ai, int flags, ai.enabled = false; } ai.enabledSetting = state.enabled; + if (state.protectedComponents != null) { + ai.protect = state.protectedComponents.size() > 0; + } } public static ApplicationInfo generateApplicationInfo(Package p, int flags, diff --git a/core/java/android/content/pm/PackageUserState.java b/core/java/android/content/pm/PackageUserState.java index a9c7be37665..22addb4f280 100644 --- a/core/java/android/content/pm/PackageUserState.java +++ b/core/java/android/content/pm/PackageUserState.java @@ -36,6 +36,8 @@ public class PackageUserState { public ArraySet disabledComponents; public ArraySet enabledComponents; + public ArraySet protectedComponents; + public ArraySet visibleComponents; public PackageUserState() { installed = true; @@ -55,5 +57,9 @@ public PackageUserState(PackageUserState o) { enabledComponents = o.enabledComponents != null ? new ArraySet(o.enabledComponents) : null; blockUninstall = o.blockUninstall; + protectedComponents = o.protectedComponents != null + ? new ArraySet(o.protectedComponents) : null; + visibleComponents = o.visibleComponents != null + ? new ArraySet(o.visibleComponents) : null; } } diff --git a/core/java/android/content/pm/PermissionInfo.java b/core/java/android/content/pm/PermissionInfo.java index 4e7da48b4c9..19ac8eea346 100644 --- a/core/java/android/content/pm/PermissionInfo.java +++ b/core/java/android/content/pm/PermissionInfo.java @@ -129,6 +129,14 @@ public class PermissionInfo extends PackageItemInfo implements Parcelable { */ public CharSequence nonLocalizedDescription; + /** + * Whether this permission will be granted to apps signed with white-listed keys in + * /system/etc/permissions/someapp.xml + * + * @hide + */ + public boolean allowViaWhitelist; + /** @hide */ public static int fixProtectionLevel(int level) { if (level == PROTECTION_SIGNATURE_OR_SYSTEM) { @@ -176,6 +184,7 @@ public PermissionInfo(PermissionInfo orig) { group = orig.group; descriptionRes = orig.descriptionRes; nonLocalizedDescription = orig.nonLocalizedDescription; + allowViaWhitelist = orig.allowViaWhitelist; } /** @@ -218,6 +227,7 @@ public void writeToParcel(Parcel dest, int parcelableFlags) { dest.writeInt(flags); dest.writeString(group); dest.writeInt(descriptionRes); + dest.writeInt(allowViaWhitelist ? 1 : 0); TextUtils.writeToParcel(nonLocalizedDescription, dest, parcelableFlags); } @@ -237,6 +247,7 @@ private PermissionInfo(Parcel source) { flags = source.readInt(); group = source.readString(); descriptionRes = source.readInt(); + allowViaWhitelist = source.readInt() == 1; nonLocalizedDescription = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); } } diff --git a/core/java/android/content/pm/RegisteredServicesCache.java b/core/java/android/content/pm/RegisteredServicesCache.java index 391ef226921..96f0df88546 100644 --- a/core/java/android/content/pm/RegisteredServicesCache.java +++ b/core/java/android/content/pm/RegisteredServicesCache.java @@ -50,6 +50,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -541,7 +542,7 @@ private void readPersistentServicesLocked() { } fis = mPersistentServicesFile.openRead(); XmlPullParser parser = Xml.newPullParser(); - parser.setInput(fis, null); + parser.setInput(fis, StandardCharsets.UTF_8.name()); int eventType = parser.getEventType(); while (eventType != XmlPullParser.START_TAG && eventType != XmlPullParser.END_DOCUMENT) { @@ -591,7 +592,7 @@ private void writePersistentServicesLocked() { try { fos = mPersistentServicesFile.startWrite(); XmlSerializer out = new FastXmlSerializer(); - out.setOutput(fos, "utf-8"); + out.setOutput(fos, StandardCharsets.UTF_8.name()); out.startDocument(null, true); out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); out.startTag(null, "services"); diff --git a/core/java/android/content/pm/ResolveInfo.java b/core/java/android/content/pm/ResolveInfo.java index fe3aec93f0d..bf60b285b44 100644 --- a/core/java/android/content/pm/ResolveInfo.java +++ b/core/java/android/content/pm/ResolveInfo.java @@ -127,6 +127,14 @@ public class ResolveInfo implements Parcelable { */ public String resolvePackageName; + /** + * Optional -- if non-null, the component name of the target of the original + * intent. The ResolveInfo can change the component so this field stores + * the origial target. This can be used for retargetting by the receiver. + * @hide + */ + public ComponentName targetComponentName; + /** * If not equal to UserHandle.USER_CURRENT, then the intent will be forwarded to this user. * @hide diff --git a/core/java/android/content/pm/ThemeInfo.aidl b/core/java/android/content/pm/ThemeInfo.aidl new file mode 100644 index 00000000000..acbc85e9c8b --- /dev/null +++ b/core/java/android/content/pm/ThemeInfo.aidl @@ -0,0 +1,3 @@ +package android.content.pm; + +parcelable ThemeInfo; diff --git a/core/java/android/content/pm/ThemeInfo.java b/core/java/android/content/pm/ThemeInfo.java new file mode 100644 index 00000000000..ab798db7b00 --- /dev/null +++ b/core/java/android/content/pm/ThemeInfo.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2010, T-Mobile USA, Inc. + * + * 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.content.pm; + +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParser; + +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.content.res.Resources; + +/** + * Overall information about "theme" package. This corresponds + * to the information collected from AndroidManifest.xml + * + * Below is an example of the manifest: + * + * + * + * + * @hide + */ +public final class ThemeInfo extends BaseThemeInfo { + + public static final String META_TAG_NAME = "org.cyanogenmod.theme.name"; + public static final String META_TAG_AUTHOR = "org.cyanogenmod.theme.author"; + + public ThemeInfo(Bundle bundle) { + super(); + name = bundle.getString(META_TAG_NAME); + themeId = name; + author = bundle.getString(META_TAG_AUTHOR); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public ThemeInfo createFromParcel(Parcel source) { + return new ThemeInfo(source); + } + + public ThemeInfo[] newArray(int size) { + return new ThemeInfo[size]; + } + }; + + private ThemeInfo(Parcel source) { + super(source); + } +} diff --git a/core/java/android/content/pm/ThemeUtils.java b/core/java/android/content/pm/ThemeUtils.java new file mode 100644 index 00000000000..ef9fca0729d --- /dev/null +++ b/core/java/android/content/pm/ThemeUtils.java @@ -0,0 +1,779 @@ +/* + * Copyright (C) 2014 The CyanogenMod 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.content.pm; + +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.IntentFilter; +import android.content.res.AssetManager; +import android.content.res.Configuration; +import android.content.res.ThemeConfig; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.FileUtils; +import android.os.SystemProperties; +import android.provider.MediaStore; +import android.provider.Settings; +import android.provider.ThemesContract; +import android.provider.ThemesContract.ThemesColumns; +import android.text.TextUtils; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.WindowManager; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.InputStreamReader; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +import static android.content.res.ThemeConfig.SYSTEM_DEFAULT; + +/** + * @hide + */ +public class ThemeUtils { + private static final String TAG = "ThemeUtils"; + + /* Path inside a theme APK to the overlay folder */ + public static final String OVERLAY_PATH = "assets/overlays/"; + public static final String ICONS_PATH = "assets/icons/"; + public static final String COMMON_RES_PATH = "assets/overlays/common/"; + public static final String FONT_XML = "fonts.xml"; + public static final String RESOURCE_CACHE_DIR = "/data/resource-cache/"; + public static final String IDMAP_SUFFIX = "@idmap"; + public static final String COMMON_RES_SUFFIX = ".common"; + public static final String COMMON_RES_TARGET = "common"; + public static final String ICON_HASH_FILENAME = "hash"; + + // path to external theme resources, i.e. bootanimation.zip + public static final String SYSTEM_THEME_PATH = "/data/system/theme"; + public static final String SYSTEM_THEME_FONT_PATH = SYSTEM_THEME_PATH + File.separator + "fonts"; + public static final String SYSTEM_THEME_RINGTONE_PATH = SYSTEM_THEME_PATH + + File.separator + "ringtones"; + public static final String SYSTEM_THEME_NOTIFICATION_PATH = SYSTEM_THEME_PATH + + File.separator + "notifications"; + public static final String SYSTEM_THEME_ALARM_PATH = SYSTEM_THEME_PATH + + File.separator + "alarms"; + public static final String SYSTEM_THEME_ICON_CACHE_DIR = SYSTEM_THEME_PATH + + File.separator + "icons"; + // internal path to bootanimation.zip inside theme apk + public static final String THEME_BOOTANIMATION_PATH = "assets/bootanimation/bootanimation.zip"; + + public static final String SYSTEM_MEDIA_PATH = "/system/media/audio"; + public static final String SYSTEM_ALARMS_PATH = SYSTEM_MEDIA_PATH + File.separator + + "alarms"; + public static final String SYSTEM_RINGTONES_PATH = SYSTEM_MEDIA_PATH + File.separator + + "ringtones"; + public static final String SYSTEM_NOTIFICATIONS_PATH = SYSTEM_MEDIA_PATH + File.separator + + "notifications"; + + // path to asset lockscreen and wallpapers directory + public static final String LOCKSCREEN_WALLPAPER_PATH = "lockscreen"; + public static final String WALLPAPER_PATH = "wallpapers"; + + private static final String MEDIA_CONTENT_URI = "content://media/internal/audio/media"; + + // Constants for theme change broadcast + public static final String ACTION_THEME_CHANGED = "org.cyanogenmod.intent.action.THEME_CHANGED"; + public static final String CATEGORY_THEME_COMPONENT_PREFIX = "org.cyanogenmod.intent.category."; + public static final String EXTRA_COMPONENTS = "components"; + public static final String EXTRA_REQUEST_TYPE = "request_type"; + public static final String EXTRA_UPDATE_TIME = "update_time"; + + public static final int SYSTEM_TARGET_API = 0; + + // Package name for any app which does not have a specific theme applied + private static final String DEFAULT_PKG = "default"; + + private static final String SETTINGS_DB = + "/data/data/com.android.providers.settings/databases/settings.db"; + private static final String SETTINGS_SECURE_TABLE = "secure"; + + /** + * IDMAP hash version code used to alter the resulting hash and force recreating + * of the idmap. This value should be changed whenever there is a need to force + * an update to all idmaps. + */ + private static final byte IDMAP_HASH_VERSION = 3; + + // Actions in manifests which identify legacy icon packs + public static final String[] sSupportedActions = new String[] { + "org.adw.launcher.THEMES", + "com.gau.go.launcherex.theme", + "com.novalauncher.THEME" + }; + + // Categories in manifests which identify legacy icon packs + public static final String[] sSupportedCategories = new String[] { + "com.fede.launcher.THEME_ICONPACK", + "com.anddoes.launcher.THEME", + "com.teslacoilsw.launcher.THEME" + }; + + + /** + * Get the root path of the resource cache for the given theme + * @param themePkgName + * @return Root resource cache path for the given theme + */ + public static String getOverlayResourceCacheDir(String themePkgName) { + return RESOURCE_CACHE_DIR + themePkgName; + } + + /** + * Get the path of the resource cache for the given target and theme + * @param targetPkgName + * @param themePkg + * @return Path to the resource cache for this target and theme + */ + public static String getTargetCacheDir(String targetPkgName, PackageInfo themePkg) { + return getTargetCacheDir(targetPkgName, themePkg.packageName); + } + + public static String getTargetCacheDir(String targetPkgName, PackageParser.Package themePkg) { + return getTargetCacheDir(targetPkgName, themePkg.packageName); + } + + public static String getTargetCacheDir(String targetPkgName, String themePkgName) { + return getOverlayResourceCacheDir(themePkgName) + File.separator + targetPkgName; + } + + /** + * Get the path to the icons for the given theme + * @param pkgName + * @return + */ + public static String getIconPackDir(String pkgName) { + return getOverlayResourceCacheDir(pkgName) + File.separator + "icons"; + } + + public static String getIconHashFile(String pkgName) { + return getIconPackDir(pkgName) + File.separator + ICON_HASH_FILENAME; + } + + public static String getIconPackApkPath(String pkgName) { + return getIconPackDir(pkgName) + "/resources.apk"; + } + + public static String getIconPackResPath(String pkgName) { + return getIconPackDir(pkgName) + "/resources.arsc"; + } + + public static String getIdmapPath(String targetPkgName, String overlayPkgName) { + return getTargetCacheDir(targetPkgName, overlayPkgName) + File.separator + "idmap"; + } + + public static String getOverlayPathToTarget(String targetPkgName) { + StringBuilder sb = new StringBuilder(); + sb.append(OVERLAY_PATH); + sb.append(targetPkgName); + sb.append('/'); + return sb.toString(); + } + + public static String getCommonPackageName(String themePackageName) { + if (TextUtils.isEmpty(themePackageName)) return null; + + return COMMON_RES_TARGET; + } + + public static void createCacheDirIfNotExists() throws IOException { + File file = new File(RESOURCE_CACHE_DIR); + if (!file.exists() && !file.mkdir()) { + throw new IOException("Could not create dir: " + file.toString()); + } + FileUtils.setPermissions(file, FileUtils.S_IRWXU + | FileUtils.S_IRWXG | FileUtils.S_IROTH | FileUtils.S_IXOTH, -1, -1); + } + + public static void createResourcesDirIfNotExists(String targetPkgName, String overlayPkgName) + throws IOException { + createDirIfNotExists(getOverlayResourceCacheDir(overlayPkgName)); + File file = new File(getTargetCacheDir(targetPkgName, overlayPkgName)); + if (!file.exists() && !file.mkdir()) { + throw new IOException("Could not create dir: " + file.toString()); + } + FileUtils.setPermissions(file, FileUtils.S_IRWXU + | FileUtils.S_IRWXG | FileUtils.S_IROTH | FileUtils.S_IXOTH, -1, -1); + } + + public static void createIconDirIfNotExists(String pkgName) throws IOException { + createDirIfNotExists(getOverlayResourceCacheDir(pkgName)); + File file = new File(getIconPackDir(pkgName)); + if (!file.exists() && !file.mkdir()) { + throw new IOException("Could not create dir: " + file.toString()); + } + FileUtils.setPermissions(file, FileUtils.S_IRWXU + | FileUtils.S_IRWXG | FileUtils.S_IROTH | FileUtils.S_IXOTH, -1, -1); + } + + private static boolean dirExists(String dirPath) { + final File dir = new File(dirPath); + return dir.exists() && dir.isDirectory(); + } + + private static void createDirIfNotExists(String dirPath) { + if (!dirExists(dirPath)) { + File dir = new File(dirPath); + if (dir.mkdir()) { + FileUtils.setPermissions(dir, FileUtils.S_IRWXU | + FileUtils.S_IRWXG| FileUtils.S_IROTH | FileUtils.S_IXOTH, -1, -1); + } + } + } + + /** + * Create SYSTEM_THEME_PATH directory if it does not exist + */ + public static void createThemeDirIfNotExists() { + createDirIfNotExists(SYSTEM_THEME_PATH); + } + + /** + * Create SYSTEM_FONT_PATH directory if it does not exist + */ + public static void createFontDirIfNotExists() { + createDirIfNotExists(SYSTEM_THEME_FONT_PATH); + } + + /** + * Create SYSTEM_THEME_RINGTONE_PATH directory if it does not exist + */ + public static void createRingtoneDirIfNotExists() { + createDirIfNotExists(SYSTEM_THEME_RINGTONE_PATH); + } + + /** + * Create SYSTEM_THEME_NOTIFICATION_PATH directory if it does not exist + */ + public static void createNotificationDirIfNotExists() { + createDirIfNotExists(SYSTEM_THEME_NOTIFICATION_PATH); + } + + /** + * Create SYSTEM_THEME_ALARM_PATH directory if it does not exist + */ + public static void createAlarmDirIfNotExists() { + createDirIfNotExists(SYSTEM_THEME_ALARM_PATH); + } + + /** + * Create SYSTEM_THEME_ICON_CACHE_DIR directory if it does not exist + */ + public static void createIconCacheDirIfNotExists() { + createDirIfNotExists(SYSTEM_THEME_ICON_CACHE_DIR); + } + + public static void clearIconCache() { + FileUtils.deleteContents(new File(SYSTEM_THEME_ICON_CACHE_DIR)); + } + + public static InputStream getInputStreamFromAsset(Context ctx, String path) throws IOException { + if (ctx == null || path == null) + return null; + InputStream is = null; + String ASSET_BASE = "file:///android_asset/"; + path = path.substring(ASSET_BASE.length()); + AssetManager assets = ctx.getAssets(); + is = assets.open(path); + return is; + } + + public static void closeQuietly(InputStream stream) { + if (stream == null) + return; + try { + stream.close(); + } catch (IOException e) { + } + } + + public static void closeQuietly(OutputStream stream) { + if (stream == null) + return; + try { + stream.close(); + } catch (IOException e) { + } + } + + /** + * Scale the boot animation to better fit the device by editing the desc.txt found + * in the bootanimation.zip + * @param context Context to use for getting an instance of the WindowManager + * @param input InputStream of the original bootanimation.zip + * @param dst Path to store the newly created bootanimation.zip + * @throws IOException + */ + public static void copyAndScaleBootAnimation(Context context, InputStream input, String dst) + throws IOException { + final OutputStream os = new FileOutputStream(dst); + final ZipOutputStream zos = new ZipOutputStream(new BufferedOutputStream(os)); + final ZipInputStream bootAni = new ZipInputStream(new BufferedInputStream(input)); + ZipEntry ze; + + zos.setMethod(ZipOutputStream.STORED); + final byte[] bytes = new byte[4096]; + int len; + while ((ze = bootAni.getNextEntry()) != null) { + ZipEntry entry = new ZipEntry(ze.getName()); + entry.setMethod(ZipEntry.STORED); + entry.setCrc(ze.getCrc()); + entry.setSize(ze.getSize()); + entry.setCompressedSize(ze.getSize()); + if (!ze.getName().equals("desc.txt")) { + // just copy this entry straight over into the output zip + zos.putNextEntry(entry); + while ((len = bootAni.read(bytes)) > 0) { + zos.write(bytes, 0, len); + } + } else { + String line; + BufferedReader reader = new BufferedReader(new InputStreamReader(bootAni)); + final String[] info = reader.readLine().split(" "); + + int scaledWidth; + int scaledHeight; + WindowManager wm = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); + DisplayMetrics dm = new DisplayMetrics(); + wm.getDefaultDisplay().getRealMetrics(dm); + // just in case the device is in landscape orientation we will + // swap the values since most (if not all) animations are portrait + if (dm.widthPixels > dm.heightPixels) { + scaledWidth = dm.heightPixels; + scaledHeight = dm.widthPixels; + } else { + scaledWidth = dm.widthPixels; + scaledHeight = dm.heightPixels; + } + + int width = Integer.parseInt(info[0]); + int height = Integer.parseInt(info[1]); + + if (width == height) + scaledHeight = scaledWidth; + else { + // adjust scaledHeight to retain original aspect ratio + float scale = (float)scaledWidth / (float)width; + int newHeight = (int)((float)height * scale); + if (newHeight < scaledHeight) + scaledHeight = newHeight; + } + + CRC32 crc32 = new CRC32(); + int size = 0; + ByteBuffer buffer = ByteBuffer.wrap(bytes); + line = String.format("%d %d %s\n", scaledWidth, scaledHeight, info[2]); + buffer.put(line.getBytes()); + size += line.getBytes().length; + crc32.update(line.getBytes()); + while ((line = reader.readLine()) != null) { + line = String.format("%s\n", line); + buffer.put(line.getBytes()); + size += line.getBytes().length; + crc32.update(line.getBytes()); + } + entry.setCrc(crc32.getValue()); + entry.setSize(size); + entry.setCompressedSize(size); + zos.putNextEntry(entry); + zos.write(buffer.array(), 0, size); + } + zos.closeEntry(); + } + zos.close(); + } + + public static boolean isValidAudible(String fileName) { + return (fileName != null && + (fileName.endsWith(".mp3") || fileName.endsWith(".ogg"))); + } + + public static boolean setAudible(Context context, File ringtone, int type, String name) { + final String path = ringtone.getAbsolutePath(); + final String mimeType = name.endsWith(".ogg") ? "audio/ogg" : "audio/mp3"; + ContentValues values = new ContentValues(); + values.put(MediaStore.MediaColumns.DATA, path); + values.put(MediaStore.MediaColumns.TITLE, name); + values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType); + values.put(MediaStore.MediaColumns.SIZE, ringtone.length()); + values.put(MediaStore.Audio.Media.IS_RINGTONE, type == RingtoneManager.TYPE_RINGTONE); + values.put(MediaStore.Audio.Media.IS_NOTIFICATION, + type == RingtoneManager.TYPE_NOTIFICATION); + values.put(MediaStore.Audio.Media.IS_ALARM, type == RingtoneManager.TYPE_ALARM); + values.put(MediaStore.Audio.Media.IS_MUSIC, false); + + Uri uri = MediaStore.Audio.Media.getContentUriForPath(path); + Uri newUri = null; + Cursor c = context.getContentResolver().query(uri, + new String[] {MediaStore.MediaColumns._ID}, + MediaStore.MediaColumns.DATA + "='" + path + "'", + null, null); + if (c != null && c.getCount() > 0) { + c.moveToFirst(); + long id = c.getLong(0); + c.close(); + newUri = Uri.withAppendedPath(Uri.parse(MEDIA_CONTENT_URI), "" + id); + context.getContentResolver().update(uri, values, + MediaStore.MediaColumns._ID + "=" + id, null); + } + if (newUri == null) + newUri = context.getContentResolver().insert(uri, values); + try { + RingtoneManager.setActualDefaultRingtoneUri(context, type, newUri); + } catch (Exception e) { + return false; + } + return true; + } + + public static boolean setDefaultAudible(Context context, int type) { + final String audiblePath = getDefaultAudiblePath(type); + if (audiblePath != null) { + Uri uri = MediaStore.Audio.Media.getContentUriForPath(audiblePath); + Cursor c = context.getContentResolver().query(uri, + new String[] {MediaStore.MediaColumns._ID}, + MediaStore.MediaColumns.DATA + "='" + audiblePath + "'", + null, null); + if (c != null && c.getCount() > 0) { + c.moveToFirst(); + long id = c.getLong(0); + c.close(); + uri = Uri.withAppendedPath( + Uri.parse(MEDIA_CONTENT_URI), "" + id); + } + if (uri != null) + RingtoneManager.setActualDefaultRingtoneUri(context, type, uri); + } else { + return false; + } + return true; + } + + public static String getDefaultAudiblePath(int type) { + final String name; + final String path; + switch (type) { + case RingtoneManager.TYPE_ALARM: + name = SystemProperties.get("ro.config.alarm_alert", null); + path = name != null ? SYSTEM_ALARMS_PATH + File.separator + name : null; + break; + case RingtoneManager.TYPE_NOTIFICATION: + name = SystemProperties.get("ro.config.notification_sound", null); + path = name != null ? SYSTEM_NOTIFICATIONS_PATH + File.separator + name : null; + break; + case RingtoneManager.TYPE_RINGTONE: + name = SystemProperties.get("ro.config.ringtone", null); + path = name != null ? SYSTEM_RINGTONES_PATH + File.separator + name : null; + break; + default: + path = null; + break; + } + return path; + } + + public static void clearAudibles(Context context, String audiblePath) { + final File audibleDir = new File(audiblePath); + if (audibleDir.exists()) { + String[] files = audibleDir.list(); + final ContentResolver resolver = context.getContentResolver(); + for (String s : files) { + final String filePath = audiblePath + File.separator + s; + Uri uri = MediaStore.Audio.Media.getContentUriForPath(filePath); + resolver.delete(uri, MediaStore.MediaColumns.DATA + "=\"" + + filePath + "\"", null); + (new File(filePath)).delete(); + } + } + } + + public static Context createUiContext(final Context context) { + try { + Context uiContext = context.createPackageContext("com.android.systemui", + Context.CONTEXT_RESTRICTED); + return new ThemedUiContext(uiContext, context.getApplicationContext()); + } catch (PackageManager.NameNotFoundException e) { + } + + return null; + } + + public static void registerThemeChangeReceiver(final Context context, + final BroadcastReceiver receiver) { + IntentFilter filter = new IntentFilter(ACTION_THEME_CHANGED); + + context.registerReceiver(receiver, filter); + } + + public static String getLockscreenWallpaperPath(AssetManager assetManager) throws IOException { + String[] assets = assetManager.list(LOCKSCREEN_WALLPAPER_PATH); + String asset = getFirstNonEmptyAsset(assets); + if (asset == null) return null; + return LOCKSCREEN_WALLPAPER_PATH + File.separator + asset; + } + + public static String getWallpaperPath(AssetManager assetManager) throws IOException { + String[] assets = assetManager.list(WALLPAPER_PATH); + String asset = getFirstNonEmptyAsset(assets); + if (asset == null) return null; + return WALLPAPER_PATH + File.separator + asset; + } + + public static List getWallpaperPathList(AssetManager assetManager) + throws IOException { + List wallpaperList = new ArrayList(); + String[] assets = assetManager.list(WALLPAPER_PATH); + for (String asset : assets) { + if (!TextUtils.isEmpty(asset)) { + wallpaperList.add(WALLPAPER_PATH + File.separator + asset); + } + } + return wallpaperList; + } + + // Returns the first non-empty asset name. Empty assets can occur if the APK is built + // with folders included as zip entries in the APK. Searching for files inside "folderName" via + // assetManager.list("folderName") can cause these entries to be included as empty strings. + private static String getFirstNonEmptyAsset(String[] assets) { + if (assets == null) return null; + String filename = null; + for(String asset : assets) { + if (!TextUtils.isEmpty(asset)) { + filename = asset; + break; + } + } + return filename; + } + + public static String getDefaultThemePackageName(Context context) { + final String defaultThemePkg = Settings.Secure.getString(context.getContentResolver(), + Settings.Secure.DEFAULT_THEME_PACKAGE); + if (!TextUtils.isEmpty(defaultThemePkg)) { + PackageManager pm = context.getPackageManager(); + try { + if (pm.getPackageInfo(defaultThemePkg, 0) != null) { + return defaultThemePkg; + } + } catch (PackageManager.NameNotFoundException e) { + // doesn't exist so system will be default + Log.w(TAG, "Default theme " + defaultThemePkg + " not found", e); + } + } + + return SYSTEM_DEFAULT; + } + + private static class ThemedUiContext extends ContextWrapper { + private Context mAppContext; + + public ThemedUiContext(Context context, Context appContext) { + super(context); + mAppContext = appContext; + } + + @Override + public Context getApplicationContext() { + return mAppContext; + } + + @Override + public String getPackageName() { + return mAppContext.getPackageName(); + } + } + + // Returns a mutable list of all theme components + public static List getAllComponents() { + List components = new ArrayList(9); + components.add(ThemesColumns.MODIFIES_FONTS); + components.add(ThemesColumns.MODIFIES_LAUNCHER); + components.add(ThemesColumns.MODIFIES_ALARMS); + components.add(ThemesColumns.MODIFIES_BOOT_ANIM); + components.add(ThemesColumns.MODIFIES_ICONS); + components.add(ThemesColumns.MODIFIES_LOCKSCREEN); + components.add(ThemesColumns.MODIFIES_NOTIFICATIONS); + components.add(ThemesColumns.MODIFIES_OVERLAYS); + components.add(ThemesColumns.MODIFIES_RINGTONES); + components.add(ThemesColumns.MODIFIES_STATUS_BAR); + components.add(ThemesColumns.MODIFIES_NAVIGATION_BAR); + return components; + } + + /** + * Returns a mutable list of all the theme components supported by a given package + * NOTE: This queries the themes content provider. If there isn't a provider installed + * or if it is too early in the boot process this method will not work. + */ + public static List getSupportedComponents(Context context, String pkgName) { + List supportedComponents = new ArrayList(); + + String selection = ThemesContract.ThemesColumns.PKG_NAME + "= ?"; + String[] selectionArgs = new String[]{ pkgName }; + Cursor c = context.getContentResolver().query(ThemesContract.ThemesColumns.CONTENT_URI, + null, selection, selectionArgs, null); + + if (c != null) { + if (c.moveToFirst()) { + List allComponents = getAllComponents(); + for (String component : allComponents) { + int index = c.getColumnIndex(component); + if (c.getInt(index) == 1) { + supportedComponents.add(component); + } + } + } + c.close(); + } + return supportedComponents; + } + + /** + * Get the components from the default theme. If the default theme is not SYSTEM then any + * components that are not in the default theme will come from SYSTEM to create a complete + * component map. + * @param context + * @return + */ + public static Map getDefaultComponents(Context context) { + String defaultThemePkg = getDefaultThemePackageName(context); + List defaultComponents = null; + List systemComponents = getSupportedComponents(context, SYSTEM_DEFAULT); + if (!SYSTEM_DEFAULT.equals(defaultThemePkg)) { + defaultComponents = getSupportedComponents(context, defaultThemePkg); + } + + Map componentMap = new HashMap(systemComponents.size()); + if (defaultComponents != null) { + for (String component : defaultComponents) { + componentMap.put(component, defaultThemePkg); + } + } + for (String component : systemComponents) { + if (!componentMap.containsKey(component)) { + componentMap.put(component, SYSTEM_DEFAULT); + } + } + + return componentMap; + } + + /** + * Takes an existing component map and adds any missing components from the default + * map of components. + * @param context + * @param componentMap An existing component map + */ + public static void completeComponentMap(Context context, + Map componentMap) { + if (componentMap == null) return; + + Map defaultComponents = getDefaultComponents(context); + for (String component : defaultComponents.keySet()) { + if (!componentMap.containsKey(component)) { + componentMap.put(component, defaultComponents.get(component)); + } + } + } + + /** + * Get the boot theme by accessing the settings.db directly instead of using a content resolver. + * Only use this when the system is starting up and the settings content provider is not ready. + * + * Note: This method will only succeed if the system is calling this since normal apps will not + * be able to access the settings db path. + * + * @return The boot theme or null if unable to read the database or get the entry for theme + * config + */ + public static ThemeConfig getBootThemeDirty() { + ThemeConfig config = null; + SQLiteDatabase db = null; + try { + db = SQLiteDatabase.openDatabase(SETTINGS_DB, null, + SQLiteDatabase.OPEN_READONLY); + if (db != null) { + String selection = "name=?"; + String[] selectionArgs = + { Configuration.THEME_PKG_CONFIGURATION_PERSISTENCE_PROPERTY }; + String[] columns = {"value"}; + Cursor c = db.query(SETTINGS_SECURE_TABLE, columns, selection, selectionArgs, + null, null, null); + if (c != null) { + if (c.getCount() > 0) { + c.moveToFirst(); + String json = c.getString(0); + if (json != null) { + config = ThemeConfig.fromJson(json); + } + } + c.close(); + } + } + } catch (Exception e) { + Log.w(TAG, "Unable to open " + SETTINGS_DB, e); + } finally { + if (db != null) { + db.close(); + } + } + + return config; + } + + /** + * Convenience method to determine if a theme component is a per app theme and not a standard + * component. + * @param component + * @return + */ + public static boolean isPerAppThemeComponent(String component) { + return !(DEFAULT_PKG.equals(component) + || ThemeConfig.SYSTEMUI_STATUS_BAR_PKG.equals(component) + || ThemeConfig.SYSTEMUI_NAVBAR_PKG.equals(component)); + } + + /** + * Get a 32 bit hashcode for the given package. + * @param pkg + * @return + */ + public static int getPackageHashCode(PackageParser.Package pkg) { + int hash = pkg.manifestDigest != null ? pkg.manifestDigest.hashCode() : 0; + hash = 31 * hash + IDMAP_HASH_VERSION; + return hash; + } +} diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java index ecae52c3cd6..4472f142d74 100644 --- a/core/java/android/content/res/AssetManager.java +++ b/core/java/android/content/res/AssetManager.java @@ -21,9 +21,11 @@ import android.util.SparseArray; import android.util.TypedValue; +import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.HashMap; /** @@ -77,6 +79,16 @@ public final class AssetManager implements AutoCloseable { private boolean mOpen = true; private HashMap mRefStacks; + private String mAppName; + + private boolean mThemeSupport; + private String mThemePackageName; + private String mIconPackageName; + private String mCommonResPackageName; + private ArrayList mThemeCookies = new ArrayList(2); + private int mIconPackCookie; + private int mCommonResCookie; + /** * Create a new AssetManager containing only the basic system assets. * Applications will not generally use this method, instead retrieving the @@ -252,6 +264,12 @@ public void close() { } } + /*package*/ final void recreateStringBlocks() { + synchronized (this) { + makeStringBlocks(sSystem.mStringBlocks); + } + } + /*package*/ final void makeStringBlocks(StringBlock[] seed) { final int seedNum = (seed != null) ? seed.length : 0; final int num = getStringBlockCount(); @@ -612,7 +630,9 @@ protected void finalize() throws Throwable public final int addAssetPath(String path) { synchronized (this) { int res = addAssetPathNative(path); - makeStringBlocks(mStringBlocks); + if (mStringBlocks != null) { + makeStringBlocks(mStringBlocks); + } return res; } } @@ -627,7 +647,68 @@ public final int addAssetPath(String path) { * * {@hide} */ - public native final int addOverlayPath(String idmapPath); + // TODO: change the signature of this method to match addOverlayPathNative + public final int addOverlayPath(String idmapPath, String themeApkPath, + String resApkPath, String targetPkgPath, String prefixPath) { + synchronized (this) { + return addOverlayPathNative(idmapPath, themeApkPath, resApkPath, targetPkgPath, + prefixPath); + } + } + + private native final int addOverlayPathNative(String idmapPath, String themeApkPath, + String resApkPath, String targetPkgPath, String prefixPath); + + /** + * Add a set of common assets. + * + * {@hide} + */ + public final int addCommonOverlayPath(String themeApkPath, + String resApkPath, String prefixPath) { + synchronized (this) { + return addCommonOverlayPathNative(themeApkPath, resApkPath, prefixPath); + } + } + + private native final int addCommonOverlayPathNative(String themeApkPath, + String resApkPath, String prefixPath); + + /** + * Add a set of assets as an icon pack. A pkgIdOverride value will change the package's id from + * what is in the resource table to a new value. Manage this carefully, if icon pack has more + * than one package then that next package's id will use pkgIdOverride+1. + * + * Icon packs are different from overlays as they have a different pkg id and + * do not use idmap so no targetPkg is required + * + * {@hide} + */ + public final int addIconPath(String idmapPath, String resApkPath, + String prefixPath, int pkgIdOverride) { + synchronized (this) { + return addIconPathNative(idmapPath, resApkPath, prefixPath, pkgIdOverride); + } + } + + private native final int addIconPathNative(String idmapPath, + String resApkPath, String prefixPath, int pkgIdOverride); + + /** + * Delete a set of overlay assets from the asset manager. Not for use by + * applications. Returns true if succeeded or false on failure. + * + * Also works for icon packs + * + * {@hide} + */ + public final boolean removeOverlayPath(String packageName, int cookie) { + synchronized (this) { + return removeOverlayPathNative(packageName, cookie); + } + } + + private native final boolean removeOverlayPathNative(String packageName, int cookie); /** * Add multiple sets of assets to the asset manager at once. See @@ -649,6 +730,126 @@ public final int[] addAssetPaths(String[] paths) { return cookies; } + /** + * Sets a flag indicating that this AssetManager should have themes + * attached, according to the initial request to create it by the + * ApplicationContext. + * + * {@hide} + */ + public final void setThemeSupport(boolean themeSupport) { + mThemeSupport = themeSupport; + } + + /** + * Should this AssetManager have themes attached, according to the initial + * request to create it by the ApplicationContext? + * + * {@hide} + */ + public final boolean hasThemeSupport() { + return mThemeSupport; + } + + /** + * Get package name of current icon pack (may return null). + * {@hide} + */ + public String getIconPackageName() { + return mIconPackageName; + } + + /** + * Sets icon package name + * {@hide} + */ + public void setIconPackageName(String packageName) { + mIconPackageName = packageName; + } + + /** + * Get package name of current common resources (may return null). + * {@hide} + */ + public String getCommonResPackageName() { + return mCommonResPackageName; + } + + /** + * Sets common resources package name + * {@hide} + */ + public void setCommonResPackageName(String packageName) { + mCommonResPackageName = packageName; + } + + /** + * Get package name of current theme (may return null). + * {@hide} + */ + public String getThemePackageName() { + return mThemePackageName; + } + + /** + * Sets package name and highest level style id for current theme (null, 0 is allowed). + * {@hide} + */ + public void setThemePackageName(String packageName) { + mThemePackageName = packageName; + } + + /** + * Get asset cookie for current theme (may return 0). + * {@hide} + */ + public ArrayList getThemeCookies() { + return mThemeCookies; + } + + /** {@hide} */ + public void setIconPackCookie(int cookie) { + mIconPackCookie = cookie; + } + + /** {@hide} */ + public int getIconPackCookie() { + return mIconPackCookie; + } + + /** {@hide} */ + public void setCommonResCookie(int cookie) { + mCommonResCookie = cookie; + } + + /** {@hide} */ + public int getCommonResCookie() { + return mCommonResCookie; + } + + /** + * Sets asset cookie for current theme (0 if not a themed asset manager). + * {@hide} + */ + public void addThemeCookie(int cookie) { + mThemeCookies.add(cookie); + } + + /** {@hide} */ + public String getAppName() { + return mAppName; + } + + /** {@hide} */ + public void setAppName(String pkgName) { + mAppName = pkgName; + } + + /** {@hide} */ + public boolean hasThemedAssets() { + return mThemeCookies.size() > 0; + } + /** * Determine whether the state in this asset manager is up-to-date with * the files on the filesystem. If false is returned, you need to @@ -686,7 +887,7 @@ public native final void setConfiguration(int mcc, int mnc, String locale, int orientation, int touchscreen, int density, int keyboard, int keyboardHidden, int navigation, int screenWidth, int screenHeight, int smallestScreenWidthDp, int screenWidthDp, int screenHeightDp, - int screenLayout, int uiMode, int majorVersion); + int screenLayout, int uiThemeMode, int uiMode, int majorVersion); /** * Retrieve the resource identifier for the given resource name. @@ -784,6 +985,26 @@ private native final int loadResourceBagValue(int ident, int bagEntryId, TypedVa /*package*/ native final int[] getStyleAttributes(int themeRes); private native final void init(boolean isSystem); + /** + * {@hide} + */ + public native final int getBasePackageCount(); + + /** + * {@hide} + */ + public native final String getBasePackageName(int index); + + /** + * {@hide} + */ + public native final String getBaseResourcePackageName(int index); + + /** + * {@hide} + */ + public native final int getBasePackageId(int index); + private native final void destroy(); private final void incRefsLocked(long id) { diff --git a/core/java/android/content/res/CompatibilityInfo.java b/core/java/android/content/res/CompatibilityInfo.java index da35ee92267..2b8951e0f8c 100644 --- a/core/java/android/content/res/CompatibilityInfo.java +++ b/core/java/android/content/res/CompatibilityInfo.java @@ -241,8 +241,7 @@ public CompatibilityInfo(ApplicationInfo appInfo, int screenLayout, int sw, mCompatibilityFlags = compatFlags; } - private CompatibilityInfo(int compFlags, - int dens, float scale, float invertedScale) { + private CompatibilityInfo(int compFlags, int dens, float scale, float invertedScale) { mCompatibilityFlags = compFlags; applicationDensity = dens; applicationScale = scale; diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java index 14af58481f0..11d9be88a5f 100644 --- a/core/java/android/content/res/Configuration.java +++ b/core/java/android/content/res/Configuration.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2008 The Android Open Source Project + * This code has been modified. Portions copyright (C) 2010, T-Mobile USA, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -81,6 +82,11 @@ public final class Configuration implements Parcelable, Comparable